From 0c84145e360846a1496e2c0206056ee25fe9966d Mon Sep 17 00:00:00 2001 From: Vedanth Bora Date: Sun, 6 Sep 2026 16:39:07 +0530 Subject: [PATCH] fix(chatkit): preserve input tags in user messages --- .../agent_framework_chatkit/_converter.py | 35 ++++--- .../packages/chatkit/tests/test_converter.py | 96 ++++++++++++++++++- 2 files changed, 115 insertions(+), 16 deletions(-) diff --git a/python/packages/chatkit/agent_framework_chatkit/_converter.py b/python/packages/chatkit/agent_framework_chatkit/_converter.py index c4fe24670f2..88f75598b5b 100644 --- a/python/packages/chatkit/agent_framework_chatkit/_converter.py +++ b/python/packages/chatkit/agent_framework_chatkit/_converter.py @@ -81,33 +81,40 @@ async def user_message_to_input( """ # Extract text content from the user message text_content = "" + contents: list[Content] = [] + + def append_text_content() -> None: + nonlocal text_content + if stripped_text := text_content.strip(): + contents.append(Content.from_text(text=stripped_text)) + text_content = "" + if item.content: for content_part in item.content: if isinstance(content_part, UserMessageTextContent): text_content += content_part.text + elif isinstance(content_part, UserMessageTagContent): + tag_content = self.tag_to_message_content(content_part) + if tag_content.type == "text": + text_content += tag_content.text or "" + else: + append_text_content() + contents.append(tag_content) + + append_text_content() - # Convert attachments to Content - data_contents: list[Content] = [] + # Append attachments after the ordered message content. if item.attachments: for attachment in item.attachments: content = await self.attachment_to_message_content(attachment) if content is not None: - data_contents.append(content) + contents.append(content) # Create the message with text and attachments - if not text_content.strip() and not data_contents: + if not contents: return None - # If only text and no attachments, use text parameter for simplicity - if text_content.strip() and not data_contents: - user_message = Message(role="user", contents=[text_content.strip()]) - else: - # Build contents list with both text and attachments - contents: list[Content] = [] - if text_content.strip(): - contents.append(Content.from_text(text=text_content.strip())) - contents.extend(data_contents) - user_message = Message(role="user", contents=contents) + user_message = Message(role="user", contents=contents) # Handle quoted text if this is the last message messages = [user_message] diff --git a/python/packages/chatkit/tests/test_converter.py b/python/packages/chatkit/tests/test_converter.py index 730fd6ec6e1..a0fb0244549 100644 --- a/python/packages/chatkit/tests/test_converter.py +++ b/python/packages/chatkit/tests/test_converter.py @@ -7,8 +7,8 @@ from unittest.mock import Mock import pytest -from agent_framework import Message -from chatkit.types import InferenceOptions, UserMessageTextContent +from agent_framework import Content, Message +from chatkit.types import InferenceOptions, UserMessageTagContent, UserMessageTextContent from pydantic import AnyUrl from agent_framework_chatkit import ThreadItemConverter, simple_to_agent_input @@ -110,6 +110,98 @@ async def test_to_agent_input_multiple_content_parts(self, converter): assert len(result) == 1 assert result[0].text == "Hello world!" + async def test_to_agent_input_keeps_tag_inline_with_text(self, converter): + """Test converting user message tags in their original text position.""" + from chatkit.types import UserMessageItem + + input_item = UserMessageItem( + id="msg_tag", + thread_id="thread_1", + created_at=datetime.now(), + type="user_message", + content=[ + UserMessageTextContent(text="Ask "), + UserMessageTagContent( + type="input_tag", + id="tag_1", + text="john", + data={"name": "John Doe"}, + interactive=False, + ), + UserMessageTextContent(text=" about the report."), + ], + attachments=[], + inference_options=InferenceOptions(), + ) + + result = await converter.to_agent_input(input_item) + + assert len(result) == 1 + assert result[0].text == "Ask Name:John Doe about the report." + + async def test_to_agent_input_keeps_tag_only_message(self, converter): + """Test that a user message containing only a tag is not discarded.""" + from chatkit.types import UserMessageItem + + input_item = UserMessageItem( + id="msg_tag_only", + thread_id="thread_1", + created_at=datetime.now(), + type="user_message", + content=[ + UserMessageTagContent( + type="input_tag", + id="tag_1", + text="john", + data={"name": "John Doe"}, + interactive=False, + ) + ], + attachments=[], + inference_options=InferenceOptions(), + ) + + result = await converter.to_agent_input(input_item) + + assert len(result) == 1 + assert result[0].text == "Name:John Doe" + + async def test_to_agent_input_preserves_non_text_tag_position(self): + """Test that custom non-text tag conversions remain between adjacent text.""" + from chatkit.types import UserMessageItem + + class UriTagConverter(ThreadItemConverter): + def tag_to_message_content(self, tag: UserMessageTagContent) -> Content: + return Content.from_uri(uri=f"https://example.com/users/{tag.text}", media_type="text/html") + + input_item = UserMessageItem( + id="msg_uri_tag", + thread_id="thread_1", + created_at=datetime.now(), + type="user_message", + content=[ + UserMessageTextContent(text="Ask"), + UserMessageTagContent( + type="input_tag", + id="tag_1", + text="john", + data={"name": "John Doe"}, + interactive=False, + ), + UserMessageTextContent(text="about the report."), + ], + attachments=[], + inference_options=InferenceOptions(), + ) + + result = await UriTagConverter().to_agent_input(input_item) + + assert len(result) == 1 + assert [content.type for content in result[0].contents] == ["text", "uri", "text"] + assert result[0].contents[0].text == "Ask" + assert result[0].contents[1].uri == "https://example.com/users/john" + assert result[0].contents[2].text == "about the report." + async def test_to_agent_input_with_quoted_text_for_last_message(self, converter): """Test quoted text is prepended as context for the last user message.""" from chatkit.types import UserMessageItem