Python: [Breaking] Simplified Content types to a single class with classmethod constructors. (#3252)

* ported Content to a new model

* fixed linting

* fixes

* fixed data format handling

* fix for 3.10 mypy

* fix

* fix int test
This commit is contained in:
Eduard van Valkenburg
2026-01-20 22:09:39 +00:00
committed by GitHub
parent 73761aa4a3
commit 83e6229c11
132 changed files with 3949 additions and 4741 deletions
@@ -20,13 +20,8 @@ from agent_framework import (
ChatOptions,
ChatResponse,
ChatResponseUpdate,
Contents,
DataContent,
FunctionCallContent,
FunctionResultContent,
Content,
Role,
TextContent,
TextReasoningContent,
ToolProtocol,
UsageDetails,
get_logger,
@@ -452,31 +447,31 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp
return [OllamaMessage(role="system", content=message.text)]
def _format_user_message(self, message: ChatMessage) -> list[OllamaMessage]:
if not any(isinstance(c, (DataContent, TextContent)) for c in message.contents) and not message.text:
if not any(c.type in {"text", "data"} for c in message.contents) and not message.text:
raise ServiceInvalidRequestError(
"Ollama connector currently only supports user messages with TextContent or DataContent."
)
if not any(isinstance(c, DataContent) for c in message.contents):
if not any(c.type == "data" for c in message.contents):
return [OllamaMessage(role="user", content=message.text)]
user_message = OllamaMessage(role="user", content=message.text)
data_contents = [c for c in message.contents if isinstance(c, DataContent)]
data_contents = [c for c in message.contents if c.type == "data"]
if data_contents:
if not any(c.has_top_level_media_type("image") for c in data_contents):
raise ServiceInvalidRequestError("Only image data content is supported for user messages in Ollama.")
# Ollama expects base64 strings without prefix
user_message["images"] = [c.uri.split(",")[1] for c in data_contents]
user_message["images"] = [c.uri.split(",")[1] for c in data_contents if c.uri]
return [user_message]
def _format_assistant_message(self, message: ChatMessage) -> list[OllamaMessage]:
text_content = message.text
# Ollama shouldn't have encrypted reasoning, so we just process text.
reasoning_contents = "".join((c.text or "") for c in message.contents if isinstance(c, TextReasoningContent))
reasoning_contents = "".join((c.text or "") for c in message.contents if c.type == "text_reasoning")
assistant_message = OllamaMessage(role="assistant", content=text_content, thinking=reasoning_contents)
tool_calls = [item for item in message.contents if isinstance(item, FunctionCallContent)]
tool_calls = [item for item in message.contents if item.type == "function_call"]
if tool_calls:
assistant_message["tool_calls"] = [
{
@@ -497,15 +492,15 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp
return [
OllamaMessage(role="tool", content=str(item.result), tool_name=item.call_id)
for item in message.contents
if isinstance(item, FunctionResultContent)
if item.type == "function_result"
]
def _parse_contents_from_ollama(self, response: OllamaChatResponse) -> list[Contents]:
contents: list[Contents] = []
def _parse_contents_from_ollama(self, response: OllamaChatResponse) -> list[Content]:
contents: list[Content] = []
if response.message.thinking:
contents.append(TextReasoningContent(text=response.message.thinking))
contents.append(Content.from_text_reasoning(text=response.message.thinking))
if response.message.content:
contents.append(TextContent(text=response.message.content))
contents.append(Content.from_text(text=response.message.content))
if response.message.tool_calls:
tool_calls = self._parse_tool_calls_from_ollama(response.message.tool_calls)
contents.extend(tool_calls)
@@ -533,10 +528,10 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp
),
)
def _parse_tool_calls_from_ollama(self, tool_calls: Sequence[OllamaMessage.ToolCall]) -> list[Contents]:
resp: list[Contents] = []
def _parse_tool_calls_from_ollama(self, tool_calls: Sequence[OllamaMessage.ToolCall]) -> list[Content]:
resp: list[Content] = []
for tool in tool_calls:
fcc = FunctionCallContent(
fcc = Content.from_function_call(
call_id=tool.function.name, # Use name of function as call ID since Ollama doesn't provide a call ID
name=tool.function.name,
arguments=tool.function.arguments if isinstance(tool.function.arguments, dict) else "",
@@ -9,13 +9,8 @@ from agent_framework import (
BaseChatClient,
ChatMessage,
ChatResponseUpdate,
DataContent,
FunctionCallContent,
FunctionResultContent,
Content,
HostedWebSearchTool,
TextContent,
TextReasoningContent,
UriContent,
ai_function,
chat_middleware,
)
@@ -231,7 +226,7 @@ async def test_cmc_reasoning(
ollama_client = OllamaChatClient()
result = await ollama_client.get_response(messages=chat_history)
reasoning = "".join(c.text for c in result.messages.pop().contents if isinstance(c, TextReasoningContent))
reasoning = "".join(c.text for c in result.messages.pop().contents if c.type == "text_reasoning")
assert reasoning == "test"
@@ -286,7 +281,7 @@ async def test_cmc_streaming_reasoning(
result = ollama_client.get_streaming_response(messages=chat_history)
async for chunk in result:
reasoning = "".join(c.text for c in chunk.contents if isinstance(c, TextReasoningContent))
reasoning = "".join(c.text for c in chunk.contents if c.type == "text_reasoning")
assert reasoning == "test"
@@ -333,14 +328,14 @@ async def test_cmc_streaming_with_tool_call(
chunks.append(chunk)
# Check parsed Toolcalls
assert isinstance(chunks[0].contents[0], FunctionCallContent)
assert chunks[0].contents[0].type == "function_call"
tool_call = chunks[0].contents[0]
assert tool_call.name == "hello_world"
assert tool_call.arguments == {"arg1": "value1"}
assert isinstance(chunks[1].contents[0], FunctionResultContent)
assert chunks[1].contents[0].type == "function_result"
tool_result = chunks[1].contents[0]
assert tool_result.result == "Hello World"
assert isinstance(chunks[2].contents[0], TextContent)
assert chunks[2].contents[0].type == "text"
text_result = chunks[2].contents[0]
assert text_result.text == "test"
@@ -378,7 +373,7 @@ async def test_cmc_with_data_content_type(
mock_chat.return_value = mock_chat_completion_response
chat_history.append(
ChatMessage(
contents=[DataContent(uri="data:image/png;base64,xyz", media_type="image/png")],
contents=[Content.from_uri(uri="data:image/png;base64,xyz", media_type="image/png")],
role="user",
)
)
@@ -401,7 +396,7 @@ async def test_cmc_with_invalid_data_content_media_type(
# Remote Uris are not supported by Ollama client
chat_history.append(
ChatMessage(
contents=[DataContent(uri="data:audio/mp3;base64,xyz", media_type="audio/mp3")],
contents=[Content.from_uri(uri="data:audio/mp3;base64,xyz", media_type="audio/mp3")],
role="user",
)
)
@@ -424,7 +419,7 @@ async def test_cmc_with_invalid_content_type(
# Remote Uris are not supported by Ollama client
chat_history.append(
ChatMessage(
contents=[UriContent(uri="http://example.com/image.png", media_type="image/png")],
contents=[Content.from_uri(uri="http://example.com/image.png", media_type="image/png")],
role="user",
)
)
@@ -444,7 +439,7 @@ async def test_cmc_integration_with_tool_call(
result = await ollama_client.get_response(messages=chat_history, options={"tools": [hello_world]})
assert "hello" in result.text.lower() and "world" in result.text.lower()
assert isinstance(result.messages[-2].contents[0], FunctionResultContent)
assert result.messages[-2].contents[0].type == "function_result"
tool_result = result.messages[-2].contents[0]
assert tool_result.result == "Hello World"
@@ -478,10 +473,10 @@ async def test_cmc_streaming_integration_with_tool_call(
for c in chunks:
if len(c.contents) > 0:
if isinstance(c.contents[0], FunctionResultContent):
if c.contents[0].type == "function_result":
tool_result = c.contents[0]
assert tool_result.result == "Hello World"
if isinstance(c.contents[0], FunctionCallContent):
if c.contents[0].type == "function_call":
tool_call = c.contents[0]
assert tool_call.name == "hello_world"