Python: Thread storage and serialization (#394)

* Updates for message store support

* Added unit tests

* Added suspend-resume example

* Added example with custom chat message store

* Small fix

* Addressed PR feedback

* Renaming and documentation

* More renaming

* Addressed more PR feedback

* Small fixes in Foundry chat client and examples

* Small update

* Addressed PR feedback

* Increased timeout for Azure tests
This commit is contained in:
Dmytro Struk
2025-08-14 14:15:42 -07:00
committed by GitHub
Unverified
parent 95cb20ca40
commit dea736e550
18 changed files with 1151 additions and 316 deletions
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent
from agent_framework.azure import AzureAssistantsClient
from azure.identity import DefaultAzureCredential
from pydantic import Field
@@ -95,7 +95,7 @@ async def example_with_existing_thread_id() -> None:
print(f"Agent: {result1.text}")
# The thread ID is set after the first response
existing_thread_id = thread.id
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
if existing_thread_id:
@@ -108,7 +108,7 @@ async def example_with_existing_thread_id() -> None:
tools=get_weather,
) as agent:
# Create a thread with the existing ID
thread = ChatClientAgentThread(id=existing_thread_id)
thread = AgentThread(service_thread_id=existing_thread_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent, ChatMessageList
from agent_framework.azure import AzureChatClient
from azure.identity import DefaultAzureCredential
from pydantic import Field
@@ -88,7 +88,6 @@ async def example_with_existing_thread_messages() -> None:
# Start a conversation and build up message history
thread = agent.get_new_thread()
assert isinstance(thread, ChatClientAgentThread) # Ensure we have the right type
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
@@ -96,7 +95,9 @@ async def example_with_existing_thread_messages() -> None:
print(f"Agent: {result1.text}")
# The thread now contains the conversation history in memory
message_count = len(thread.chat_messages or [])
messages = await thread.list_messages()
message_count = len(messages or [])
print(f"Thread contains {message_count} messages")
print("\n--- Continuing with the same thread in a new agent instance ---")
@@ -118,8 +119,9 @@ async def example_with_existing_thread_messages() -> None:
print("\n--- Alternative: Creating a new thread from existing messages ---")
# You can also create a new thread from existing messages
existing_messages = thread.chat_messages or []
new_thread = ChatClientAgentThread(messages=existing_messages)
messages = await thread.list_messages()
new_thread = AgentThread(message_store=ChatMessageList(messages))
query3 = "How does the Paris weather compare to London?"
print(f"User: {query3}")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent
from agent_framework.azure import AzureResponsesClient
from azure.identity import DefaultAzureCredential
from pydantic import Field
@@ -57,28 +57,27 @@ async def example_with_thread_persistence_in_memory() -> None:
# 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.")
print(f"Thread contains {len(await thread.list_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.")
print(f"Thread contains {len(await thread.list_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(f"Thread contains {len(await thread.list_messages() or [])} messages in-memory.")
print("Note: The agent remembers context from previous messages in the same thread.\n")
@@ -100,17 +99,16 @@ async def example_with_existing_thread_id() -> None:
# 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 Azure 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.")
print(f"Thread contains {len(await thread.list_messages() or [])} messages in-memory.")
# The thread ID is set after the first response
existing_thread_id = thread.id
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
if existing_thread_id:
@@ -123,13 +121,13 @@ async def example_with_existing_thread_id() -> None:
)
# Create a thread with the existing ID
thread = ChatClientAgentThread(id=existing_thread_id)
thread = AgentThread(service_thread_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(f"Thread contains {len(await thread.list_messages() or [])} messages in-memory.")
print("Note: The agent continues the conversation from the previous thread by using thread ID.\n")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import DefaultAzureCredential
from pydantic import Field
@@ -104,7 +104,7 @@ async def example_with_existing_thread_id() -> None:
print(f"Agent: {result1.text}")
# The thread ID is set after the first response
existing_thread_id = thread.id
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
if existing_thread_id:
@@ -120,7 +120,7 @@ async def example_with_existing_thread_id() -> None:
) as agent,
):
# Create a thread with the existing ID
thread = ChatClientAgentThread(id=existing_thread_id)
thread = AgentThread(service_thread_id=existing_thread_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent
from agent_framework.openai import OpenAIAssistantsClient
from pydantic import Field
@@ -94,7 +94,7 @@ async def example_with_existing_thread_id() -> None:
print(f"Agent: {result1.text}")
# The thread ID is set after the first response
existing_thread_id = thread.id
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
if existing_thread_id:
@@ -107,7 +107,7 @@ async def example_with_existing_thread_id() -> None:
tools=get_weather,
) as agent:
# Create a thread with the existing ID
thread = ChatClientAgentThread(id=existing_thread_id)
thread = AgentThread(service_thread_id=existing_thread_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent, ChatMessageList
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
@@ -87,7 +87,6 @@ async def example_with_existing_thread_messages() -> None:
# Start a conversation and build up message history
thread = agent.get_new_thread()
assert isinstance(thread, ChatClientAgentThread) # Ensure we have the right type
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
@@ -95,7 +94,9 @@ async def example_with_existing_thread_messages() -> None:
print(f"Agent: {result1.text}")
# The thread now contains the conversation history in memory
message_count = len(thread.chat_messages or [])
messages = await thread.list_messages()
message_count = len(messages or [])
print(f"Thread contains {message_count} messages")
print("\n--- Continuing with the same thread in a new agent instance ---")
@@ -117,8 +118,9 @@ async def example_with_existing_thread_messages() -> None:
print("\n--- Alternative: Creating a new thread from existing messages ---")
# You can also create a new thread from existing messages
existing_messages = thread.chat_messages or []
new_thread = ChatClientAgentThread(messages=existing_messages)
messages = await thread.list_messages()
new_thread = AgentThread(message_store=ChatMessageList(messages))
query3 = "How does the Paris weather compare to London?"
print(f"User: {query3}")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatClientAgent, ChatClientAgentThread
from agent_framework import AgentThread, ChatClientAgent
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
@@ -56,28 +56,29 @@ async def example_with_thread_persistence_in_memory() -> None:
# 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.")
print(f"Thread contains {len(await thread.list_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.")
print(f"Thread contains {len(await thread.list_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(f"Thread contains {len(await thread.list_messages() or [])} messages in-memory.")
print("Note: The agent remembers context from previous messages in the same thread.\n")
@@ -99,17 +100,16 @@ async def example_with_existing_thread_id() -> None:
# 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.")
print(f"Thread contains {len(await thread.list_messages() or [])} messages in-memory.")
# The thread ID is set after the first response
existing_thread_id = thread.id
existing_thread_id = thread.service_thread_id
print(f"Thread ID: {existing_thread_id}")
if existing_thread_id:
@@ -122,13 +122,13 @@ async def example_with_existing_thread_id() -> None:
)
# Create a thread with the existing ID
thread = ChatClientAgentThread(id=existing_thread_id)
thread = AgentThread(service_thread_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(f"Thread contains {len(await thread.list_messages() or [])} messages in-memory.")
print("Note: The agent continues the conversation from the previous thread by using thread ID.\n")