Skip to content
Merged
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
35 changes: 21 additions & 14 deletions python/packages/chatkit/agent_framework_chatkit/_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
96 changes: 94 additions & 2 deletions python/packages/chatkit/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <TAG>Name:John Doe</TAG> 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 == "<TAG>Name:John Doe</TAG>"

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
Expand Down
Loading