-
Notifications
You must be signed in to change notification settings - Fork 4
fix(converter): keep only last message from full-history agent output (HYBIM-988) #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -447,6 +447,10 @@ def _set_orchestration_content(attrs: MutableMapping[str, AttributeValue], span: | |||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| if full_history and input_messages is not None and output_messages[: len(input_messages)] == input_messages: | ||||||||||||||||||||||||||||||
| output_messages = output_messages[len(input_messages) :] | ||||||||||||||||||||||||||||||
| # Full history includes all messages in the run, not just the final response. | ||||||||||||||||||||||||||||||
| # Keep only the last message — it is always the agent's final output. | ||||||||||||||||||||||||||||||
| if full_history and len(output_messages) > 1: | ||||||||||||||||||||||||||||||
| output_messages = [output_messages[-1]] | ||||||||||||||||||||||||||||||
|
Comment on lines
448
to
+453
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major (bug): Silent message loss on non-root LangGraph node spans.
Concrete failure: LangGraph's prebuilt The ticket's own wording is "After dedup removes the input prefix, multiple messages remain" — so gate on the dedup actually having matched. That preserves the fix for the reported case and leaves genuine multi-message node updates intact. One caveat to confirm: if a graph ever prepends or trims messages so the input is no longer a prefix, the dedup won't match and the root span won't be reduced. If that is a real shape for your app, prefer reducing at the handler layer for the root agent node instead (mirroring
Suggested change
🤖 Generated by the Astra agent |
||||||||||||||||||||||||||||||
| attrs["gen_ai.output.messages"] = _json_string(_with_finish_reasons(output_messages)) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -418,6 +418,90 @@ def test_orchestration_output_omits_repeated_input_history() -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_orchestration_full_history_with_tool_call_keeps_last_message() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # LangGraph accumulated state: user → tool-call AI (empty content) → tool response → final AI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The first post-dedup message has empty content; the UI would show "—" without the fix. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user = {"role": "user", "content": "What is the dosage of Lisinopril?"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ai_toolcall = {"role": "assistant", "content": "", "tool_calls": [{"id": "tc1", "function": {"name": "rag_search", "arguments": '{"query":"Lisinopril dosage"}'}}]} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 nit (other): 🤖 Generated by the Astra agent |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_resp = {"role": "tool", "content": "Lisinopril: 10mg daily", "tool_call_id": "tc1"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ai_final = {"role": "assistant", "content": "Common dosage is 10mg once daily."} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| span = AgentSpan( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="Agent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent_type=AgentType.default, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input=json.dumps({"messages": [user]}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output=json.dumps({"messages": [user, ai_toolcall, tool_resp, ai_final]}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attrs = build_span_attributes(span) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_messages = json.loads(attrs["gen_ai.output.messages"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(output_messages) == 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert output_messages[0]["role"] == "assistant" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert output_messages[0]["parts"][0]["content"] == "Common dosage is 10mg once daily." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_orchestration_full_history_multi_turn_keeps_last_message() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Multi-turn: output contains the full conversation history after multiple exchanges. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Only the last message should be kept regardless of role. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user1 = {"role": "user", "content": "Hello"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ai1 = {"role": "assistant", "content": "Hi, how can I help?"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user2 = {"role": "user", "content": "What is Lisinopril?"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ai2 = {"role": "assistant", "content": "Lisinopril is a blood pressure medication."} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| span = AgentSpan( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="Agent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent_type=AgentType.default, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input=json.dumps({"messages": [user1]}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output=json.dumps({"messages": [user1, ai1, user2, ai2]}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attrs = build_span_attributes(span) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_messages = json.loads(attrs["gen_ai.output.messages"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(output_messages) == 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert output_messages[0]["parts"][0]["content"] == "Lisinopril is a blood pressure medication." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_orchestration_full_history_multiple_tool_rounds_keeps_last_message() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Two tool call rounds before the final answer — last message is still the only output. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user = {"role": "user", "content": "Compare Lisinopril and Amlodipine"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tc1_ai = {"role": "assistant", "content": "", "tool_calls": [{"id": "tc1", "function": {"name": "search", "arguments": '{"query":"Lisinopril"}'}}]} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tc1_resp = {"role": "tool", "content": "Lisinopril: ACE inhibitor", "tool_call_id": "tc1"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tc2_ai = {"role": "assistant", "content": "", "tool_calls": [{"id": "tc2", "function": {"name": "search", "arguments": '{"query":"Amlodipine"}'}}]} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tc2_resp = {"role": "tool", "content": "Amlodipine: calcium channel blocker", "tool_call_id": "tc2"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ai_final = {"role": "assistant", "content": "Lisinopril is an ACE inhibitor; Amlodipine is a calcium channel blocker."} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| span = AgentSpan( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="Agent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent_type=AgentType.default, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input=json.dumps({"messages": [user]}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output=json.dumps({"messages": [user, tc1_ai, tc1_resp, tc2_ai, tc2_resp, ai_final]}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attrs = build_span_attributes(span) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_messages = json.loads(attrs["gen_ai.output.messages"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(output_messages) == 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert "Amlodipine" in output_messages[0]["parts"][0]["content"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+421
to
+486
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major (testing): Missing coverage for the cases where this change is most likely to be wrong. All three new positive tests are
Also note Separately: 🤖 Generated by the Astra agent |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_orchestration_non_full_history_output_not_reduced() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Plain string output (full_history=False) — the last-message reduction must NOT fire. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| span = AgentSpan( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="Agent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent_type=AgentType.default, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input="What is Lisinopril?", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output="Lisinopril is a blood pressure medication.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attrs = build_span_attributes(span) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_messages = json.loads(attrs["gen_ai.output.messages"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(output_messages) == 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert output_messages[0]["parts"][0]["content"] == "Lisinopril is a blood pressure medication." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+489
to
+502
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major (testing): This test is vacuous — it passes identically with or without the change, so it does not guard the Tracing a plain string through To actually exercise the guard, use an input where the reduction would fire if the gate were dropped — a bare JSON list of messages, which
Suggested change
🤖 Generated by the Astra agent |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_orchestration_preserves_schema_valid_parts_and_tool_calls() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| span = WorkflowSpan( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="tool-workflow", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 minor (bug): "it is always the agent's final output" is not true, and the comment will mislead whoever touches this next.
The last message is only the final answer when the run terminated normally on an assistant text message. It is not when the run ends on a tool-call
AIMessage— e.g.interrupt_before=["tools"], a hit recursion limit, or an aborted run. In that casecontentis""and_mapped_message(line 190) producesparts == [tool_call_part]with no text part, so the Output column still renders—and the preceding assistant text that was present is now discarded as well. The bug this PR sets out to fix therefore survives for those runs.At minimum, soften the comment to describe the heuristic rather than assert an invariant. If you want the interrupted-run case actually covered, prefer the last message that yields a non-empty text part, falling back to the last message — and add a test for it.
🤖 Generated by the Astra agent