Python: OpenAI Responses Agent - threads, code interpreter, bug fixes and examples (#242)

* Added basic example with small fix

* Added example with function tools

* Added example with thread management

* Small renaming

* Added example with code interpreter
This commit is contained in:
Dmytro Struk
2025-07-28 10:07:29 -07:00
committed by GitHub
Unverified
parent 190035bb69
commit c794892d79
10 changed files with 403 additions and 35 deletions
@@ -13,6 +13,7 @@ else:
from openai import AsyncOpenAI, AsyncStream
from openai.types.responses.response import Response as OpenAIResponse
from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall
from openai.types.responses.response_completed_event import ResponseCompletedEvent
from openai.types.responses.response_content_part_added_event import ResponseContentPartAddedEvent
from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
@@ -27,6 +28,7 @@ from openai.types.responses.response_usage import ResponseUsage
from pydantic import BaseModel, SecretStr, ValidationError
from .._clients import ChatClientBase, use_tool_calling
from .._tools import HostedCodeInterpreterTool
from .._types import (
AIContents,
AITool,
@@ -189,8 +191,9 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
timeout=timeout,
)
filtered_options.update(additional_properties or {})
chat_options = ChatOptions(
ai_model_id=model,
return await super().get_response(
messages=messages,
model=model,
max_tokens=max_tokens,
response_format=response_format,
seed=seed,
@@ -198,13 +201,9 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
temperature=temperature,
top_p=top_p,
tool_choice=tool_choice,
tools=tools, # type: ignore
tools=tools,
user=user,
additional_properties=filtered_options,
)
return await super().get_response(
messages=messages,
chat_options=chat_options,
**kwargs,
)
@@ -282,8 +281,9 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
timeout=timeout,
)
filtered_options.update(additional_properties or {})
chat_options = ChatOptions(
ai_model_id=model,
async for update in super().get_streaming_response(
messages=messages,
model=model,
max_tokens=max_tokens,
response_format=response_format,
seed=seed,
@@ -291,13 +291,9 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
temperature=temperature,
top_p=top_p,
tool_choice=tool_choice,
tools=tools, # type: ignore
tools=tools,
user=user,
additional_properties=filtered_options,
)
async for update in super().get_streaming_response(
messages=messages,
chat_options=chat_options,
**kwargs,
):
yield update
@@ -307,6 +303,8 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
for tool in tools:
if isinstance(tool, AITool):
# TODO(peterychang): Support AITools
if isinstance(tool, HostedCodeInterpreterTool):
response_tools.append({"type": "code_interpreter", "container": {"type": "auto"}})
continue
if "function" not in tool:
response_tools.append(tool if isinstance(tool, dict) else dict(tool))
@@ -337,7 +335,7 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
})
response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages))
assert isinstance(response, OpenAIResponse) # nosec # noqa: S101
return next(self._create_response_content(response, item) for item in response.output)
return next(self._create_response_content(response, item, store=chat_options.store) for item in response.output)
async def _inner_get_streaming_response(
self,
@@ -357,12 +355,14 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
if not isinstance(response, AsyncStream):
raise ServiceInvalidResponseError("Expected an AsyncStream[ResponseStreamEvent] response.")
async for chunk in response:
update = self._create_streaming_response_content(chunk) # type: ignore
update = self._create_streaming_response_content(chunk, store=chat_options.store) # type: ignore
if not update:
continue
yield update
def _create_response_content(self, response: OpenAIResponse, item: ResponseOutputItem) -> "ChatResponse":
def _create_response_content(
self, response: OpenAIResponse, item: ResponseOutputItem, store: bool | None
) -> "ChatResponse":
"""Create a chat message content object from a choice."""
items: MutableSequence[ChatMessage] = []
metadata: dict[str, Any] = response.metadata or {}
@@ -377,8 +377,11 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
metadata.update(self._get_metadata_from_response(content))
elif isinstance(content, ResponseOutputRefusal):
items.append(ChatMessage(role=item.role, text=content.refusal))
if isinstance(item, ResponseCodeInterpreterToolCall):
items.append(ChatMessage(role=ChatRole.ASSISTANT, text=response.output_text))
return ChatResponse(
response_id=response.id,
conversation_id=response.id if store is True else None,
created_at=datetime.fromtimestamp(response.created_at).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
usage_details=self._usage_details_from_openai(response.usage) if response.usage else None,
messages=items,
@@ -388,12 +391,12 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
)
def _create_streaming_response_content(
self,
event: OpenAIResponseStreamEvent,
self, event: OpenAIResponseStreamEvent, store: bool | None
) -> ChatResponseUpdate | None:
"""Create a streaming chat message content object from a choice."""
metadata: dict[str, Any] = {}
items: list[AIContents] = []
conversation_id: str | None = None
# TODO(peterychang): Add support for other content types
if isinstance(event, ResponseContentPartAddedEvent):
if isinstance(event.part, ResponseOutputText):
@@ -405,6 +408,7 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
items.append(TextContent(text=event.delta))
metadata.update(self._get_metadata_from_response(event))
elif isinstance(event, ResponseCompletedEvent):
conversation_id = event.response.id if store is True else None
# Tool calls are available in the completed event
if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_response(event.response)]:
items.extend(parsed_tool_calls)
@@ -412,6 +416,7 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
return None
return ChatResponseUpdate(
contents=items,
conversation_id=conversation_id,
role=ChatRole.ASSISTANT,
ai_model_id=self.ai_model_id,
additional_properties=metadata,
@@ -455,21 +460,22 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
match content:
case FunctionResultContent():
new_args: dict[str, Any] = {}
new_args.update(self._openai_content_parser(content, tool_id_to_call_id))
new_args.update(self._openai_content_parser(message.role, content, tool_id_to_call_id))
all_messages.append(new_args)
case FunctionCallContent():
function_call = self._openai_content_parser(content, tool_id_to_call_id)
function_call = self._openai_content_parser(message.role, content, tool_id_to_call_id)
all_messages.append(function_call) # type: ignore
case _:
if "content" not in args:
args["content"] = []
args["content"].append(self._openai_content_parser(content, tool_id_to_call_id)) # type: ignore
args["content"].append(self._openai_content_parser(message.role, content, tool_id_to_call_id)) # type: ignore
if "content" in args or "tool_calls" in args:
all_messages.append(args)
return all_messages
def _openai_content_parser(
self,
role: ChatRole,
content: AIContents,
tool_id_to_call_id: dict[str, str],
) -> dict[str, Any]:
@@ -492,19 +498,14 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
}
case TextContent():
return {
"type": "input_text",
"type": "output_text" if role == ChatRole.ASSISTANT else "input_text",
"text": content.text,
}
# TODO(peterychang): We'll probably need to specialize the other content types as well
case _:
return content.model_dump(exclude_none=True)
def _prepare_chat_history_for_request(
self,
chat_messages: Sequence[ChatMessage],
role_key: str = "role",
content_key: str = "content",
) -> list[dict[str, Any]]:
def _prepare_chat_history_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]:
"""Prepare the chat history for a request.
Allowing customization of the key names for role/author, and optionally overriding the role.
@@ -517,8 +518,6 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
Args:
chat_messages: The chat history to prepare.
role_key: The key name for the role/author.
content_key: The key name for the content/message.
Returns:
prepared_chat_history (Any): The prepared chat history for a request.
@@ -238,6 +238,11 @@ class OpenAIHandler(AFBaseModel, ABC):
**options_dict,
text_format=resp_format,
)
if "store" not in options_dict:
options_dict["store"] = False
if "conversation_id" in options_dict:
options_dict["previous_response_id"] = options_dict["conversation_id"]
options_dict.pop("conversation_id")
return await self.client.responses.create(**options_dict) # type: ignore
except BadRequestError as ex:
if ex.code == "content_filter":
@@ -62,7 +62,7 @@ async def example_with_thread_persistence() -> None:
print(f"Agent: {result1.text}")
# Second conversation using the same thread - maintains context
query2 = "How about comparing it to London?"
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread)
print(f"Agent: {result2.text}")
@@ -33,7 +33,7 @@ def get_code_interpreter_chunk(chunk: AgentRunResponseUpdate) -> str | None:
async def main() -> None:
"""Example showing how to use the HostedCodeInterpreterTool with Foundry."""
print("=== Foundry Chat Client with Code Interpreter Example ===")
print("=== Foundry Agent with Code Interpreter Example ===")
async with ChatClientAgent(
chat_client=FoundryChatClient(),
@@ -60,7 +60,7 @@ async def example_with_thread_persistence() -> None:
print(f"Agent: {result1.text}")
# Second conversation using the same thread - maintains context
query2 = "How about comparing it to London?"
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread)
print(f"Agent: {result2.text}")
@@ -62,7 +62,7 @@ async def example_with_thread_persistence() -> None:
print(f"Agent: {result1.text}")
# Second conversation using the same thread - maintains context
query2 = "How about comparing it to London?"
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread)
print(f"Agent: {result2.text}")
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def non_streaming_example() -> None:
"""Example of non-streaming response (get the complete result at once)."""
print("=== Non-streaming Response Example ===")
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
query = "What's the weather like in Seattle?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Result: {result}\n")
async def streaming_example() -> None:
"""Example of streaming response (get results as they are generated)."""
print("=== Streaming Response Example ===")
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
query = "What's the weather like in Portland?"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run_stream(query):
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
async def main() -> None:
print("=== Basic OpenAI Responses Client Agent Example ===")
await non_streaming_example()
await streaming_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,37 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import ChatClientAgent, HostedCodeInterpreterTool
from agent_framework.openai import OpenAIResponsesClient
from openai.types.responses.response import Response as OpenAIResponse
from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall
async def main() -> None:
"""Example showing how to use the HostedCodeInterpreterTool with OpenAI Responses."""
print("=== OpenAI Responses Agent with Code Interpreter Example ===")
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful assistant that can write and execute Python code to solve problems.",
tools=HostedCodeInterpreterTool(),
)
query = "What is current datetime?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Result: {result}\n")
if (
isinstance(result.raw_representation, OpenAIResponse)
and len(result.raw_representation.output) > 0
and isinstance(result.raw_representation.output[0], ResponseCodeInterpreterToolCall)
):
generated_code = result.raw_representation.output[0].code
print(f"Generated code:\n{generated_code}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,120 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from datetime import datetime, timezone
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
def get_time() -> str:
"""Get the current UTC time."""
current_time = datetime.now(timezone.utc)
return f"The current UTC time is {current_time.strftime('%Y-%m-%d %H:%M:%S')}."
async def tools_on_agent_level() -> None:
"""Example showing tools defined when creating the agent."""
print("=== Tools Defined on Agent Level ===")
# Tools are provided when creating the agent
# The agent can use these tools for any query during its lifetime
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful assistant that can provide weather and time information.",
tools=[get_weather, get_time], # Tools defined at agent creation
)
# First query - agent can use weather tool
query1 = "What's the weather like in New York?"
print(f"User: {query1}")
result1 = await agent.run(query1)
print(f"Agent: {result1}\n")
# Second query - agent can use time tool
query2 = "What's the current UTC time?"
print(f"User: {query2}")
result2 = await agent.run(query2)
print(f"Agent: {result2}\n")
# Third query - agent can use both tools if needed
query3 = "What's the weather in London and what's the current UTC time?"
print(f"User: {query3}")
result3 = await agent.run(query3)
print(f"Agent: {result3}\n")
async def tools_on_run_level() -> None:
"""Example showing tools passed to the run method."""
print("=== Tools Passed to Run Method ===")
# Agent created without tools
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful assistant.",
# No tools defined here
)
# First query with weather tool
query1 = "What's the weather like in Seattle?"
print(f"User: {query1}")
result1 = await agent.run(query1, tools=[get_weather]) # Tool passed to run method
print(f"Agent: {result1}\n")
# Second query with time tool
query2 = "What's the current UTC time?"
print(f"User: {query2}")
result2 = await agent.run(query2, tools=[get_time]) # Different tool for this query
print(f"Agent: {result2}\n")
# Third query with multiple tools
query3 = "What's the weather in Chicago and what's the current UTC time?"
print(f"User: {query3}")
result3 = await agent.run(query3, tools=[get_weather, get_time]) # Multiple tools
print(f"Agent: {result3}\n")
async def mixed_tools_example() -> None:
"""Example showing both agent-level tools and run-method tools."""
print("=== Mixed Tools Example (Agent + Run Method) ===")
# Agent created with some base tools
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a comprehensive assistant that can help with various information requests.",
tools=[get_weather], # Base tool available for all queries
)
# Query using both agent tool and additional run-method tools
query = "What's the weather in Denver and what's the current UTC time?"
print(f"User: {query}")
# Agent has access to get_weather (from creation) + additional tools from run method
result = await agent.run(
query,
tools=[get_time], # Additional tools for this specific query
)
print(f"Agent: {result}\n")
async def main() -> None:
print("=== OpenAI Responses Client Agent with Function Tools Examples ===\n")
await tools_on_agent_level()
await tools_on_run_level()
await mixed_tools_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,144 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def example_with_automatic_thread_creation() -> None:
"""Example showing automatic thread creation."""
print("=== Automatic Thread Creation Example ===")
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# First conversation - no thread provided, will be created automatically
query1 = "What's the weather like in Seattle?"
print(f"User: {query1}")
result1 = await agent.run(query1)
print(f"Agent: {result1.text}")
# Second conversation - still no thread provided, will create another new thread
query2 = "What was the last city I asked about?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2)
print(f"Agent: {result2.text}")
print("Note: Each call creates a separate thread, so the agent doesn't remember previous context.\n")
async def example_with_thread_persistence_in_memory() -> None:
"""
Example showing thread persistence across multiple conversations.
In this example, messages are stored in-memory.
"""
print("=== Thread Persistence Example (In-Memory) ===")
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Create a new thread that will be reused
thread = agent.get_new_thread()
assert isinstance(thread, ChatClientAgentThread)
# First conversation
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
print(f"Agent: {result1.text}")
print(f"Thread contains {len(thread.chat_messages or [])} messages in-memory.")
# Second conversation using the same thread - maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread)
print(f"Agent: {result2.text}")
print(f"Thread contains {len(thread.chat_messages or [])} messages in-memory.")
# Third conversation - agent should remember both previous cities
query3 = "Which of the cities I asked about has better weather?"
print(f"\nUser: {query3}")
result3 = await agent.run(query3, thread=thread)
print(f"Agent: {result3.text}")
print(f"Thread contains {len(thread.chat_messages or [])} messages in-memory.")
print("Note: The agent remembers context from previous messages in the same thread.\n")
async def example_with_existing_thread_id() -> None:
"""
Example showing how to work with an existing thread ID from the service.
In this example, messages are stored on the server using OpenAI conversation state.
"""
print("=== Existing Thread ID Example ===")
# First, create a conversation and capture the thread ID
existing_thread_id = None
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Start a conversation and get the thread ID
thread = agent.get_new_thread()
assert isinstance(thread, ChatClientAgentThread)
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
# Enable OpenAI conversation state by setting `store` parameter to True
result1 = await agent.run(query1, thread=thread, store=True)
print(f"Agent: {result1.text}")
print(f"Thread contains {len(thread.chat_messages or [])} messages in-memory.")
# The thread ID is set after the first response
existing_thread_id = thread.id
print(f"Thread ID: {existing_thread_id}")
if existing_thread_id:
print("\n--- Continuing with the same thread ID in a new agent instance ---")
agent = ChatClientAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Create a thread with the existing ID
thread = ChatClientAgentThread(id=existing_thread_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await agent.run(query2, thread=thread, store=True)
print(f"Agent: {result2.text}")
print(f"Thread contains {len(thread.chat_messages or [])} messages in-memory.")
print("Note: The agent continues the conversation from the previous thread.\n")
async def main() -> None:
print("=== OpenAI Response Client Agent Thread Management Examples ===\n")
await example_with_automatic_thread_creation()
await example_with_thread_persistence_in_memory()
await example_with_existing_thread_id()
if __name__ == "__main__":
asyncio.run(main())