Python: [BREAKING] PR2 — Wire context provider pipeline, remove old types, update all consumers (#3850)

* PR2: Wire context provider pipeline and update all internal consumers

- Replace AgentThread with AgentSession across all packages
- Replace ContextProvider with BaseContextProvider across all packages
- Replace context_provider param with context_providers (Sequence)
- Replace thread= with session= in run() signatures
- Replace get_new_thread() with create_session()
- Add get_session(service_session_id) to agent interface
- DurableAgentThread -> DurableAgentSession
- Remove _notify_thread_of_new_messages from WorkflowAgent
- Wire before_run/after_run context provider pipeline in RawAgent
- Auto-inject InMemoryHistoryProvider when no providers configured

* fix: update all tests for context provider pipeline, fix lazy-loaders, remove old test files

* refactor: update all sample files for context provider pipeline (AgentThread→AgentSession, ContextProvider→BaseContextProvider)

* fix: update remaining ag-ui references (client docstring, getting_started sample)

* fix: make get_session service_session_id keyword-only to avoid confusion with session_id

* refactor: rename _RunContext.thread_messages to session_messages

* refactor: remove _threads.py, _memory.py, and old provider files; migrate devui to use plain message lists

* rename: remove _new_ prefix from test files

* refactor: rewrite SlidingWindowChatMessageStore as SlidingWindowHistoryProvider(InMemoryHistoryProvider)

* fix: read full history from session state directly instead of reaching into provider internals

* fix: update stale .pyi stubs, sample imports, and README references for new provider types

* fix: remove stale message_store, _notify_thread_of_new_messages, and session_id.key references in samples

* refactor: merge context_providers and sessions sample folders into sessions, remove aggregate_context_provider

* refactor: UserInfoMemory stores state in session.state instead of instance attributes

* feat: add Pydantic BaseModel support to session state serialization

Pydantic models stored in session.state are now automatically serialized
via model_dump() and restored via model_validate() during to_dict()/from_dict()
round-trips. Models are auto-registered on first serialization; use
register_state_type() for cold-start deserialization.

Also export register_state_type as a public API.

* fix mem0

* Update sample README links and descriptions for session terminology

- Replace 'thread' with 'session' in sample descriptions across all READMEs
- Update file links for renamed samples (mem0_sessions, redis_sessions, etc.)
- Fix Threads section → Sessions section in main samples/README.md
- Update tools, middleware, workflows, durabletask, azure_functions READMEs
- Update architecture diagrams in concepts/tools/README.md
- Update migration guides (autogen, semantic-kernel)

* Fix broken Redis README link to renamed sample

* Fix Mem0 OSS client search: pass scoping params as direct kwargs

AsyncMemory (OSS) expects user_id/agent_id/run_id as direct kwargs,
while AsyncMemoryClient (Platform) expects them in a filters dict.
Adds tests for both client types.

Port of fix from #3844 to new Mem0ContextProvider.

* Fix rebase issues: restore missing _conversation_state.py and checkpoint decode logic

- Add back _conversation_state.py (encode/decode_chat_messages) lost in rebase
- Fix on_checkpoint_restore to decode cache/conversation with decode_chat_messages
- Fix on_checkpoint_restore to use decode_checkpoint_value for pending requests
- Add tests/workflow/__init__.py for relative import support
- Fix test_agent_executor checkpoint selection (checkpoints[1] not superstep)

* Add STORES_BY_DEFAULT ClassVar to skip redundant InMemoryHistoryProvider injection

Chat clients that store history server-side by default (OpenAI Responses API,
Azure AI Agent) now declare STORES_BY_DEFAULT = True. The agent checks this
during auto-injection and skips InMemoryHistoryProvider unless the user
explicitly sets store=False.

* Fix broken markdown links in azure_ai and redis READMEs

* Fix getting-started samples to use session API instead of removed thread/ContextProvider API

* updates to workflow as agent

* fix group chat import

* Rename Thread→Session throughout, fix service_session_id propagation, remove stale AGUIThread

- Fix: Propagate conversation_id from ChatResponse back to session.service_session_id
  in both streaming and non-streaming paths in _agents.py
- Rename AgentThreadException → AgentSessionException
- Remove stale AGUIThread from ag_ui lazy-loader
- Rename use_service_thread → use_service_session in ag-ui package
- Rename test functions from *_thread_* to *_session_*
- Rename sample files from *_thread* to *_session*
- Update docstrings and comments: thread → session
- Update _mcp.py kwargs filter: add 'session' alongside 'thread'
- Fix ContinuationToken docstring example: thread=thread → session=session
- Fix _clients.py docstring: 'Agent threads' → 'Agent sessions'

* Fix broken markdown links after thread→session file renames

* fix azure ai test
This commit is contained in:
Eduard van Valkenburg
2026-02-12 21:00:32 +00:00
committed by GitHub
parent 0c67dbbce5
commit 1e350ea22f
312 changed files with 6669 additions and 11423 deletions
@@ -14,12 +14,12 @@ This folder contains examples demonstrating different ways to create and use age
| [`openai_assistants_with_file_search.py`](openai_assistants_with_file_search.py) | Using `OpenAIAssistantsClient.get_file_search_tool()` with `OpenAIAssistantProvider` for file search capabilities. |
| [`openai_assistants_with_function_tools.py`](openai_assistants_with_function_tools.py) | Function tools with `OpenAIAssistantProvider` at both agent-level and query-level. |
| [`openai_assistants_with_response_format.py`](openai_assistants_with_response_format.py) | Structured outputs with `OpenAIAssistantProvider` using Pydantic models. |
| [`openai_assistants_with_thread.py`](openai_assistants_with_thread.py) | Thread management with `OpenAIAssistantProvider` for conversation context persistence. |
| [`openai_assistants_with_session.py`](openai_assistants_with_session.py) | Session management with `OpenAIAssistantProvider` for conversation context persistence. |
| [`openai_chat_client_basic.py`](openai_chat_client_basic.py) | The simplest way to create an agent using `Agent` with `OpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with OpenAI models. |
| [`openai_chat_client_with_explicit_settings.py`](openai_chat_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific chat client, configuring settings explicitly including API key and model ID. |
| [`openai_chat_client_with_function_tools.py`](openai_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
| [`openai_chat_client_with_local_mcp.py`](openai_chat_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. |
| [`openai_chat_client_with_thread.py`](openai_chat_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`openai_chat_client_with_session.py`](openai_chat_client_with_session.py) | Demonstrates session management with OpenAI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. |
| [`openai_chat_client_with_web_search.py`](openai_chat_client_with_web_search.py) | Shows how to use `OpenAIChatClient.get_web_search_tool()` for web search capabilities with OpenAI agents. |
| [`openai_chat_client_with_runtime_json_schema.py`](openai_chat_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. |
| [`openai_responses_client_basic.py`](openai_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `OpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with OpenAI models. |
@@ -37,7 +37,7 @@ This folder contains examples demonstrating different ways to create and use age
| [`openai_responses_client_with_local_mcp.py`](openai_responses_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. |
| [`openai_responses_client_with_runtime_json_schema.py`](openai_responses_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. |
| [`openai_responses_client_with_structured_output.py`](openai_responses_client_with_structured_output.py) | Demonstrates how to use structured outputs with OpenAI agents to get structured data responses in predefined formats. |
| [`openai_responses_client_with_thread.py`](openai_responses_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`openai_responses_client_with_session.py`](openai_responses_client_with_session.py) | Demonstrates session management with OpenAI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. |
| [`openai_responses_client_with_web_search.py`](openai_responses_client_with_web_search.py) | Shows how to use `OpenAIResponsesClient.get_web_search_tool()` for web search capabilities. |
## Environment Variables
@@ -20,7 +20,7 @@ assistant lifecycle management, showing both streaming and non-streaming respons
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -22,7 +22,7 @@ This sample demonstrates the methods available on the OpenAIAssistantProvider cl
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,7 @@ using the provider's get_agent() and as_agent() methods.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -20,7 +20,7 @@ settings rather than relying on environment variable defaults.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -19,7 +19,7 @@ showing both agent-level and query-level tool configuration patterns.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -5,22 +5,22 @@ import os
from random import randint
from typing import Annotated
from agent_framework import AgentThread, tool
from agent_framework import AgentSession, tool
from agent_framework.openai import OpenAIAssistantProvider
from openai import AsyncOpenAI
from pydantic import Field
"""
OpenAI Assistants with Thread Management Example
OpenAI Assistants with Session Management Example
This sample demonstrates thread management with OpenAI Assistants, showing
persistent conversation threads and context preservation across interactions.
This sample demonstrates session management with OpenAI Assistants, showing
persistent conversation sessions and context preservation across interactions.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -30,9 +30,9 @@ def get_weather(
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 (service-managed thread)."""
print("=== Automatic Thread Creation Example ===")
async def example_with_automatic_session_creation() -> None:
"""Example showing automatic session creation (service-managed session)."""
print("=== Automatic Session Creation Example ===")
client = AsyncOpenAI()
provider = OpenAIAssistantProvider(client)
@@ -45,26 +45,26 @@ async def example_with_automatic_thread_creation() -> None:
)
try:
# First conversation - no thread provided, will be created automatically
# First conversation - no session 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
# Second conversation - still no session provided, will create another new session
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")
print("Note: Each call creates a separate session, so the agent doesn't remember previous context.\n")
finally:
await client.beta.assistants.delete(agent.id)
async def example_with_thread_persistence() -> None:
"""Example showing thread persistence across multiple conversations."""
print("=== Thread Persistence Example ===")
print("Using the same thread across multiple conversations to maintain context.\n")
async def example_with_session_persistence() -> None:
"""Example showing session persistence across multiple conversations."""
print("=== Session Persistence Example ===")
print("Using the same session across multiple conversations to maintain context.\n")
client = AsyncOpenAI()
provider = OpenAIAssistantProvider(client)
@@ -77,41 +77,41 @@ async def example_with_thread_persistence() -> None:
)
try:
# Create a new thread that will be reused
thread = agent.get_new_thread()
# Create a new session that will be reused
session = agent.create_session()
# First conversation
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
result1 = await agent.run(query1, session=session)
print(f"Agent: {result1.text}")
# Second conversation using the same thread - maintains context
# Second conversation using the same session - maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread)
result2 = await agent.run(query2, session=session)
print(f"Agent: {result2.text}")
# 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)
result3 = await agent.run(query3, session=session)
print(f"Agent: {result3.text}")
print("Note: The agent remembers context from previous messages in the same thread.\n")
print("Note: The agent remembers context from previous messages in the same session.\n")
finally:
await client.beta.assistants.delete(agent.id)
async def example_with_existing_thread_id() -> None:
"""Example showing how to work with an existing thread ID from the service."""
print("=== Existing Thread ID Example ===")
print("Using a specific thread ID to continue an existing conversation.\n")
async def example_with_existing_session_id() -> None:
"""Example showing how to work with an existing session ID from the service."""
print("=== Existing Session ID Example ===")
print("Using a specific session ID to continue an existing conversation.\n")
client = AsyncOpenAI()
provider = OpenAIAssistantProvider(client)
# First, create a conversation and capture the thread ID
existing_thread_id = None
# First, create a conversation and capture the session ID
existing_session_id = None
assistant_id = None
agent = await provider.create_agent(
@@ -123,19 +123,19 @@ async def example_with_existing_thread_id() -> None:
assistant_id = agent.id
try:
# Start a conversation and get the thread ID
thread = agent.get_new_thread()
# Start a conversation and get the session ID
session = agent.create_session()
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
result1 = await agent.run(query1, session=session)
print(f"Agent: {result1.text}")
# The thread ID is set after the first response
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
# The session ID is set after the first response
existing_session_id = session.service_session_id
print(f"Session ID: {existing_session_id}")
if existing_thread_id:
print("\n--- Continuing with the same thread ID using get_agent ---")
if existing_session_id:
print("\n--- Continuing with the same session ID using get_agent ---")
# Get the existing assistant by ID
agent2 = await provider.get_agent(
@@ -143,25 +143,25 @@ async def example_with_existing_thread_id() -> None:
tools=[get_weather], # Must provide function implementations
)
# Create a thread with the existing ID
thread = AgentThread(service_thread_id=existing_thread_id)
# Create a session with the existing ID
session = AgentSession(service_session_id=existing_session_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await agent2.run(query2, thread=thread)
result2 = await agent2.run(query2, session=session)
print(f"Agent: {result2.text}")
print("Note: The agent continues the conversation from the previous thread.\n")
print("Note: The agent continues the conversation from the previous session.\n")
finally:
if assistant_id:
await client.beta.assistants.delete(assistant_id)
async def main() -> None:
print("=== OpenAI Assistants Provider Thread Management Examples ===\n")
print("=== OpenAI Assistants Provider Session Management Examples ===\n")
await example_with_automatic_thread_creation()
await example_with_thread_persistence()
await example_with_existing_thread_id()
await example_with_automatic_session_creation()
await example_with_session_persistence()
await example_with_existing_session_id()
if __name__ == "__main__":
@@ -17,7 +17,7 @@ interactions, showing both streaming and non-streaming responses.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, "The location to get the weather for."],
@@ -19,7 +19,7 @@ settings rather than relying on environment variable defaults.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -19,7 +19,7 @@ showing both agent-level and query-level tool configuration patterns.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -0,0 +1,149 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import Agent, AgentSession, tool
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
"""
OpenAI Chat Client with Session Management Example
This sample demonstrates session management with OpenAI Chat Client, showing
conversation sessions and message history preservation across interactions.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
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_session_creation() -> None:
"""Example showing automatic session creation (service-managed session)."""
print("=== Automatic Session Creation Example ===")
agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# First conversation - no session 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 session provided, will create another new session
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 session, so the agent doesn't remember previous context.\n")
async def example_with_session_persistence() -> None:
"""Example showing session persistence across multiple conversations."""
print("=== Session Persistence Example ===")
print("Using the same session across multiple conversations to maintain context.\n")
agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Create a new session that will be reused
session = agent.create_session()
# First conversation
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, session=session)
print(f"Agent: {result1.text}")
# Second conversation using the same session - maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, session=session)
print(f"Agent: {result2.text}")
# 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, session=session)
print(f"Agent: {result3.text}")
print("Note: The agent remembers context from previous messages in the same session.\n")
async def example_with_existing_session_messages() -> None:
"""Example showing how to work with existing session messages for OpenAI."""
print("=== Existing Session Messages Example ===")
agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Start a conversation and build up message history
session = agent.create_session()
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
result1 = await agent.run(query1, session=session)
print(f"Agent: {result1.text}")
# The session now contains the conversation history in state
memory_state = session.state.get("memory", {})
messages = memory_state.get("messages", [])
if messages:
print(f"Session contains {len(messages)} messages")
print("\n--- Continuing with the same session in a new agent instance ---")
# Create a new agent instance but use the existing session with its message history
new_agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Use the same session object which contains the conversation history
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await new_agent.run(query2, session=session)
print(f"Agent: {result2.text}")
print("Note: The agent continues the conversation using the local message history.\n")
print("\n--- Alternative: Creating a new session from existing messages ---")
new_session = AgentSession()
query3 = "How does the Paris weather compare to London?"
print(f"User: {query3}")
result3 = await new_agent.run(query3, session=new_session)
print(f"Agent: {result3.text}")
print("Note: This creates a new session with the same conversation history.\n")
async def main() -> None:
print("=== OpenAI Chat Client Agent Session Management Examples ===\n")
await example_with_automatic_session_creation()
await example_with_session_persistence()
await example_with_existing_session_messages()
if __name__ == "__main__":
asyncio.run(main())
@@ -1,151 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import Agent, AgentThread, ChatMessageStore, tool
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
"""
OpenAI Chat Client with Thread Management Example
This sample demonstrates thread management with OpenAI Chat Client, showing
conversation threads and message history preservation across interactions.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
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 (service-managed thread)."""
print("=== Automatic Thread Creation Example ===")
agent = Agent(
client=OpenAIChatClient(),
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() -> None:
"""Example showing thread persistence across multiple conversations."""
print("=== Thread Persistence Example ===")
print("Using the same thread across multiple conversations to maintain context.\n")
agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Create a new thread that will be reused
thread = agent.get_new_thread()
# 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}")
# 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}")
# 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("Note: The agent remembers context from previous messages in the same thread.\n")
async def example_with_existing_thread_messages() -> None:
"""Example showing how to work with existing thread messages for OpenAI."""
print("=== Existing Thread Messages Example ===")
agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Start a conversation and build up message history
thread = agent.get_new_thread()
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
print(f"Agent: {result1.text}")
# The thread now contains the conversation history in memory
if thread.message_store:
messages = await thread.message_store.list_messages()
print(f"Thread contains {len(messages or [])} messages")
print("\n--- Continuing with the same thread in a new agent instance ---")
# Create a new agent instance but use the existing thread with its message history
new_agent = Agent(
client=OpenAIChatClient(),
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Use the same thread object which contains the conversation history
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await new_agent.run(query2, thread=thread)
print(f"Agent: {result2.text}")
print("Note: The agent continues the conversation using the local message history.\n")
print("\n--- Alternative: Creating a new thread from existing messages ---")
# You can also create a new thread from existing messages
messages = await thread.message_store.list_messages() if thread.message_store else []
new_thread = AgentThread(message_store=ChatMessageStore(messages))
query3 = "How does the Paris weather compare to London?"
print(f"User: {query3}")
result3 = await new_agent.run(query3, thread=new_thread)
print(f"Agent: {result3.text}")
print("Note: This creates a new thread with the same conversation history.\n")
async def main() -> None:
print("=== OpenAI Chat Client Agent Thread Management Examples ===\n")
await example_with_automatic_thread_creation()
await example_with_thread_persistence()
await example_with_existing_thread_messages()
if __name__ == "__main__":
asyncio.run(main())
@@ -68,7 +68,7 @@ async def security_and_override_middleware(
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -19,7 +19,7 @@ settings rather than relying on environment variable defaults.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -19,7 +19,7 @@ showing both agent-level and query-level tool configuration patterns.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -14,11 +14,11 @@ OpenAI Responses Client, including user approval workflows for function call sec
"""
if TYPE_CHECKING:
from agent_framework import AgentThread, SupportsAgentRun
from agent_framework import AgentSession, SupportsAgentRun
async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun"):
"""When we don't have a thread, we need to ensure we return with the input, approval request and approval."""
async def handle_approvals_without_session(query: str, agent: "SupportsAgentRun"):
"""When we don't have a session, we need to ensure we return with the input, approval request and approval."""
from agent_framework import Message
result = await agent.run(query)
@@ -42,11 +42,11 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun")
return result
async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", thread: "AgentThread"):
"""Here we let the thread deal with the previous responses, and we just rerun with the approval."""
async def handle_approvals_with_session(query: str, agent: "SupportsAgentRun", session: "AgentSession"):
"""Here we let the session deal with the previous responses, and we just rerun with the approval."""
from agent_framework import Message
result = await agent.run(query, thread=thread, store=True)
result = await agent.run(query, session=session, store=True)
while len(result.user_input_requests) > 0:
new_input: list[Any] = []
for user_input_needed in result.user_input_requests:
@@ -61,12 +61,12 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th
contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")],
)
)
result = await agent.run(new_input, thread=thread, store=True)
result = await agent.run(new_input, session=session, store=True)
return result
async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAgentRun", thread: "AgentThread"):
"""Here we let the thread deal with the previous responses, and we just rerun with the approval."""
async def handle_approvals_with_session_streaming(query: str, agent: "SupportsAgentRun", session: "AgentSession"):
"""Here we let the session deal with the previous responses, and we just rerun with the approval."""
from agent_framework import Message
new_input: list[Message] = []
@@ -74,7 +74,7 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAge
while new_input_added:
new_input_added = False
new_input.append(Message(role="user", text=query))
async for update in agent.run(new_input, thread=thread, stream=True, options={"store": True}):
async for update in agent.run(new_input, session=session, stream=True, options={"store": True}):
if update.user_input_requests:
for user_input_needed in update.user_input_requests:
print(
@@ -93,9 +93,9 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAge
yield update
async def run_hosted_mcp_without_thread_and_specific_approval() -> None:
"""Example showing Mcp Tools with approvals without using a thread."""
print("=== Mcp with approvals and without thread ===")
async def run_hosted_mcp_without_session_and_specific_approval() -> None:
"""Example showing Mcp Tools with approvals without using a session."""
print("=== Mcp with approvals and without session ===")
client = OpenAIResponsesClient()
# Create MCP tool with specific approval mode
@@ -116,13 +116,13 @@ async def run_hosted_mcp_without_thread_and_specific_approval() -> None:
# First query
query1 = "How to create an Azure storage account using az cli?"
print(f"User: {query1}")
result1 = await handle_approvals_without_thread(query1, agent)
result1 = await handle_approvals_without_session(query1, agent)
print(f"{agent.name}: {result1}\n")
print("\n=======================================\n")
# Second query
query2 = "What is Microsoft Agent Framework?"
print(f"User: {query2}")
result2 = await handle_approvals_without_thread(query2, agent)
result2 = await handle_approvals_without_session(query2, agent)
print(f"{agent.name}: {result2}\n")
@@ -148,19 +148,19 @@ async def run_hosted_mcp_without_approval() -> None:
# First query
query1 = "How to create an Azure storage account using az cli?"
print(f"User: {query1}")
result1 = await handle_approvals_without_thread(query1, agent)
result1 = await handle_approvals_without_session(query1, agent)
print(f"{agent.name}: {result1}\n")
print("\n=======================================\n")
# Second query
query2 = "What is Microsoft Agent Framework?"
print(f"User: {query2}")
result2 = await handle_approvals_without_thread(query2, agent)
result2 = await handle_approvals_without_session(query2, agent)
print(f"{agent.name}: {result2}\n")
async def run_hosted_mcp_with_thread() -> None:
"""Example showing Mcp Tools with approvals using a thread."""
print("=== Mcp with approvals and with thread ===")
async def run_hosted_mcp_with_session() -> None:
"""Example showing Mcp Tools with approvals using a session."""
print("=== Mcp with approvals and with session ===")
client = OpenAIResponsesClient()
# Create MCP tool that always requires approval
@@ -178,22 +178,22 @@ async def run_hosted_mcp_with_thread() -> None:
tools=mcp_tool,
) as agent:
# First query
thread = agent.get_new_thread()
session = agent.create_session()
query1 = "How to create an Azure storage account using az cli?"
print(f"User: {query1}")
result1 = await handle_approvals_with_thread(query1, agent, thread)
result1 = await handle_approvals_with_session(query1, agent, session)
print(f"{agent.name}: {result1}\n")
print("\n=======================================\n")
# Second query
query2 = "What is Microsoft Agent Framework?"
print(f"User: {query2}")
result2 = await handle_approvals_with_thread(query2, agent, thread)
result2 = await handle_approvals_with_session(query2, agent, session)
print(f"{agent.name}: {result2}\n")
async def run_hosted_mcp_with_thread_streaming() -> None:
"""Example showing Mcp Tools with approvals using a thread."""
print("=== Mcp with approvals and with thread ===")
async def run_hosted_mcp_with_session_streaming() -> None:
"""Example showing Mcp Tools with approvals using a session."""
print("=== Mcp with approvals and with session ===")
client = OpenAIResponsesClient()
# Create MCP tool that always requires approval
@@ -211,11 +211,11 @@ async def run_hosted_mcp_with_thread_streaming() -> None:
tools=mcp_tool,
) as agent:
# First query
thread = agent.get_new_thread()
session = agent.create_session()
query1 = "How to create an Azure storage account using az cli?"
print(f"User: {query1}")
print(f"{agent.name}: ", end="")
async for update in handle_approvals_with_thread_streaming(query1, agent, thread):
async for update in handle_approvals_with_session_streaming(query1, agent, session):
print(update, end="")
print("\n")
print("\n=======================================\n")
@@ -223,7 +223,7 @@ async def run_hosted_mcp_with_thread_streaming() -> None:
query2 = "What is Microsoft Agent Framework?"
print(f"User: {query2}")
print(f"{agent.name}: ", end="")
async for update in handle_approvals_with_thread_streaming(query2, agent, thread):
async for update in handle_approvals_with_session_streaming(query2, agent, session):
print(update, end="")
print("\n")
@@ -232,9 +232,9 @@ async def main() -> None:
print("=== OpenAI Responses Client Agent with Hosted Mcp Tools Examples ===\n")
await run_hosted_mcp_without_approval()
await run_hosted_mcp_without_thread_and_specific_approval()
await run_hosted_mcp_with_thread()
await run_hosted_mcp_with_thread_streaming()
await run_hosted_mcp_without_session_and_specific_approval()
await run_hosted_mcp_with_session()
await run_hosted_mcp_with_session_streaming()
if __name__ == "__main__":
@@ -4,21 +4,21 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import Agent, AgentThread, tool
from agent_framework import Agent, AgentSession, tool
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
"""
OpenAI Responses Client with Thread Management Example
OpenAI Responses Client with Session Management Example
This sample demonstrates thread management with OpenAI Responses Client, showing
This sample demonstrates session management with OpenAI Responses Client, showing
persistent conversation context and simplified response handling.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -28,9 +28,9 @@ def get_weather(
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 ===")
async def example_with_automatic_session_creation() -> None:
"""Example showing automatic session creation."""
print("=== Automatic Session Creation Example ===")
agent = Agent(
client=OpenAIResponsesClient(),
@@ -38,26 +38,26 @@ async def example_with_automatic_thread_creation() -> None:
tools=get_weather,
)
# First conversation - no thread provided, will be created automatically
# First conversation - no session 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
# Second conversation - still no session provided, will create another new session
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")
print("Note: Each call creates a separate session, so the agent doesn't remember previous context.\n")
async def example_with_thread_persistence_in_memory() -> None:
async def example_with_session_persistence_in_memory() -> None:
"""
Example showing thread persistence across multiple conversations.
Example showing session persistence across multiple conversations.
In this example, messages are stored in-memory.
"""
print("=== Thread Persistence Example (In-Memory) ===")
print("=== Session Persistence Example (In-Memory) ===")
agent = Agent(
client=OpenAIResponsesClient(),
@@ -65,38 +65,38 @@ async def example_with_thread_persistence_in_memory() -> None:
tools=get_weather,
)
# Create a new thread that will be reused
thread = agent.get_new_thread()
# Create a new session that will be reused
session = agent.create_session()
# First conversation
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread, store=False)
result1 = await agent.run(query1, session=session, store=False)
print(f"Agent: {result1.text}")
# Second conversation using the same thread - maintains context
# Second conversation using the same session - maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread, store=False)
result2 = await agent.run(query2, session=session, store=False)
print(f"Agent: {result2.text}")
# 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, store=False)
result3 = await agent.run(query3, session=session, store=False)
print(f"Agent: {result3.text}")
print("Note: The agent remembers context from previous messages in the same thread.\n")
print("Note: The agent remembers context from previous messages in the same session.\n")
async def example_with_existing_thread_id() -> None:
async def example_with_existing_session_id() -> None:
"""
Example showing how to work with an existing thread ID from the service.
Example showing how to work with an existing session ID from the service.
In this example, messages are stored on the server using OpenAI conversation state.
"""
print("=== Existing Thread ID Example ===")
print("=== Existing Session ID Example ===")
# First, create a conversation and capture the thread ID
existing_thread_id = None
# First, create a conversation and capture the session ID
existing_session_id = None
agent = Agent(
client=OpenAIResponsesClient(),
@@ -104,20 +104,20 @@ async def example_with_existing_thread_id() -> None:
tools=get_weather,
)
# Start a conversation and get the thread ID
thread = agent.get_new_thread()
# Start a conversation and get the session ID
session = agent.create_session()
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
result1 = await agent.run(query1, session=session)
print(f"Agent: {result1.text}")
# The thread ID is set after the first response
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
# The session ID is set after the first response
existing_session_id = session.service_session_id
print(f"Session ID: {existing_session_id}")
if existing_thread_id:
print("\n--- Continuing with the same thread ID in a new agent instance ---")
if existing_session_id:
print("\n--- Continuing with the same session ID in a new agent instance ---")
agent = Agent(
client=OpenAIResponsesClient(),
@@ -125,22 +125,22 @@ async def example_with_existing_thread_id() -> None:
tools=get_weather,
)
# Create a thread with the existing ID
thread = AgentThread(service_thread_id=existing_thread_id)
# Create a session with the existing ID
session = AgentSession(service_session_id=existing_session_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await agent.run(query2, thread=thread)
result2 = await agent.run(query2, session=session)
print(f"Agent: {result2.text}")
print("Note: The agent continues the conversation from the previous thread by using thread ID.\n")
print("Note: The agent continues the conversation from the previous session by using session ID.\n")
async def main() -> None:
print("=== OpenAI Response Client Agent Thread Management Examples ===\n")
print("=== OpenAI Response Client Agent Session Management Examples ===\n")
await example_with_automatic_thread_creation()
await example_with_thread_persistence_in_memory()
await example_with_existing_thread_id()
await example_with_automatic_session_creation()
await example_with_session_persistence_in_memory()
await example_with_existing_session_id()
if __name__ == "__main__":