mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
73761aa4a3
commit
83e6229c11
@@ -16,14 +16,10 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Contents,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
ToolProtocol,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
get_logger,
|
||||
prepare_function_call_results,
|
||||
@@ -328,7 +324,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
response = await self._inner_get_response(messages=messages, options=options, **kwargs)
|
||||
contents = list(response.messages[0].contents if response.messages else [])
|
||||
if response.usage_details:
|
||||
contents.append(UsageContent(details=response.usage_details))
|
||||
contents.append(Content.from_usage(usage_details=response.usage_details)) # type: ignore[arg-type]
|
||||
yield ChatResponseUpdate(
|
||||
response_id=response.response_id,
|
||||
contents=contents,
|
||||
@@ -472,37 +468,41 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
blocks.append(block)
|
||||
return blocks
|
||||
|
||||
def _convert_content_to_bedrock_block(self, content: Contents) -> dict[str, Any] | None:
|
||||
if isinstance(content, TextContent):
|
||||
return {"text": content.text}
|
||||
if isinstance(content, FunctionCallContent):
|
||||
arguments = content.parse_arguments() or {}
|
||||
return {
|
||||
"toolUse": {
|
||||
"toolUseId": content.call_id or self._generate_tool_call_id(),
|
||||
"name": content.name,
|
||||
"input": arguments,
|
||||
def _convert_content_to_bedrock_block(self, content: Content) -> dict[str, Any] | None:
|
||||
match content.type:
|
||||
case "text":
|
||||
return {"text": content.text}
|
||||
case "function_call":
|
||||
arguments = content.parse_arguments() or {}
|
||||
return {
|
||||
"toolUse": {
|
||||
"toolUseId": content.call_id or self._generate_tool_call_id(),
|
||||
"name": content.name,
|
||||
"input": arguments,
|
||||
}
|
||||
}
|
||||
}
|
||||
if isinstance(content, FunctionResultContent):
|
||||
tool_result_block = {
|
||||
"toolResult": {
|
||||
"toolUseId": content.call_id,
|
||||
"content": self._convert_tool_result_to_blocks(content.result),
|
||||
"status": "error" if content.exception else "success",
|
||||
case "function_result":
|
||||
tool_result_block = {
|
||||
"toolResult": {
|
||||
"toolUseId": content.call_id,
|
||||
"content": self._convert_tool_result_to_blocks(content.result),
|
||||
"status": "error" if content.exception else "success",
|
||||
}
|
||||
}
|
||||
}
|
||||
if content.exception:
|
||||
tool_result = tool_result_block["toolResult"]
|
||||
existing_content = tool_result.get("content")
|
||||
content_list: list[dict[str, Any]]
|
||||
if isinstance(existing_content, list):
|
||||
content_list = existing_content
|
||||
else:
|
||||
content_list = []
|
||||
tool_result["content"] = content_list
|
||||
content_list.append({"text": str(content.exception)})
|
||||
return tool_result_block
|
||||
if content.exception:
|
||||
tool_result = tool_result_block["toolResult"]
|
||||
existing_content = tool_result.get("content")
|
||||
content_list: list[dict[str, Any]]
|
||||
if isinstance(existing_content, list):
|
||||
content_list = existing_content
|
||||
else:
|
||||
content_list = []
|
||||
tool_result["content"] = content_list
|
||||
content_list.append({"text": str(content.exception)})
|
||||
return tool_result_block
|
||||
case _:
|
||||
# Bedrock does not support other content types at this time
|
||||
pass
|
||||
return None
|
||||
|
||||
def _convert_tool_result_to_blocks(self, result: Any) -> list[dict[str, Any]]:
|
||||
@@ -531,7 +531,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
return {"text": value}
|
||||
if isinstance(value, (int, float, bool)) or value is None:
|
||||
return {"json": value}
|
||||
if isinstance(value, TextContent) and getattr(value, "text", None):
|
||||
if isinstance(value, Content) and value.type == "text":
|
||||
return {"text": value.text}
|
||||
if hasattr(value, "to_dict"):
|
||||
try:
|
||||
@@ -586,23 +586,23 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
def _parse_usage(self, usage: dict[str, Any] | None) -> UsageDetails | None:
|
||||
if not usage:
|
||||
return None
|
||||
details = UsageDetails()
|
||||
details: UsageDetails = {}
|
||||
if (input_tokens := usage.get("inputTokens")) is not None:
|
||||
details.input_token_count = input_tokens
|
||||
details["input_token_count"] = input_tokens
|
||||
if (output_tokens := usage.get("outputTokens")) is not None:
|
||||
details.output_token_count = output_tokens
|
||||
details["output_token_count"] = output_tokens
|
||||
if (total_tokens := usage.get("totalTokens")) is not None:
|
||||
details.additional_counts["bedrock.total_tokens"] = total_tokens
|
||||
details["total_token_count"] = total_tokens
|
||||
return details
|
||||
|
||||
def _parse_message_contents(self, content_blocks: Sequence[MutableMapping[str, Any]]) -> list[Any]:
|
||||
contents: list[Any] = []
|
||||
for block in content_blocks:
|
||||
if text_value := block.get("text"):
|
||||
contents.append(TextContent(text=text_value, raw_representation=block))
|
||||
contents.append(Content.from_text(text=text_value, raw_representation=block))
|
||||
continue
|
||||
if (json_value := block.get("json")) is not None:
|
||||
contents.append(TextContent(text=json.dumps(json_value), raw_representation=block))
|
||||
contents.append(Content.from_text(text=json.dumps(json_value), raw_representation=block))
|
||||
continue
|
||||
tool_use = block.get("toolUse")
|
||||
if isinstance(tool_use, MutableMapping):
|
||||
@@ -610,7 +610,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
if not tool_name:
|
||||
raise ServiceInvalidResponseError("Bedrock response missing required tool name in toolUse block.")
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=tool_use.get("toolUseId") or self._generate_tool_call_id(),
|
||||
name=tool_name,
|
||||
arguments=tool_use.get("input"),
|
||||
@@ -626,10 +626,10 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
exception = RuntimeError(f"Bedrock tool result status: {status}")
|
||||
result_value = self._convert_bedrock_tool_result_to_value(tool_result.get("content"))
|
||||
contents.append(
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=tool_result.get("toolUseId") or self._generate_tool_call_id(),
|
||||
result=result_value,
|
||||
exception=exception,
|
||||
exception=str(exception) if exception else None, # type: ignore[arg-type]
|
||||
raw_representation=block,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatMessage, Role, TextContent
|
||||
from agent_framework import ChatMessage, Content, Role
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
|
||||
from agent_framework_bedrock import BedrockChatClient
|
||||
@@ -42,8 +42,8 @@ def test_get_response_invokes_bedrock_runtime() -> None:
|
||||
)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are concise.")]),
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="hello")]),
|
||||
ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="You are concise.")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="hello")]),
|
||||
]
|
||||
|
||||
response = asyncio.run(client.get_response(messages=messages, options={"max_tokens": 32}))
|
||||
@@ -53,7 +53,7 @@ def test_get_response_invokes_bedrock_runtime() -> None:
|
||||
assert payload["modelId"] == "amazon.titan-text"
|
||||
assert payload["messages"][0]["content"][0]["text"] == "hello"
|
||||
assert response.messages[0].contents[0].text == "Bedrock says hi"
|
||||
assert response.usage_details and response.usage_details.input_token_count == 10
|
||||
assert response.usage_details and response.usage_details["input_token_count"] == 10
|
||||
|
||||
|
||||
def test_build_request_requires_non_system_messages() -> None:
|
||||
@@ -63,7 +63,7 @@ def test_build_request_requires_non_system_messages() -> None:
|
||||
client=_StubBedrockRuntime(),
|
||||
)
|
||||
|
||||
messages = [ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="Only system text")])]
|
||||
messages = [ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="Only system text")])]
|
||||
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
client._prepare_options(messages, {})
|
||||
|
||||
@@ -9,10 +9,8 @@ from agent_framework import (
|
||||
AIFunction,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -49,7 +47,7 @@ def test_build_request_includes_tool_config() -> None:
|
||||
"tools": [tool],
|
||||
"tool_choice": {"mode": "required", "required_function_name": "get_weather"},
|
||||
}
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="hi")])]
|
||||
messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="hi")])]
|
||||
|
||||
request = client._prepare_options(messages, options)
|
||||
|
||||
@@ -61,14 +59,16 @@ def test_build_request_serializes_tool_history() -> None:
|
||||
client = _build_client()
|
||||
options: ChatOptions = {}
|
||||
messages = [
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="how's weather?")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="how's weather?")]),
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[FunctionCallContent(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}')
|
||||
],
|
||||
),
|
||||
ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-1", result={"answer": "72F"})],
|
||||
contents=[Content.from_function_result(call_id="call-1", result={"answer": "72F"})],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -101,9 +101,9 @@ def test_process_response_parses_tool_use_and_result() -> None:
|
||||
chat_response = client._process_converse_response(response)
|
||||
contents = chat_response.messages[0].contents
|
||||
|
||||
assert isinstance(contents[0], FunctionCallContent)
|
||||
assert contents[0].type == "function_call"
|
||||
assert contents[0].name == "get_weather"
|
||||
assert isinstance(contents[1], TextContent)
|
||||
assert contents[1].type == "text"
|
||||
assert chat_response.finish_reason == client._map_finish_reason("tool_use")
|
||||
|
||||
|
||||
@@ -131,5 +131,5 @@ def test_process_response_parses_tool_result() -> None:
|
||||
chat_response = client._process_converse_response(response)
|
||||
contents = chat_response.messages[0].contents
|
||||
|
||||
assert isinstance(contents[0], FunctionResultContent)
|
||||
assert contents[0].type == "function_result"
|
||||
assert contents[0].result == {"answer": 42}
|
||||
|
||||
Reference in New Issue
Block a user