diff --git a/python/packages/main/agent_framework/openai/_responses_client.py b/python/packages/main/agent_framework/openai/_responses_client.py index 92c27e8790..a469ffd0b2 100644 --- a/python/packages/main/agent_framework/openai/_responses_client.py +++ b/python/packages/main/agent_framework/openai/_responses_client.py @@ -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. diff --git a/python/packages/main/agent_framework/openai/_shared.py b/python/packages/main/agent_framework/openai/_shared.py index 86d4a9d2f4..8567f44ecc 100644 --- a/python/packages/main/agent_framework/openai/_shared.py +++ b/python/packages/main/agent_framework/openai/_shared.py @@ -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": diff --git a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py index 3f85881486..623efce0e3 100644 --- a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py @@ -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}") diff --git a/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py b/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py index 1c4fedb775..1d11123a5e 100644 --- a/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py +++ b/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py @@ -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(), diff --git a/python/samples/getting_started/agents/foundry/foundry_with_thread.py b/python/samples/getting_started/agents/foundry/foundry_with_thread.py index a34a32a420..ae596ad510 100644 --- a/python/samples/getting_started/agents/foundry/foundry_with_thread.py +++ b/python/samples/getting_started/agents/foundry/foundry_with_thread.py @@ -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}") diff --git a/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_thread.py b/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_thread.py index 92bd7e113c..cb48ef7b75 100644 --- a/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_thread.py @@ -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}") diff --git a/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_basic.py b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_basic.py new file mode 100644 index 0000000000..6c29260a69 --- /dev/null +++ b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_basic.py @@ -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()) diff --git a/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_code_interpreter.py b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_code_interpreter.py new file mode 100644 index 0000000000..9810ae8eca --- /dev/null +++ b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_code_interpreter.py @@ -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()) diff --git a/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_function_tools.py b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_function_tools.py new file mode 100644 index 0000000000..39176b15f9 --- /dev/null +++ b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_function_tools.py @@ -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()) diff --git a/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_thread.py b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_thread.py new file mode 100644 index 0000000000..2e7f0b6b22 --- /dev/null +++ b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_thread.py @@ -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())