mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] cleanup of thread API and serialization (#893)
* cleanup of threads and serialization * fix for sliding window * fix redis test * updated from comments * updated context provider and threads * updated lock * add asyncio default * fix redis tests * fix tests * fix tests * renamed to invoking * fixed tests * fix for instructions
This commit is contained in:
committed by
GitHub
Unverified
parent
bf5931932e
commit
10d10364a9
@@ -63,7 +63,7 @@ The provider supports both full‑text only and hybrid vector search:
|
||||
|
||||
`redis_basics.py` walks through three scenarios:
|
||||
|
||||
1. Standalone provider usage: adds messages and retrieves context via `model_invoking`.
|
||||
1. Standalone provider usage: adds messages and retrieves context via `invoking`.
|
||||
2. Agent integration: teaches the agent a preference and verifies it is remembered across turns.
|
||||
3. Agent + tool: calls a sample tool (flight search) and then asks the agent to recall details remembered from the tool output.
|
||||
|
||||
@@ -108,5 +108,3 @@ You should see the agent responses and, when using embeddings, context retrieved
|
||||
- Ensure at least one of `application_id`, `agent_id`, `user_id`, or `thread_id` is set; the provider requires a scope.
|
||||
- If using embeddings, verify `OPENAI_API_KEY` is set and reachable.
|
||||
- Make sure Redis exposes RediSearch (Redis Stack image or managed service with search enabled).
|
||||
|
||||
|
||||
|
||||
@@ -27,21 +27,17 @@ Run:
|
||||
python redis_basics.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ChatMessage, Role
|
||||
from agent_framework_redis._provider import RedisProvider
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
from agent_framework_redis._provider import RedisProvider
|
||||
from redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
|
||||
|
||||
def search_flights(
|
||||
origin_airport_code: str,
|
||||
destination_airport_code: str,
|
||||
detailed: bool = False
|
||||
) -> str:
|
||||
def search_flights(origin_airport_code: str, destination_airport_code: str, detailed: bool = False) -> str:
|
||||
"""Simulated flight-search tool to demonstrate tool memory.
|
||||
|
||||
The agent can call this function, and the returned details can be stored
|
||||
@@ -50,9 +46,27 @@ def search_flights(
|
||||
"""
|
||||
# Minimal static catalog used to simulate a tool's structured output
|
||||
flights = {
|
||||
("JFK", "LAX"): {"airline": "SkyJet", "duration": "6h 15m", "price": 325, "cabin": "Economy", "baggage": "1 checked bag"},
|
||||
("SFO", "SEA"): {"airline": "Pacific Air", "duration": "2h 5m", "price": 129, "cabin": "Economy", "baggage": "Carry-on only"},
|
||||
("LHR", "DXB"): {"airline": "EuroWings", "duration": "6h 50m", "price": 499, "cabin": "Business", "baggage": "2 bags included"},
|
||||
("JFK", "LAX"): {
|
||||
"airline": "SkyJet",
|
||||
"duration": "6h 15m",
|
||||
"price": 325,
|
||||
"cabin": "Economy",
|
||||
"baggage": "1 checked bag",
|
||||
},
|
||||
("SFO", "SEA"): {
|
||||
"airline": "Pacific Air",
|
||||
"duration": "2h 5m",
|
||||
"price": 129,
|
||||
"cabin": "Economy",
|
||||
"baggage": "Carry-on only",
|
||||
},
|
||||
("LHR", "DXB"): {
|
||||
"airline": "EuroWings",
|
||||
"duration": "6h 50m",
|
||||
"price": 499,
|
||||
"cabin": "Business",
|
||||
"baggage": "2 bags included",
|
||||
},
|
||||
}
|
||||
|
||||
route = (origin_airport_code.upper(), destination_airport_code.upper())
|
||||
@@ -97,7 +111,7 @@ async def main() -> None:
|
||||
)
|
||||
# The provider manages persistence and retrieval. application_id/agent_id/user_id
|
||||
# scope data for multi-tenant separation; thread_id (set later) narrows to a
|
||||
# specific conversation.
|
||||
# specific conversation.
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_basics",
|
||||
@@ -109,7 +123,7 @@ async def main() -> None:
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
)
|
||||
|
||||
|
||||
# Build sample chat messages to persist to Redis
|
||||
messages = [
|
||||
ChatMessage(role=Role.USER, text="runA CONVO: User Message"),
|
||||
@@ -121,14 +135,12 @@ async def main() -> None:
|
||||
# Threads are logical boundaries used by the provider to group and retrieve
|
||||
# conversation-specific context.
|
||||
await provider.thread_created(thread_id="runA")
|
||||
await provider.messages_adding(thread_id="runA", new_messages=messages)
|
||||
await provider.invoked(request_messages=messages)
|
||||
|
||||
# Retrieve relevant memories for a hypothetical model call. The provider uses
|
||||
# the current request messages as the retrieval query and returns context to
|
||||
# be injected into the model's instructions.
|
||||
ctx = await provider.model_invoking([
|
||||
ChatMessage(role=Role.SYSTEM, text="B: Assistant Message")
|
||||
])
|
||||
ctx = await provider.invoking([ChatMessage(role=Role.SYSTEM, text="B: Assistant Message")])
|
||||
|
||||
# Inspect retrieved memories that would be injected into instructions
|
||||
# (Debug-only output so you can verify retrieval works as expected.)
|
||||
@@ -167,13 +179,14 @@ async def main() -> None:
|
||||
# Create agent wired to the Redis context provider. The provider automatically
|
||||
# persists conversational details and surfaces relevant context on each turn.
|
||||
agent = client.create_agent(
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=provider)
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=provider,
|
||||
)
|
||||
|
||||
# Teach a user preference; the agent writes this to the provider's memory
|
||||
query = "Remember that I enjoy glugenflorgle"
|
||||
@@ -201,20 +214,21 @@ async def main() -> None:
|
||||
prefix="context_3",
|
||||
application_id="matrix_of_kermits",
|
||||
agent_id="agent_kermit",
|
||||
user_id="kermit"
|
||||
user_id="kermit",
|
||||
)
|
||||
|
||||
# Create agent exposing the flight search tool. Tool outputs are captured by the
|
||||
# provider and become retrievable context for later turns.
|
||||
client = OpenAIChatClient(ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
|
||||
agent = client.create_agent(
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=search_flights,
|
||||
context_providers=provider)
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=search_flights,
|
||||
context_providers=provider,
|
||||
)
|
||||
# Invoke the tool; outputs become part of memory/context
|
||||
query = "Are there any flights from new york city (jfk) to la? Give me details"
|
||||
result = await agent.run(query)
|
||||
@@ -229,5 +243,6 @@ async def main() -> None:
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
"""Redis Context Provider: Basic usage and agent integration
|
||||
|
||||
This example demonstrates how to use the Redis ChatMessageStore to persist
|
||||
This example demonstrates how to use the Redis ChatMessageStoreProtocol to persist
|
||||
conversational details. Pass it as a constructor argument to create_agent.
|
||||
|
||||
Requirements:
|
||||
@@ -14,15 +14,14 @@ Run:
|
||||
python redis_conversation.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework_redis._provider import RedisProvider
|
||||
from agent_framework_redis._chat_message_store import RedisChatMessageStore
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
from agent_framework_redis._chat_message_store import RedisChatMessageStore
|
||||
from agent_framework_redis._provider import RedisProvider
|
||||
from redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -65,15 +64,15 @@ async def main() -> None:
|
||||
# Create agent wired to the Redis context provider. The provider automatically
|
||||
# persists conversational details and surfaces relevant context on each turn.
|
||||
agent = client.create_agent(
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=provider,
|
||||
chat_message_store_factory=chat_message_store_factory,
|
||||
)
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=provider,
|
||||
chat_message_store_factory=chat_message_store_factory,
|
||||
)
|
||||
|
||||
# Teach a user preference; the agent writes this to the provider's memory
|
||||
query = "Remember that I enjoy gumbo"
|
||||
@@ -109,5 +108,6 @@ async def main() -> None:
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import MutableSequence, Sequence
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, ChatOptions, Context, ContextProvider
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
name: str | None = None
|
||||
age: int | None = None
|
||||
|
||||
|
||||
class UserInfoMemory(ContextProvider):
|
||||
def __init__(self, chat_client: ChatClientProtocol, user_info: UserInfo | None = None, **kwargs: Any):
|
||||
"""Create the memory.
|
||||
|
||||
If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
|
||||
"""
|
||||
|
||||
self._chat_client = chat_client
|
||||
if user_info:
|
||||
self.user_info = user_info
|
||||
elif kwargs:
|
||||
self.user_info = UserInfo.model_validate(kwargs)
|
||||
else:
|
||||
self.user_info = UserInfo()
|
||||
|
||||
async def invoked(
|
||||
self,
|
||||
request_messages: ChatMessage | Sequence[ChatMessage],
|
||||
response_messages: ChatMessage | Sequence[ChatMessage] | None = None,
|
||||
invoke_exception: Exception | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Extract user information from messages after each agent call."""
|
||||
# Check if we need to extract user info from user messages
|
||||
user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role.value == "user"] # type: ignore
|
||||
|
||||
if (self.user_info.name is None or self.user_info.age is None) and user_messages:
|
||||
try:
|
||||
# Use the chat client to extract structured information
|
||||
result = await self._chat_client.get_response(
|
||||
messages=request_messages, # type: ignore
|
||||
chat_options=ChatOptions(
|
||||
instructions="Extract the user's name and age from the message if present. If not present return nulls.",
|
||||
response_format=UserInfo,
|
||||
),
|
||||
)
|
||||
|
||||
# Update user info with extracted data
|
||||
if result.value:
|
||||
if self.user_info.name is None and result.value.name:
|
||||
self.user_info.name = result.value.name
|
||||
if self.user_info.age is None and result.value.age:
|
||||
self.user_info.age = result.value.age
|
||||
|
||||
except Exception:
|
||||
pass # Failed to extract, continue without updating
|
||||
|
||||
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
|
||||
"""Provide user information context before each agent call."""
|
||||
instructions: list[str] = []
|
||||
|
||||
if self.user_info.name is None:
|
||||
instructions.append(
|
||||
"Ask the user for their name and politely decline to answer any questions until they provide it."
|
||||
)
|
||||
else:
|
||||
instructions.append(f"The user's name is {self.user_info.name}.")
|
||||
|
||||
if self.user_info.age is None:
|
||||
instructions.append(
|
||||
"Ask the user for their age and politely decline to answer any questions until they provide it."
|
||||
)
|
||||
else:
|
||||
instructions.append(f"The user's age is {self.user_info.age}.")
|
||||
|
||||
# Return context with additional instructions
|
||||
return Context(instructions=" ".join(instructions))
|
||||
|
||||
def serialize(self) -> str:
|
||||
"""Serialize the user info for thread persistence."""
|
||||
return self.user_info.model_dump_json()
|
||||
|
||||
|
||||
async def main():
|
||||
async with AzureCliCredential() as credential:
|
||||
chat_client = AzureAIAgentClient(async_credential=credential)
|
||||
|
||||
# Create the memory provider
|
||||
memory_provider = UserInfoMemory(chat_client)
|
||||
|
||||
# Create the agent with memory
|
||||
async with ChatAgent(
|
||||
chat_client=chat_client,
|
||||
instructions="You are a friendly assistant. Always address the user by their name.",
|
||||
context_providers=memory_provider,
|
||||
) as agent:
|
||||
# Create a new thread for the conversation
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
print(await agent.run("Hello, what is the square root of 9?", thread=thread))
|
||||
print(await agent.run("My name is Ruaidhrí", thread=thread))
|
||||
print(await agent.run("I am 20 years old", thread=thread))
|
||||
|
||||
# Access the memory component via the thread's get_service method and inspect the memories
|
||||
user_info_memory = thread.context_provider.providers[0] # type: ignore
|
||||
if user_info_memory:
|
||||
print()
|
||||
print(f"MEMORY - User Name: {user_info_memory.user_info.name}") # type: ignore
|
||||
print(f"MEMORY - User Age: {user_info_memory.user_info.age}") # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user