@@ -317,9 +317,11 @@ def test_end_trace_updates_and_ends_span(self, mock_langfuse):
317317 mock_langfuse ["child_span" ].update .assert_called ()
318318 mock_langfuse ["child_span" ].end .assert_called ()
319319
320- def test_end_updates_root_span_and_trace (self , mock_langfuse ):
321- """Test that end() updates both root span and trace, then ends."""
320+ def test_end_exposes_stable_evaluator_trace_input_and_output (self , mock_langfuse ):
321+ """Trace input/output should expose stable JSONPath keys with the flow boundary messages."""
322+ from langflow .serialization .serialization import serialize
322323 from langflow .services .tracing .langfuse import LangFuseTracer
324+ from lfx .schema .message import Message
323325
324326 tracer = LangFuseTracer (
325327 trace_name = "test-flow - flow-123" ,
@@ -328,18 +330,197 @@ def test_end_updates_root_span_and_trace(self, mock_langfuse):
328330 trace_id = uuid .uuid4 (),
329331 )
330332
333+ component_inputs = {
334+ "Config (config-id)" : {"model" : "test-model" },
335+ "Chat Input (chat-input-id)" : {"input_value" : "What is Langflow?" , "sender" : "User" },
336+ "Prompt (prompt-id)" : {"template" : "Answer the user" },
337+ }
338+ component_outputs = {
339+ "Chat Input (chat-input-id)" : {"message" : Message (text = "What is Langflow?" )},
340+ "Agent (agent-id)" : {"response" : Message (text = "An intermediate response" )},
341+ "Chat Output (chat-output-id)" : {"message" : Message (text = "Langflow is a visual workflow builder." )},
342+ "Audit Sink (audit-id)" : {"record" : {"status" : "stored" }},
343+ }
344+ tracer .add_trace (
345+ trace_id = "chat-input-id" ,
346+ trace_name = "Chat Input (chat-input-id)" ,
347+ trace_type = "chain" ,
348+ inputs = component_inputs ["Chat Input (chat-input-id)" ],
349+ vertex = MagicMock (is_input = True , is_output = False ),
350+ )
351+ tracer .add_trace (
352+ trace_id = "chat-output-id" ,
353+ trace_name = "Chat Output (chat-output-id)" ,
354+ trace_type = "chain" ,
355+ inputs = {"input_value" : "Langflow is a visual workflow builder." },
356+ vertex = MagicMock (is_input = False , is_output = True ),
357+ )
331358 tracer .end (
332- inputs = { "flow_input" : "test" } ,
333- outputs = { "flow_output" : "result" } ,
359+ inputs = component_inputs ,
360+ outputs = component_outputs ,
334361 metadata = {"final" : True },
335362 )
336363
337- # Should update root span
338- mock_langfuse ["root_span" ].update .assert_called ()
339- # Should update trace metadata
340- assert mock_langfuse ["root_span" ].update_trace .call_count >= 2 # init + end
341- # Should end root span
342- mock_langfuse ["root_span" ].end .assert_called ()
364+ root_update = mock_langfuse ["root_span" ].update .call_args .kwargs
365+ assert root_update ["input" ] == serialize (component_inputs )
366+ assert root_update ["output" ] == serialize (component_outputs )
367+
368+ trace_update = mock_langfuse ["root_span" ].update_trace .call_args .kwargs
369+ assert trace_update ["input" ] == {"input" : "What is Langflow?" }
370+ assert trace_update ["output" ] == {"output" : "Langflow is a visual workflow builder." }
371+ assert trace_update ["metadata" ] == {"final" : True }
372+ mock_langfuse ["root_span" ].end .assert_called_once ()
373+
374+ def test_end_exposes_stable_trace_io_for_marked_arbitrary_graphs (self , mock_langfuse ):
375+ """Marked non-chat boundaries should expose their structured input and output values."""
376+ from langflow .services .tracing .langfuse import LangFuseTracer
377+
378+ tracer = LangFuseTracer (
379+ trace_name = "test-flow - flow-123" ,
380+ trace_type = "chain" ,
381+ project_name = "test-project" ,
382+ trace_id = uuid .uuid4 (),
383+ )
384+
385+ component_inputs = {
386+ "Webhook (webhook-id)" : {"payload" : {"order_id" : 42 }, "method" : "POST" },
387+ "Transform (transform-id)" : {"mapping" : "order" },
388+ }
389+ component_outputs = {
390+ "Webhook (webhook-id)" : {},
391+ "Transform (transform-id)" : {"record" : {"order_id" : 42 , "status" : "ready" }},
392+ "Data Output (data-output-id)" : {"data" : [{"order_id" : 42 , "status" : "ready" }]},
393+ }
394+ tracer .add_trace (
395+ trace_id = "webhook-id" ,
396+ trace_name = "Webhook (webhook-id)" ,
397+ trace_type = "chain" ,
398+ inputs = component_inputs ["Webhook (webhook-id)" ],
399+ vertex = MagicMock (is_input = True , is_output = False ),
400+ )
401+ tracer .add_trace (
402+ trace_id = "data-output-id" ,
403+ trace_name = "Data Output (data-output-id)" ,
404+ trace_type = "chain" ,
405+ inputs = {},
406+ vertex = MagicMock (is_input = False , is_output = True ),
407+ )
408+ tracer .end (inputs = component_inputs , outputs = component_outputs )
409+
410+ trace_update = mock_langfuse ["root_span" ].update_trace .call_args .kwargs
411+ assert trace_update ["input" ] == {"input" : component_inputs ["Webhook (webhook-id)" ]}
412+ assert trace_update ["output" ] == {"output" : [{"order_id" : 42 , "status" : "ready" }]}
413+
414+ def test_end_falls_back_to_stable_aggregate_when_boundaries_are_unmarked (self , mock_langfuse ):
415+ """Custom graphs without boundary markers should retain their complete structured aggregates."""
416+ from langflow .services .tracing .langfuse import LangFuseTracer
417+
418+ tracer = LangFuseTracer (
419+ trace_name = "test-flow - flow-123" ,
420+ trace_type = "chain" ,
421+ project_name = "test-project" ,
422+ trace_id = uuid .uuid4 (),
423+ )
424+ component_inputs = {"Custom Source (source-id)" : {"payload" : {"order_id" : 42 }}}
425+ component_outputs = {"Custom Sink (sink-id)" : {"record" : {"order_id" : 42 , "status" : "ready" }}}
426+
427+ tracer .end (inputs = component_inputs , outputs = component_outputs )
428+
429+ trace_update = mock_langfuse ["root_span" ].update_trace .call_args .kwargs
430+ assert trace_update ["input" ] == {"input" : component_inputs }
431+ assert trace_update ["output" ] == {"output" : component_outputs }
432+
433+ def test_end_orders_multiple_boundary_outputs_by_component_id (self , mock_langfuse ):
434+ """Multiple graph outputs should be deterministic even when their completion order is not."""
435+ from langflow .services .tracing .langfuse import LangFuseTracer
436+ from lfx .schema .message import Message
437+
438+ tracer = LangFuseTracer (
439+ trace_name = "test-flow - flow-123" ,
440+ trace_type = "chain" ,
441+ project_name = "test-project" ,
442+ trace_id = uuid .uuid4 (),
443+ )
444+ component_outputs = {
445+ "Data Output (z-output-id)" : {"data" : {"order_id" : 42 }},
446+ "Text Output (a-output-id)" : {"text" : Message (text = "ready" )},
447+ }
448+ tracer .add_trace (
449+ trace_id = "z-output-id" ,
450+ trace_name = "Data Output (z-output-id)" ,
451+ trace_type = "chain" ,
452+ inputs = {},
453+ vertex = MagicMock (is_input = False , is_output = True ),
454+ )
455+ tracer .add_trace (
456+ trace_id = "a-output-id" ,
457+ trace_name = "Text Output (a-output-id)" ,
458+ trace_type = "chain" ,
459+ inputs = {},
460+ vertex = MagicMock (is_input = False , is_output = True ),
461+ )
462+
463+ tracer .end (inputs = {}, outputs = component_outputs )
464+
465+ trace_update = mock_langfuse ["root_span" ].update_trace .call_args .kwargs
466+ assert trace_update ["output" ] == {"output" : ["ready" , {"order_id" : 42 }]}
467+
468+ def test_end_uses_component_input_for_dual_role_boundary (self , mock_langfuse ):
469+ """A component marked as both input and output should preserve the original request."""
470+ from langflow .services .tracing .langfuse import LangFuseTracer
471+ from lfx .schema .message import Message
472+
473+ tracer = LangFuseTracer (
474+ trace_name = "test-flow - flow-123" ,
475+ trace_type = "chain" ,
476+ project_name = "test-project" ,
477+ trace_id = uuid .uuid4 (),
478+ )
479+ trace_name = "Bidirectional (component-id)"
480+ tracer .add_trace (
481+ trace_id = "component-id" ,
482+ trace_name = trace_name ,
483+ trace_type = "chain" ,
484+ inputs = {"request" : "ping" },
485+ vertex = MagicMock (is_input = True , is_output = True ),
486+ )
487+
488+ tracer .end (
489+ inputs = {trace_name : {"request" : "ping" }},
490+ outputs = {trace_name : {"response" : Message (text = "pong" )}},
491+ )
492+
493+ trace_update = mock_langfuse ["root_span" ].update_trace .call_args .kwargs
494+ assert trace_update ["input" ] == {"input" : "ping" }
495+ assert trace_update ["output" ] == {"output" : "pong" }
496+
497+ def test_end_normalizes_messages_nested_in_multi_output_boundary (self , mock_langfuse ):
498+ """Message values should become evaluator-ready text without dropping sibling outputs."""
499+ from langflow .services .tracing .langfuse import LangFuseTracer
500+ from lfx .schema .message import Message
501+
502+ tracer = LangFuseTracer (
503+ trace_name = "test-flow - flow-123" ,
504+ trace_type = "chain" ,
505+ project_name = "test-project" ,
506+ trace_id = uuid .uuid4 (),
507+ )
508+ trace_name = "Composite Output (output-id)"
509+ tracer .add_trace (
510+ trace_id = "output-id" ,
511+ trace_name = trace_name ,
512+ trace_type = "chain" ,
513+ inputs = {},
514+ vertex = MagicMock (is_input = False , is_output = True ),
515+ )
516+
517+ tracer .end (
518+ inputs = {},
519+ outputs = {trace_name : {"message" : Message (text = "ready" ), "usage" : {"tokens" : 3 }}},
520+ )
521+
522+ trace_update = mock_langfuse ["root_span" ].update_trace .call_args .kwargs
523+ assert trace_update ["output" ] == {"output" : {"message" : "ready" , "usage" : {"tokens" : 3 }}}
343524
344525 def test_get_langchain_callback_uses_trace_context (self , mock_langfuse ):
345526 """Test that get_langchain_callback creates handler with trace context."""
0 commit comments