Skip to content

Commit df84675

Browse files
authored
Python: Added AgentRunResponse and AgentRunResponseUpdate types (#157)
* Removed instructions property from Agent * Added AgentRunResponse and AgentRunResponseUpdate types * Added unit tests for agent response types * Small fix * Addressed PR feedback * Small improvement * Small fix
1 parent 456e7d1 commit df84675

6 files changed

Lines changed: 241 additions & 32 deletions

File tree

python/packages/main/agent_framework/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"AFBaseModel": "._pydantic",
1515
"AFBaseSettings": "._pydantic",
1616
"Agent": "._agents",
17+
"AgentRunResponse": "._types",
18+
"AgentRunResponseUpdate": "._types",
1719
"AgentThread": "._agents",
1820
"AITool": "._tools",
1921
"ai_function": "._tools",

python/packages/main/agent_framework/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ from ._logging import get_logger
77
from ._pydantic import AFBaseModel, AFBaseSettings
88
from ._tools import AITool, ai_function
99
from ._types import (
10+
AgentRunResponse,
11+
AgentRunResponseUpdate,
1012
AIContent,
1113
AIContents,
1214
ChatFinishReason,
@@ -39,6 +41,8 @@ __all__ = [
3941
"AIContents",
4042
"AITool",
4143
"Agent",
44+
"AgentRunResponse",
45+
"AgentRunResponseUpdate",
4246
"AgentThread",
4347
"ChatClient",
4448
"ChatClientBase",

python/packages/main/agent_framework/_agents.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any, Protocol, runtime_checkable
66

77
from ._pydantic import AFBaseModel
8-
from ._types import ChatMessage, ChatResponse, ChatResponseUpdate
8+
from ._types import AgentRunResponse, AgentRunResponseUpdate, ChatMessage
99

1010
# region AgentThread
1111

@@ -82,26 +82,21 @@ def description(self) -> str | None:
8282
"""Returns the description of the agent."""
8383
...
8484

85-
@property
86-
def instructions(self) -> str | None:
87-
"""Returns the instructions for the agent."""
88-
...
89-
9085
async def run(
9186
self,
9287
messages: str | ChatMessage | list[ChatMessage] | None = None,
9388
*,
9489
thread: AgentThread | None = None,
9590
**kwargs: Any,
96-
) -> ChatResponse:
91+
) -> AgentRunResponse:
9792
"""Get a response from the agent.
9893
9994
This method returns the final result of the agent's execution
100-
as a single ChatResponse object. The caller is blocked until
95+
as a single AgentRunResponse object. The caller is blocked until
10196
the final result is available.
10297
10398
Note: For streaming responses, use the run_stream method, which returns
104-
intermediate steps and the final result as a stream of ChatResponseUpdate
99+
intermediate steps and the final result as a stream of AgentRunResponseUpdate
105100
objects. Streaming only the final result is not feasible because the timing of
106101
the final result's availability is unknown, and blocking the caller until then
107102
is undesirable in streaming scenarios.
@@ -122,16 +117,13 @@ def run_stream(
122117
*,
123118
thread: AgentThread | None = None,
124119
**kwargs: Any,
125-
) -> AsyncIterable[ChatResponseUpdate]:
120+
) -> AsyncIterable[AgentRunResponseUpdate]:
126121
"""Run the agent as a stream.
127122
128123
This method will return the intermediate steps and final results of the
129-
agent's execution as a stream of ChatResponseUpdate objects to the caller.
130-
131-
To get the intermediate steps of the agent's execution as fully formed messages,
132-
use the on_intermediate_message callback.
124+
agent's execution as a stream of AgentRunResponseUpdate objects to the caller.
133125
134-
Note: A ChatResponseUpdate object contains a chunk of a message.
126+
Note: An AgentRunResponseUpdate object contains a chunk of a message.
135127
136128
Args:
137129
messages: The message(s) to send to the agent.

python/packages/main/agent_framework/_types.py

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
TEmbedding = TypeVar("TEmbedding")
2626
TChatResponse = TypeVar("TChatResponse", bound="ChatResponse")
2727
TChatToolMode = TypeVar("TChatToolMode", bound="ChatToolMode")
28+
TAgentRunResponse = TypeVar("TAgentRunResponse", bound="AgentRunResponse")
2829

2930
CreatedAtT = str # Use a datetimeoffset type? Or a more specific type like datetime.datetime?
3031

@@ -152,7 +153,9 @@ def __iadd__(self, other: "UsageDetails | None") -> Self:
152153
return self
153154

154155

155-
def _process_update(response: "ChatResponse", update: "ChatResponseUpdate") -> None:
156+
def _process_update(
157+
response: "ChatResponse | AgentRunResponse", update: "ChatResponseUpdate | AgentRunResponseUpdate"
158+
) -> None:
156159
"""Processes a single update and modifies the response in place."""
157160
is_new_message = False
158161
if not response.messages or (update.message_id and response.messages[-1].message_id != update.message_id):
@@ -189,19 +192,21 @@ def _process_update(response: "ChatResponse", update: "ChatResponseUpdate") -> N
189192
# Incorporate the update's properties into the response.
190193
if update.response_id:
191194
response.response_id = update.response_id
192-
if update.conversation_id is not None:
193-
response.conversation_id = update.conversation_id
194195
if update.created_at is not None:
195196
response.created_at = update.created_at
196-
if update.finish_reason is not None:
197-
response.finish_reason = update.finish_reason
198-
if update.ai_model_id is not None:
199-
response.ai_model_id = update.ai_model_id
200197
if update.additional_properties is not None:
201198
if response.additional_properties is None:
202199
response.additional_properties = {}
203200
response.additional_properties.update(update.additional_properties)
204201

202+
if isinstance(response, ChatResponse) and isinstance(update, ChatResponseUpdate):
203+
if update.conversation_id is not None:
204+
response.conversation_id = update.conversation_id
205+
if update.finish_reason is not None:
206+
response.finish_reason = update.finish_reason
207+
if update.ai_model_id is not None:
208+
response.ai_model_id = update.ai_model_id
209+
205210

206211
def _coalesce_text_content(
207212
contents: list["AIContents"], type_: type["TextContent"] | type["TextReasoningContent"]
@@ -235,8 +240,8 @@ def _coalesce_text_content(
235240
contents.extend(coalesced_contents)
236241

237242

238-
def _finalize_response(response: "ChatResponse") -> None:
239-
"""Finalizes the chat response by performing any necessary post-processing."""
243+
def _finalize_response(response: "ChatResponse | AgentRunResponse") -> None:
244+
"""Finalizes the response by performing any necessary post-processing."""
240245
for msg in response.messages:
241246
_coalesce_text_content(msg.contents, TextContent)
242247
_coalesce_text_content(msg.contents, TextReasoningContent)
@@ -1554,6 +1559,110 @@ def __iadd__(self, values: Iterable[TEmbedding] | Self) -> Self:
15541559
return self
15551560

15561561

1562+
# region AgentRunResponse
1563+
1564+
1565+
class AgentRunResponse(AFBaseModel):
1566+
"""Represents the response to an Agent run request.
1567+
1568+
Provides one or more response messages and metadata about the response.
1569+
A typical response will contain a single message, but may contain multiple
1570+
messages in scenarios involving function calls, RAG retrievals, or complex logic.
1571+
"""
1572+
1573+
messages: list[ChatMessage] = Field(default_factory=list[ChatMessage])
1574+
response_id: str | None = None
1575+
created_at: CreatedAtT | None = None # use a datetimeoffset type?
1576+
usage_details: UsageDetails | None = None
1577+
raw_representation: Any | None = None
1578+
additional_properties: dict[str, Any] | None = None
1579+
1580+
def __init__(
1581+
self,
1582+
messages: ChatMessage | list[ChatMessage] | None = None,
1583+
response_id: str | None = None,
1584+
created_at: CreatedAtT | None = None,
1585+
usage_details: UsageDetails | None = None,
1586+
raw_representation: Any | None = None,
1587+
additional_properties: dict[str, Any] | None = None,
1588+
**kwargs: Any,
1589+
) -> None:
1590+
"""Initialize an AgentRunResponse.
1591+
1592+
Attributes:
1593+
messages: The list of chat messages in the response.
1594+
response_id: The ID of the chat response.
1595+
created_at: A timestamp for the chat response.
1596+
usage_details: The usage details for the chat response.
1597+
additional_properties: Any additional properties associated with the chat response.
1598+
raw_representation: The raw representation of the chat response from an underlying implementation.
1599+
**kwargs: Additional properties to set on the response.
1600+
"""
1601+
processed_messages: list[ChatMessage] = []
1602+
if messages is not None:
1603+
if isinstance(messages, ChatMessage):
1604+
processed_messages.append(messages)
1605+
elif isinstance(messages, list):
1606+
processed_messages.extend(messages)
1607+
1608+
super().__init__(
1609+
messages=processed_messages, # type: ignore[reportCallIssue]
1610+
response_id=response_id, # type: ignore[reportCallIssue]
1611+
created_at=created_at, # type: ignore[reportCallIssue]
1612+
usage_details=usage_details, # type: ignore[reportCallIssue]
1613+
additional_properties=additional_properties, # type: ignore[reportCallIssue]
1614+
raw_representation=raw_representation, # type: ignore[reportCallIssue]
1615+
**kwargs,
1616+
)
1617+
1618+
@property
1619+
def text(self) -> str:
1620+
"""Get the concatenated text of all messages."""
1621+
return "".join(msg.text for msg in self.messages) if self.messages else ""
1622+
1623+
@classmethod
1624+
def from_agent_run_response_updates(
1625+
cls: type[TAgentRunResponse], updates: Sequence["AgentRunResponseUpdate"]
1626+
) -> TAgentRunResponse:
1627+
"""Joins multiple updates into a single AgentRunResponse."""
1628+
msg = cls(messages=[])
1629+
for update in updates:
1630+
_process_update(msg, update)
1631+
_finalize_response(msg)
1632+
return msg
1633+
1634+
def __str__(self) -> str:
1635+
return self.text
1636+
1637+
1638+
# region AgentRunResponseUpdate
1639+
1640+
1641+
class AgentRunResponseUpdate(AFBaseModel):
1642+
"""Represents a single streaming response chunk from an Agent."""
1643+
1644+
contents: list[AIContents] = Field(default_factory=list[AIContents])
1645+
role: ChatRole | None = None
1646+
author_name: str | None = None
1647+
response_id: str | None = None
1648+
message_id: str | None = None
1649+
created_at: CreatedAtT | None = None # use a datetimeoffset type?
1650+
additional_properties: dict[str, Any] | None = None
1651+
raw_representation: Any | None = None
1652+
1653+
@property
1654+
def text(self) -> str:
1655+
"""Get the concatenated text of all TextContent objects in contents."""
1656+
return (
1657+
"".join(content.text for content in self.contents if isinstance(content, TextContent))
1658+
if self.contents
1659+
else ""
1660+
)
1661+
1662+
def __str__(self) -> str:
1663+
return self.text
1664+
1665+
15571666
# region: SpeechToTextOptions
15581667

15591668

python/packages/main/tests/unit/test_agents.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
from pydantic import BaseModel, Field
88
from pytest import fixture
99

10-
from agent_framework import Agent, AgentThread, ChatMessage, ChatResponse, ChatResponseUpdate, ChatRole, TextContent
10+
from agent_framework import (
11+
Agent,
12+
AgentRunResponse,
13+
AgentRunResponseUpdate,
14+
AgentThread,
15+
ChatMessage,
16+
ChatRole,
17+
TextContent,
18+
)
1119

1220
TThreadType = TypeVar("TThreadType", bound=AgentThread)
1321

@@ -29,25 +37,24 @@ class MockAgent(BaseModel):
2937
id: str = Field(default_factory=lambda: str(uuid4()))
3038
name: str | None = None
3139
description: str | None = None
32-
instructions: str | None = None
3340

3441
async def run(
3542
self,
3643
messages: ChatMessage | str | list[ChatMessage] | None = None,
3744
*,
3845
thread: AgentThread | None = None,
3946
**kwargs: Any,
40-
) -> ChatResponse:
41-
return ChatResponse(messages=[ChatMessage(role=ChatRole.ASSISTANT, contents=[TextContent("Response")])])
47+
) -> AgentRunResponse:
48+
return AgentRunResponse(messages=[ChatMessage(role=ChatRole.ASSISTANT, contents=[TextContent("Response")])])
4249

4350
async def run_stream(
4451
self,
4552
messages: str | ChatMessage | list[ChatMessage] | None = None,
4653
*,
4754
thread: AgentThread | None = None,
4855
**kwargs: Any,
49-
) -> AsyncIterable[ChatResponseUpdate]:
50-
yield ChatResponseUpdate(contents=[TextContent("Response")])
56+
) -> AsyncIterable[AgentRunResponseUpdate]:
57+
yield AgentRunResponseUpdate(contents=[TextContent("Response")])
5158

5259
def get_new_thread(self) -> AgentThread:
5360
return MockAgentThread()
@@ -107,7 +114,7 @@ async def test_agent_run(agent: Agent) -> None:
107114

108115

109116
async def test_agent_run_stream(agent: Agent) -> None:
110-
async def collect_updates(updates: AsyncIterable[ChatResponseUpdate]) -> list[ChatResponseUpdate]:
117+
async def collect_updates(updates: AsyncIterable[AgentRunResponseUpdate]) -> list[AgentRunResponseUpdate]:
111118
return [u async for u in updates]
112119

113120
updates = await collect_updates(agent.run_stream(messages="test"))

0 commit comments

Comments
 (0)