Skip to content

Commit 06b7450

Browse files
ogabrielluizanovazzi1autofix-ci[bot]
authored
feat: Enhance tool mapping and output rendering with animations (langflow-ai#4481)
* Enhance tool block mapping by using unique tool keys with name and run_id * Enhance tool output rendering with Markdown and JSON formatting in ContentDisplay component * Add animations for block title and content separators in ContentBlockDisplay component * Allow 'size' prop to accept string values and update styling in BorderTrail component * Adjust BorderTrail animation size and duration based on expansion state * fix both borders trailing at the same time * [autofix.ci] apply automated fixes * fix text sizing * fix spacing issues * Adjust header title and text styling in ContentBlockDisplay and DurationDisplay components * Refactor header title in ContentBlockDisplay component * [autofix.ci] apply automated fixes * Convert `test_handle_on_tool_start` to an async function and update tool content key logic * Handle logger without 'opt' method in code parsing error handling * Update test duration values in .test_durations file --------- Co-authored-by: anovazzi1 <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 1188812 commit 06b7450

File tree

8 files changed

+882
-702
lines changed

8 files changed

+882
-702
lines changed

src/backend/base/langflow/base/agents/events.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def handle_on_tool_start(
105105
tool_name = event["name"]
106106
tool_input = event["data"].get("input")
107107
run_id = event.get("run_id", "")
108+
tool_key = f"{tool_name}_{run_id}"
108109

109110
# Create content blocks if they don't exist
110111
if not agent_message.content_blocks:
@@ -122,11 +123,11 @@ def handle_on_tool_start(
122123
)
123124

124125
# Store in map and append to message
125-
tool_blocks_map[run_id] = tool_content
126+
tool_blocks_map[tool_key] = tool_content
126127
agent_message.content_blocks[0].contents.append(tool_content)
127128

128129
agent_message = send_message_method(message=agent_message)
129-
tool_blocks_map[run_id] = agent_message.content_blocks[0].contents[-1]
130+
tool_blocks_map[tool_key] = agent_message.content_blocks[0].contents[-1]
130131
return agent_message, start_time
131132

132133

@@ -138,7 +139,9 @@ def handle_on_tool_end(
138139
start_time: float,
139140
) -> tuple[Message, float]:
140141
run_id = event.get("run_id", "")
141-
tool_content = tool_blocks_map.get(run_id)
142+
tool_name = event.get("name", "")
143+
tool_key = f"{tool_name}_{run_id}"
144+
tool_content = tool_blocks_map.get(tool_key)
142145

143146
if tool_content and isinstance(tool_content, ToolContent):
144147
tool_content.output = event["data"].get("output")
@@ -159,7 +162,9 @@ def handle_on_tool_error(
159162
start_time: float,
160163
) -> tuple[Message, float]:
161164
run_id = event.get("run_id", "")
162-
tool_content = tool_blocks_map.get(run_id)
165+
tool_name = event.get("name", "")
166+
tool_key = f"{tool_name}_{run_id}"
167+
tool_content = tool_blocks_map.get(tool_key)
163168

164169
if tool_content and isinstance(tool_content, ToolContent):
165170
tool_content.error = event["data"].get("error", "Unknown error")

src/backend/base/langflow/utils/validate.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def validate_code(code):
2929
try:
3030
tree = ast.parse(code)
3131
except Exception as e: # noqa: BLE001
32-
logger.opt(exception=True).debug("Error parsing code")
32+
if hasattr(logger, "opt"):
33+
logger.opt(exception=True).debug("Error parsing code")
34+
else:
35+
logger.debug("Error parsing code")
3336
errors["function"]["errors"].append(str(e))
3437
return errors
3538

src/backend/tests/.test_durations

+719-651
Large diffs are not rendered by default.

src/backend/tests/unit/components/agents/test_agent_events.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def __init__(self):
392392
assert isinstance(start_time, float)
393393

394394

395-
def test_handle_on_tool_start():
395+
async def test_handle_on_tool_start():
396396
"""Test handle_on_tool_start event."""
397397
send_message = MagicMock(side_effect=lambda message: message)
398398
tool_blocks_map = {}
@@ -414,8 +414,9 @@ def test_handle_on_tool_start():
414414

415415
assert len(updated_message.content_blocks) == 1
416416
assert len(updated_message.content_blocks[0].contents) > 0
417+
tool_key = f"{event['name']}_{event['run_id']}"
417418
tool_content = updated_message.content_blocks[0].contents[-1]
418-
assert tool_content == tool_blocks_map.get("test_run")
419+
assert tool_content == tool_blocks_map.get(tool_key)
419420
assert isinstance(tool_content, ToolContent)
420421
assert tool_content.name == "test_tool"
421422
assert tool_content.tool_input == {"query": "tool input"}
@@ -452,6 +453,7 @@ async def test_handle_on_tool_end():
452453

453454
updated_message, start_time = handle_on_tool_end(end_event, agent_message, tool_blocks_map, send_message, 0.0)
454455

456+
f"{end_event['name']}_{end_event['run_id']}"
455457
tool_content = updated_message.content_blocks[0].contents[-1]
456458
assert tool_content.name == "test_tool"
457459
assert tool_content.output == "tool output"

src/frontend/src/components/chatComponents/ContentBlockDisplay.tsx

+54-28
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ export function ContentBlockDisplay({
4040
contentBlocks[0]?.contents[contentBlocks[0]?.contents.length - 1];
4141
const headerIcon =
4242
state === "partial" ? lastContent?.header?.icon || "Bot" : "Bot";
43+
4344
const headerTitle =
44-
(state === "partial"
45-
? lastContent?.header?.title
46-
: contentBlocks[0]?.title) || "Steps";
45+
state === "partial" ? (lastContent?.header?.title ?? "Steps") : "Finished";
46+
// show the block title only if state === "partial"
47+
const showBlockTitle = state === "partial";
4748

4849
return (
4950
<div className="relative py-3">
@@ -61,11 +62,10 @@ export function ContentBlockDisplay({
6162
>
6263
{isLoading && (
6364
<BorderTrail
64-
className="bg-zinc-600 opacity-50 dark:bg-zinc-400"
65-
size={60}
65+
size={100}
6666
transition={{
6767
repeat: Infinity,
68-
duration: 2,
68+
duration: 10,
6969
ease: "linear",
7070
}}
7171
/>
@@ -92,7 +92,7 @@ export function ContentBlockDisplay({
9292
<Markdown
9393
remarkPlugins={[remarkGfm]}
9494
rehypePlugins={[rehypeMathjax]}
95-
className="inline-block w-fit max-w-full font-semibold text-primary"
95+
className="inline-block w-fit max-w-full text-[14px] font-semibold text-primary"
9696
>
9797
{headerTitle}
9898
</Markdown>
@@ -139,33 +139,59 @@ export function ContentBlockDisplay({
139139
animate={{ opacity: 1 }}
140140
transition={{ duration: 0.2, delay: 0.1 }}
141141
className={cn(
142-
"relative p-4",
142+
"relative",
143143
index !== contentBlocks.length - 1 &&
144144
"border-b border-border",
145145
)}
146146
>
147-
<div className="mb-2 font-medium">
148-
<Markdown
149-
remarkPlugins={[remarkGfm]}
150-
linkTarget="_blank"
151-
rehypePlugins={[rehypeMathjax]}
152-
components={{
153-
p({ node, ...props }) {
154-
return (
155-
<span className="inline">{props.children}</span>
156-
);
157-
},
158-
}}
159-
>
160-
{block.title}
161-
</Markdown>
162-
</div>
147+
<AnimatePresence>
148+
{showBlockTitle && (
149+
<motion.div
150+
initial={{ opacity: 0, height: 0, marginBottom: 0 }}
151+
animate={{
152+
opacity: 1,
153+
height: "auto",
154+
marginBottom: 8,
155+
}}
156+
exit={{ opacity: 0, height: 0, marginBottom: 0 }}
157+
transition={{ duration: 0.2 }}
158+
className="overflow-hidden font-medium"
159+
>
160+
<Markdown
161+
className="text-[14px] font-semibold text-foreground"
162+
remarkPlugins={[remarkGfm]}
163+
linkTarget="_blank"
164+
rehypePlugins={[rehypeMathjax]}
165+
components={{
166+
p({ node, ...props }) {
167+
return (
168+
<span className="inline">{props.children}</span>
169+
);
170+
},
171+
}}
172+
>
173+
{block.title}
174+
</Markdown>
175+
</motion.div>
176+
)}
177+
</AnimatePresence>
163178
<div className="text-sm text-muted-foreground">
164179
{block.contents.map((content, index) => (
165-
<>
166-
<Separator orientation="horizontal" className="my-2" />
167-
<ContentDisplay key={index} content={content} />
168-
</>
180+
<motion.div key={index}>
181+
<AnimatePresence>
182+
{index !== 0 && (
183+
<motion.div
184+
initial={{ opacity: 0 }}
185+
animate={{ opacity: 1 }}
186+
exit={{ opacity: 0 }}
187+
transition={{ duration: 0.2 }}
188+
>
189+
<Separator orientation="horizontal" />
190+
</motion.div>
191+
)}
192+
</AnimatePresence>
193+
<ContentDisplay content={content} />
194+
</motion.div>
169195
))}
170196
</div>
171197
</motion.div>

src/frontend/src/components/chatComponents/ContentDisplay.tsx

+86-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function ContentDisplay({ content }: { content: ContentType }) {
1212
// First render the common BaseContent elements if they exist
1313
const renderHeader = content.header && (
1414
<>
15-
<div className="flex items-center gap-2">
15+
<div className="flex items-center gap-2 pb-[12px]">
1616
{content.header.icon && (
1717
<ForwardedIconComponent
1818
name={content.header.icon}
@@ -25,7 +25,7 @@ export default function ContentDisplay({ content }: { content: ContentType }) {
2525
<Markdown
2626
remarkPlugins={[remarkGfm]}
2727
rehypePlugins={[rehypeMathjax]}
28-
className="inline-block w-fit max-w-full"
28+
className="inline-block w-fit max-w-full text-[14px] font-semibold text-foreground"
2929
>
3030
{content.header.title}
3131
</Markdown>
@@ -35,7 +35,7 @@ export default function ContentDisplay({ content }: { content: ContentType }) {
3535
</>
3636
);
3737
const renderDuration = content.duration !== undefined && (
38-
<div className="absolute right-2 top-0">
38+
<div className="absolute right-2 top-4">
3939
<DurationDisplay duration={content.duration} />
4040
</div>
4141
);
@@ -54,7 +54,7 @@ export default function ContentDisplay({ content }: { content: ContentType }) {
5454
components={{
5555
p({ node, ...props }) {
5656
return (
57-
<span className="inline-block w-fit max-w-full">
57+
<span className="block w-fit max-w-full">
5858
{props.children}
5959
</span>
6060
);
@@ -135,16 +135,91 @@ export default function ContentDisplay({ content }: { content: ContentType }) {
135135
break;
136136

137137
case "tool_use":
138+
const formatToolOutput = (output: any) => {
139+
if (output === null || output === undefined) return "";
140+
141+
// If it's a string, render as markdown
142+
if (typeof output === "string") {
143+
return (
144+
<Markdown
145+
remarkPlugins={[remarkGfm]}
146+
rehypePlugins={[rehypeMathjax]}
147+
className="markdown prose max-w-full text-[14px] font-normal dark:prose-invert"
148+
components={{
149+
pre({ node, ...props }) {
150+
return <>{props.children}</>;
151+
},
152+
code: ({ node, inline, className, children, ...props }) => {
153+
const match = /language-(\w+)/.exec(className || "");
154+
return !inline ? (
155+
<SimplifiedCodeTabComponent
156+
language={(match && match[1]) || ""}
157+
code={String(children).replace(/\n$/, "")}
158+
/>
159+
) : (
160+
<code className={className} {...props}>
161+
{children}
162+
</code>
163+
);
164+
},
165+
}}
166+
>
167+
{output}
168+
</Markdown>
169+
);
170+
}
171+
172+
// For objects/arrays, format as JSON
173+
try {
174+
return (
175+
<CodeBlock
176+
language="json"
177+
value={JSON.stringify(output, null, 2)}
178+
/>
179+
);
180+
} catch {
181+
return String(output);
182+
}
183+
};
184+
138185
contentData = (
139-
<div>
140-
{content.name && <div>Tool: {content.name}</div>}
141-
<div>Input: {JSON.stringify(content.tool_input, null, 2)}</div>
142-
{content.output && (
143-
<div>Output: {JSON.stringify(content.output)}</div>
186+
<div className="flex flex-col gap-2">
187+
<Markdown
188+
remarkPlugins={[remarkGfm]}
189+
rehypePlugins={[rehypeMathjax]}
190+
className="markdown prose max-w-full text-[14px] font-normal dark:prose-invert"
191+
>
192+
{`${content.name ? `**Tool:** ${content.name}\n\n` : ""}**Input:**`}
193+
</Markdown>
194+
<CodeBlock
195+
language="json"
196+
value={JSON.stringify(content.tool_input, null, 2)}
197+
/>
198+
{content.output !== undefined && (
199+
<>
200+
<Markdown
201+
remarkPlugins={[remarkGfm]}
202+
rehypePlugins={[rehypeMathjax]}
203+
className="markdown prose max-w-full text-[14px] font-normal dark:prose-invert"
204+
>
205+
**Output:**
206+
</Markdown>
207+
<div className="mt-1">{formatToolOutput(content.output)}</div>
208+
</>
144209
)}
145210
{content.error && (
146211
<div className="text-red-500">
147-
Error: {JSON.stringify(content.error)}
212+
<Markdown
213+
remarkPlugins={[remarkGfm]}
214+
rehypePlugins={[rehypeMathjax]}
215+
className="markdown prose max-w-full text-[14px] font-normal dark:prose-invert"
216+
>
217+
**Error:**
218+
</Markdown>
219+
<CodeBlock
220+
language="json"
221+
value={JSON.stringify(content.error, null, 2)}
222+
/>
148223
</div>
149224
)}
150225
</div>
@@ -168,7 +243,7 @@ export default function ContentDisplay({ content }: { content: ContentType }) {
168243
}
169244

170245
return (
171-
<div className="relative">
246+
<div className="relative p-[16px]">
172247
{renderHeader}
173248
{renderDuration}
174249
{contentData}

src/frontend/src/components/chatComponents/DurationDisplay.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function DurationDisplay({ duration }: { duration?: number }) {
5353
bounce: 0,
5454
duration: 300,
5555
}}
56-
className="tabular-nums"
56+
className="text-[11px] font-bold tabular-nums"
5757
/>
5858
</div>
5959
</div>

src/frontend/src/components/core/border-trail.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
"use client";
21
import { cn } from "@/utils/utils";
32
import { motion, Transition } from "framer-motion";
43

54
type BorderTrailProps = {
65
className?: string;
7-
size?: number;
6+
size?: number | string;
87
transition?: Transition;
98
delay?: number;
109
onAnimationComplete?: () => void;
@@ -28,10 +27,12 @@ export function BorderTrail({
2827
return (
2928
<div className="pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]">
3029
<motion.div
31-
className={cn("absolute aspect-square bg-zinc-500", className)}
30+
className={cn("absolute bg-zinc-500", className)}
3231
style={{
3332
width: size,
34-
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
33+
offsetPath: `rect(0 auto auto 0 round 18px)`,
34+
boxShadow:
35+
"0px 0px 20px 5px rgb(255 255 255 / 90%), 0 0 30px 10px rgb(0 0 0 / 90%)",
3536
...style,
3637
}}
3738
animate={{

0 commit comments

Comments
 (0)