Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions agent-openai-agents-sdk/agent_server/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ResponsesAgentStreamEvent,
)

from agent_server.history import normalize_history_items
from agent_server.utils import (
build_mcp_url,
get_session_id,
Expand Down Expand Up @@ -99,7 +100,7 @@ async def invoke_handler(request: ResponsesAgentRequest) -> ResponsesAgentRespon
# for on-behalf-of user authentication.
async with AsyncExitStack() as stack:
agent = create_agent()
messages = [i.model_dump() for i in request.input]
messages = normalize_history_items([i.model_dump() for i in request.input])
result = await Runner.run(agent, messages)
return ResponsesAgentResponse(output=[item.to_input_item() for item in result.new_items])

Expand All @@ -121,7 +122,7 @@ async def stream_handler(
# for on-behalf-of user authentication.
async with AsyncExitStack() as stack:
agent = create_agent()
messages = [i.model_dump() for i in request.input]
messages = normalize_history_items([i.model_dump() for i in request.input])
result = Runner.run_streamed(agent, input=messages)

async for event in process_agent_stream_events(result.stream_events()):
Expand Down
40 changes: 40 additions & 0 deletions agent-openai-agents-sdk/agent_server/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Conversation-history normalization helpers.

Kept dependency-free (no Databricks SDK / MLflow imports) so it can be unit
tested in isolation.
"""


def normalize_history_items(messages: list[dict]) -> list[dict]:
"""Normalize replayed assistant history items for Chat Completions.

openai-agents >= 0.19 tightened ``Converter.maybe_response_output_message``
to require ``{"id", "content"}`` on assistant history items. The built-in
chat UI replays the prior assistant turn as an id-less
``{"type": "message", "role": "assistant", "content": [output_text]}`` item,
and MLflow's ``ResponsesAgentRequest`` strips any client-supplied ``id``, so
on the second and later prompts the item fails recognition and hits the
catch-all ``UserError: Unhandled item type or structure``.

Collapse those replayed assistant items to the easy-input
``{"role", "content"}`` form, which is recognized by
``maybe_easy_input_message`` (no id required), sidestepping the tightened
guard regardless of SDK version. Multiple ``output_text`` segments are joined
with ``"\\n"`` to match the SDK converter's own behavior byte-for-byte.
"""
normalized: list[dict] = []
for m in messages:
if (
m.get("type") == "message"
and m.get("role") == "assistant"
and isinstance(m.get("content"), list)
):
text = "\n".join(
part.get("text", "")
for part in m["content"]
if isinstance(part, dict) and part.get("type") == "output_text"
)
normalized.append({"role": "assistant", "content": text})
else:
normalized.append(m)
return normalized
85 changes: 85 additions & 0 deletions agent-openai-agents-sdk/tests/test_history_normalization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Regression tests for multi-turn assistant-history normalization.

openai-agents >= 0.19 tightened ``Converter.maybe_response_output_message`` to
require ``{"id", "content"}`` on assistant history items. The built-in chat UI
replays the prior assistant turn as an id-less ``{"type": "message",
"role": "assistant", "content": [output_text]}`` item, and MLflow's
``ResponsesAgentRequest`` strips any client-supplied ``id``, so on the second
and later prompts the item failed recognition and hit the catch-all
``UserError: Unhandled item type or structure``.

``normalize_history_items`` collapses those replayed assistant items to the
easy-input ``{"role", "content"}`` form, which is recognized without an id.
These tests lock in that transformation so the regression cannot return silently.
"""

import unittest

from agent_server.history import normalize_history_items


class NormalizeHistoryItemsTest(unittest.TestCase):
def test_idless_assistant_output_text_collapsed_to_easy_input(self):
# Exactly what the chat UI replays on the 2nd prompt (no "id").
messages = [
{"role": "user", "content": "hello"},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi! How can I help?"}],
},
{"role": "user", "content": "test"},
]
self.assertEqual(
normalize_history_items(messages),
[
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "Hi! How can I help?"},
{"role": "user", "content": "test"},
],
)

def test_multiple_output_text_segments_joined_with_newline(self):
# Matches the SDK converter, which joins segments with "\n".
messages = [
{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "part A"},
{"type": "output_text", "text": "part B"},
],
}
]
self.assertEqual(
normalize_history_items(messages),
[{"role": "assistant", "content": "part A\npart B"}],
)

def test_user_and_plain_assistant_items_are_untouched(self):
messages = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "already easy-input"},
]
self.assertEqual(normalize_history_items(messages), messages)

def test_non_output_text_content_parts_are_dropped_from_text(self):
# Only output_text parts contribute to the collapsed string.
messages = [
{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "keep"},
{"type": "something_else", "foo": "bar"},
],
}
]
self.assertEqual(
normalize_history_items(messages),
[{"role": "assistant", "content": "keep"}],
)


if __name__ == "__main__":
unittest.main()