mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: add RedisContextProvider (#716)
* Setting up * Readme * Add redis tests path to all-tests * First pass integration * Keep provider convention * First pass integration * add redis integration tests * update README.md * Add basic sample for redis integration * Add partitioning, add partition-aware tests, improve sample script * Fix code quality check * Try to resolve pytest check * Try to identify if pytest is the cause of failed checks * Re-enable tests * Rename redis test file * Removing some tests to narrow down issue * Revert, no difference * Delete temp files * Starting refactor of RedisProvider * Build dynamic schema builder, still need to do dynamic embedding model config * Add scope control * Complete first pass functionality with OpenAI + HF vectors -> Tests, Samples, Demo to follow * Fix code quality * attempt to identify rootcause of failed test * attempt to identify rootcause of failed test * Attempt to resolve code quality fail * Update pyproject.toml for foundry to pin azure-ai-projects == 1.1.0b3,azure-ai-agents == 1.2.0b3 * Add tests for redisprovider * Remove invalid tests * Add API key handling for openai vectorizer * Update uv.locl * Use master uv.lock * Begin sample file, add lazy index creation, fix faulty override * Index drop and reinit depends on drop_redis_index not overwrite * Add samples, threading included, escaped queries, verify threading works, sample README.md * Refactor filters * Opinionated vars * Allow filter expression combination * Try inline stubs for mypy * Address mypy errors * Better docstrings, tweaks for feedback * Tweak example 3 in redis_threads.py sample * adjust confusing name * Enrich docstrings * Add descriptions and comments to samples, externalize vectorizer choice, remove nltk and sentencetransformers dependnecy * Add descriptions and comments to samples, externalize vectorizer choice, remove nltk and sentencetransformers dependnecy * Incorporate initial feedback from dmytrostruk * Fix uv.lock * Attempt to resolve conflict * Use remote .tomls * Sanity check * fix tests * Remove hardcoded API key from samples * Fix incorrect env var * Make add and redis_search private * Fix tests relying on private funcs * Expand tests * Explainer comments to each test * Add a 'get_conversation_history' function to RedisProvider - This just returns messages in sequential order. Added 'created_at_*' timestamps to facilitate sequential recovery. function has to be manually invoked by user * Add agent-framework-redis to python/pyproject.toml * Remove get_conversation_history * improve redis context provider with pydantic techniques and safe index handling patterns * add RedisChatMessageStore * remove integration test :( * fix mypy error * Remove unused params * Redo schema validation to be order-invariant, handle attrs (previously throwing errors due to strict ==) * Expand explanation * Add ChatMessageStore example * Fix comments in redis_conversation.py * Resolving uv.lock conflict, update to match main * Fix test in redis provider * Apply suggestion from @ekzhu * Update python/packages/main/pyproject.toml --------- Co-authored-by: Tyler Hutcherson <tyler.hutcherson@redis.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Redis Context Provider Examples
|
||||
|
||||
The Redis context provider enables persistent, searchable memory for your agents using Redis (RediSearch). It supports full‑text search and optional hybrid search with vector embeddings, letting agents remember and retrieve user context across sessions and threads.
|
||||
|
||||
This folder contains an example demonstrating how to use the Redis context provider with the Agent Framework.
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`redis_basics.py`](redis_basics.py) | Shows standalone provider usage and agent integration. Demonstrates writing messages to Redis, retrieving context via full‑text or hybrid vector search, and persisting preferences across threads. Also includes a simple tool example whose outputs are remembered. |
|
||||
| [`redis_threads.py`](redis_threads.py) | Demonstrates thread scoping. Includes: (1) global thread scope with a fixed `thread_id` shared across operations; (2) per‑operation thread scope where `scope_to_per_operation_thread_id=True` binds memory to a single thread for the provider’s lifetime; and (3) multiple agents with isolated memory via different `agent_id` values. |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required resources
|
||||
|
||||
1. A running Redis with RediSearch (Redis Stack or a managed service)
|
||||
2. Python environment with Agent Framework Redis extra installed
|
||||
3. Optional: OpenAI API key if using vector embeddings
|
||||
|
||||
### Install the package
|
||||
|
||||
```bash
|
||||
pip install "agent-framework[redis]"
|
||||
```
|
||||
|
||||
## Running Redis
|
||||
|
||||
Pick one option:
|
||||
|
||||
### Option A: Docker (local Redis Stack)
|
||||
|
||||
```bash
|
||||
docker run --name redis -p 6379:6379 -d redis:8.0.3
|
||||
```
|
||||
|
||||
### Option B: Redis Cloud
|
||||
|
||||
Create a free database and get the connection URL at `https://redis.io/cloud/`.
|
||||
|
||||
### Option C: Azure Managed Redis
|
||||
|
||||
See quickstart: `https://learn.microsoft.com/azure/redis/quickstart-create-managed-redis`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment variables
|
||||
|
||||
- `OPENAI_API_KEY` (optional): Required only if you set `vectorizer_choice="openai"` to enable hybrid search.
|
||||
|
||||
### Provider configuration highlights
|
||||
|
||||
The provider supports both full‑text only and hybrid vector search:
|
||||
|
||||
- Set `vectorizer_choice` to `"openai"` or `"hf"` to enable embeddings and hybrid search.
|
||||
- When using a vectorizer, also set `vector_field_name` (e.g., `"vector"`).
|
||||
- Partition fields for scoping memory: `application_id`, `agent_id`, `user_id`, `thread_id`.
|
||||
- Thread scoping: `scope_to_per_operation_thread_id=True` isolates memory per operation thread.
|
||||
- Index management: `index_name`, `overwrite_redis_index`, `drop_redis_index`.
|
||||
|
||||
## What the example does
|
||||
|
||||
`redis_basics.py` walks through three scenarios:
|
||||
|
||||
1. Standalone provider usage: adds messages and retrieves context via `model_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.
|
||||
|
||||
It uses OpenAI for both chat (via `OpenAIChatClient`) and, in some steps, optional embeddings for hybrid search.
|
||||
|
||||
## How to run
|
||||
|
||||
1) Start Redis (see options above). For local default, ensure it's reachable at `redis://localhost:6379`.
|
||||
|
||||
2) Set your OpenAI key if using embeddings and for the chat client used in the sample:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="<your key>"
|
||||
```
|
||||
|
||||
3) Run the example:
|
||||
|
||||
```bash
|
||||
python redis_basics.py
|
||||
```
|
||||
|
||||
You should see the agent responses and, when using embeddings, context retrieved from Redis. The example includes commented debug helpers you can print, such as index info or all stored docs.
|
||||
|
||||
## Key concepts
|
||||
|
||||
### Memory scoping
|
||||
|
||||
- Global scope: set `application_id`, `agent_id`, `user_id`, or `thread_id` on the provider to filter memory.
|
||||
- Per‑operation thread scope: set `scope_to_per_operation_thread_id=True` to isolate memory to the current thread created by the framework.
|
||||
|
||||
### Hybrid vector search (optional)
|
||||
|
||||
- Enable by setting `vectorizer_choice` to `"openai"` (requires `OPENAI_API_KEY`) or `"hf"` (offline model).
|
||||
- Provide `vector_field_name` (e.g., `"vector"`); other vector settings have sensible defaults.
|
||||
|
||||
### Index lifecycle controls
|
||||
|
||||
- `overwrite_redis_index` and `drop_redis_index` help recreate indexes during iteration.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- 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).
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Redis Context Provider: Basic usage and agent integration
|
||||
|
||||
This example demonstrates how to use the Redis context provider to persist and
|
||||
retrieve conversational memory for agents. It covers three progressively more
|
||||
realistic scenarios:
|
||||
|
||||
1) Standalone provider usage ("basic cache")
|
||||
- Write messages to Redis and retrieve relevant context using full-text or
|
||||
hybrid vector search.
|
||||
|
||||
2) Agent + provider
|
||||
- Connect the provider to an agent so the agent can store user preferences
|
||||
and recall them across turns.
|
||||
|
||||
3) Agent + provider + tool memory
|
||||
- Expose a simple tool to the agent, then verify that details from the tool
|
||||
outputs are captured and retrievable as part of the agent's memory.
|
||||
|
||||
Requirements:
|
||||
- A Redis instance with RediSearch enabled (e.g., Redis Stack)
|
||||
- agent-framework with the Redis extra installed: pip install "agent-framework[redis]"
|
||||
- Optionally an OpenAI API key if enabling embeddings for hybrid search
|
||||
|
||||
Run:
|
||||
python redis_basics.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
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 redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
|
||||
|
||||
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
|
||||
by the Redis context provider. We later ask the agent to recall facts from
|
||||
these tool results to verify memory is working as expected.
|
||||
"""
|
||||
# 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"},
|
||||
}
|
||||
|
||||
route = (origin_airport_code.upper(), destination_airport_code.upper())
|
||||
if route not in flights:
|
||||
return f"No flights found between {origin_airport_code} and {destination_airport_code}"
|
||||
|
||||
flight = flights[route]
|
||||
if not detailed:
|
||||
return f"Flights available from {origin_airport_code} to {destination_airport_code}."
|
||||
|
||||
return (
|
||||
f"{flight['airline']} operates flights from {origin_airport_code} to {destination_airport_code}. "
|
||||
f"Duration: {flight['duration']}. "
|
||||
f"Price: ${flight['price']}. "
|
||||
f"Cabin: {flight['cabin']}. "
|
||||
f"Baggage policy: {flight['baggage']}."
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Walk through provider-only, agent integration, and tool-memory scenarios.
|
||||
|
||||
Helpful debugging (uncomment when iterating):
|
||||
- print(await provider.redis_index.info())
|
||||
- print(await provider.search_all())
|
||||
"""
|
||||
|
||||
print("1. Standalone provider usage:")
|
||||
print("-" * 40)
|
||||
# Create a provider with partition scope and OpenAI embeddings
|
||||
|
||||
# Please set the OPENAI_API_KEY and OPENAI_CHAT_MODEL_ID environment variables to use the OpenAI vectorizer
|
||||
# Recommend default for OPENAI_CHAT_MODEL_ID is gpt-4o-mini
|
||||
|
||||
# We attach an embedding vectorizer so the provider can perform hybrid (text + vector)
|
||||
# retrieval. If you prefer text-only retrieval, instantiate RedisProvider without the
|
||||
# 'vectorizer' and vector_* parameters.
|
||||
vectorizer = OpenAITextVectorizer(
|
||||
model="text-embedding-ada-002",
|
||||
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
|
||||
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url="redis://localhost:6379"),
|
||||
)
|
||||
# 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.
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_basics",
|
||||
application_id="matrix_of_kermits",
|
||||
agent_id="agent_kermit",
|
||||
user_id="kermit",
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
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"),
|
||||
ChatMessage(role=Role.ASSISTANT, text="runA CONVO: Assistant Message"),
|
||||
ChatMessage(role=Role.SYSTEM, text="runA CONVO: System Message"),
|
||||
]
|
||||
|
||||
# Declare/start a conversation/thread and write messages under 'runA'.
|
||||
# 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)
|
||||
|
||||
# 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")
|
||||
])
|
||||
|
||||
# Inspect retrieved memories that would be injected into instructions
|
||||
# (Debug-only output so you can verify retrieval works as expected.)
|
||||
print("Model Invoking Result:")
|
||||
print(ctx)
|
||||
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
# --- Agent + provider: teach and recall a preference ---
|
||||
|
||||
print("\n2. Agent + provider: teach and recall a preference")
|
||||
print("-" * 40)
|
||||
# Fresh provider for the agent demo (recreates index)
|
||||
vectorizer = OpenAITextVectorizer(
|
||||
model="text-embedding-ada-002",
|
||||
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
|
||||
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url="redis://localhost:6379"),
|
||||
)
|
||||
# Recreate a clean index so the next scenario starts fresh
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_basics_2",
|
||||
prefix="context_2",
|
||||
application_id="matrix_of_kermits",
|
||||
agent_id="agent_kermit",
|
||||
user_id="kermit",
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
)
|
||||
|
||||
# Create chat client for the agent
|
||||
client = OpenAIChatClient(ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
|
||||
# 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)
|
||||
|
||||
# Teach a user preference; the agent writes this to the provider's memory
|
||||
query = "Remember that I enjoy glugenflorgle"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Ask the agent to recall the stored preference; it should retrieve from memory
|
||||
query = "What do I enjoy?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
# --- Agent + provider + tool: store and recall tool-derived context ---
|
||||
|
||||
print("\n3. Agent + provider + tool: store and recall tool-derived context")
|
||||
print("-" * 40)
|
||||
# Text-only provider (full-text search only). Omits vectorizer and related params.
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_basics_3",
|
||||
prefix="context_3",
|
||||
application_id="matrix_of_kermits",
|
||||
agent_id="agent_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)
|
||||
# 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)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
# Verify the agent can recall tool-derived context
|
||||
query = "Which flight did I ask about?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Redis Context Provider: Basic usage and agent integration
|
||||
|
||||
This example demonstrates how to use the Redis ChatMessageStore to persist
|
||||
conversational details. Pass it as a constructor argument to create_agent.
|
||||
|
||||
Requirements:
|
||||
- A Redis instance with RediSearch enabled (e.g., Redis Stack)
|
||||
- agent-framework with the Redis extra installed: pip install "agent-framework[redis]"
|
||||
- Optionally an OpenAI API key if enabling embeddings for hybrid search
|
||||
|
||||
Run:
|
||||
python redis_conversation.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
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 redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Walk through provider and chat message store usage.
|
||||
|
||||
Helpful debugging (uncomment when iterating):
|
||||
- print(await provider.redis_index.info())
|
||||
- print(await provider.search_all())
|
||||
"""
|
||||
vectorizer = OpenAITextVectorizer(
|
||||
model="text-embedding-ada-002",
|
||||
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
|
||||
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url="redis://localhost:6379"),
|
||||
)
|
||||
|
||||
thread_id = "test_thread"
|
||||
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_conversation",
|
||||
prefix="redis_conversation",
|
||||
application_id="matrix_of_kermits",
|
||||
agent_id="agent_kermit",
|
||||
user_id="kermit",
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
thread_id=thread_id,
|
||||
)
|
||||
chat_message_store_factory = lambda: RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
thread_id=thread_id,
|
||||
key_prefix="chat_messages",
|
||||
max_messages=100,
|
||||
)
|
||||
|
||||
# Create chat client for the agent
|
||||
client = OpenAIChatClient(ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
|
||||
# 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,
|
||||
)
|
||||
|
||||
# Teach a user preference; the agent writes this to the provider's memory
|
||||
query = "Remember that I enjoy gumbo"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Ask the agent to recall the stored preference; it should retrieve from memory
|
||||
query = "What do I enjoy?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "What did I say to you just now?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "Remember that anyone who does not clean shrimp will be eaten by a shark"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "Tulips are red"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "What was the first thing I said to you this conversation?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,250 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Redis Context Provider: Thread scoping examples
|
||||
|
||||
This sample demonstrates how conversational memory can be scoped when using the
|
||||
Redis context provider. It covers three scenarios:
|
||||
|
||||
1) Global thread scope
|
||||
- Provide a fixed thread_id to share memories across operations/threads.
|
||||
|
||||
2) Per-operation thread scope
|
||||
- Enable scope_to_per_operation_thread_id to bind the provider to a single
|
||||
thread for the lifetime of that provider instance. Use the same thread
|
||||
object for reads/writes with that provider.
|
||||
|
||||
3) Multiple agents with isolated memory
|
||||
- Use different agent_id values to keep memories separated for different
|
||||
agent personas, even when the user_id is the same.
|
||||
|
||||
Requirements:
|
||||
- A Redis instance with RediSearch enabled (e.g., Redis Stack)
|
||||
- agent-framework with the Redis extra installed: pip install "agent-framework[redis]"
|
||||
- Optionally an OpenAI API key for the chat client in this demo
|
||||
|
||||
Run:
|
||||
python redis_threads.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from agent_framework_redis._provider import RedisProvider
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
from redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
|
||||
|
||||
# Please set the OPENAI_API_KEY and OPENAI_CHAT_MODEL_ID environment variables to use the OpenAI vectorizer
|
||||
# Recommend default for OPENAI_CHAT_MODEL_ID is gpt-4o-mini
|
||||
|
||||
async def example_global_thread_scope() -> None:
|
||||
"""Example 1: Global thread_id scope (memories shared across all operations)."""
|
||||
print("1. Global Thread Scope Example:")
|
||||
print("-" * 40)
|
||||
|
||||
global_thread_id = str(uuid.uuid4())
|
||||
|
||||
client = OpenAIChatClient(
|
||||
ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
)
|
||||
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_threads_global",
|
||||
# overwrite_redis_index=True,
|
||||
# drop_redis_index=True,
|
||||
application_id="threads_demo_app",
|
||||
agent_id="threads_demo_agent",
|
||||
user_id="threads_demo_user",
|
||||
thread_id=global_thread_id,
|
||||
scope_to_per_operation_thread_id=False, # Share memories across all threads
|
||||
)
|
||||
|
||||
agent = client.create_agent(
|
||||
name="GlobalMemoryAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context containing information"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=provider)
|
||||
|
||||
# Store a preference in the global scope
|
||||
query = "Remember that I prefer technical responses with code examples when discussing programming."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Create a new thread - memories should still be accessible due to global scope
|
||||
new_thread = agent.get_new_thread()
|
||||
query = "What technical responses do I prefer?"
|
||||
print(f"User (new thread): {query}")
|
||||
result = await agent.run(query, thread=new_thread)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Clean up the Redis index
|
||||
await provider.redis_index.delete()
|
||||
|
||||
|
||||
async def example_per_operation_thread_scope() -> None:
|
||||
"""Example 2: Per-operation thread scope (memories isolated per thread).
|
||||
|
||||
Note: When scope_to_per_operation_thread_id=True, the provider is bound to a single thread
|
||||
throughout its lifetime. Use the same thread object for all operations with that provider.
|
||||
"""
|
||||
print("2. Per-Operation Thread Scope Example:")
|
||||
print("-" * 40)
|
||||
|
||||
client = OpenAIChatClient(
|
||||
ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
)
|
||||
|
||||
vectorizer = OpenAITextVectorizer(
|
||||
model="text-embedding-ada-002",
|
||||
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
|
||||
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url="redis://localhost:6379"),
|
||||
)
|
||||
|
||||
provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_threads_dynamic",
|
||||
# overwrite_redis_index=True,
|
||||
# drop_redis_index=True,
|
||||
application_id="threads_demo_app",
|
||||
agent_id="threads_demo_agent",
|
||||
user_id="threads_demo_user",
|
||||
scope_to_per_operation_thread_id=True, # Isolate memories per thread
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
)
|
||||
|
||||
agent = client.create_agent(
|
||||
name="ScopedMemoryAssistant",
|
||||
instructions="You are an assistant with thread-scoped memory.",
|
||||
context_providers=provider,
|
||||
)
|
||||
|
||||
# Create a specific thread for this scoped provider
|
||||
dedicated_thread = agent.get_new_thread()
|
||||
|
||||
# Store some information in the dedicated thread
|
||||
query = "Remember that for this conversation, I'm working on a Python project about data analysis."
|
||||
print(f"User (dedicated thread): {query}")
|
||||
result = await agent.run(query, thread=dedicated_thread)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Test memory retrieval in the same dedicated thread
|
||||
query = "What project am I working on?"
|
||||
print(f"User (same dedicated thread): {query}")
|
||||
result = await agent.run(query, thread=dedicated_thread)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Store more information in the same thread
|
||||
query = "Also remember that I prefer using pandas and matplotlib for this project."
|
||||
print(f"User (same dedicated thread): {query}")
|
||||
result = await agent.run(query, thread=dedicated_thread)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Test comprehensive memory retrieval
|
||||
query = "What do you know about my current project and preferences?"
|
||||
print(f"User (same dedicated thread): {query}")
|
||||
result = await agent.run(query, thread=dedicated_thread)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Clean up the Redis index
|
||||
await provider.redis_index.delete()
|
||||
|
||||
|
||||
async def example_multiple_agents() -> None:
|
||||
"""Example 3: Multiple agents with different thread configurations (isolated via agent_id) but within 1 index."""
|
||||
print("3. Multiple Agents with Different Thread Configurations:")
|
||||
print("-" * 40)
|
||||
|
||||
client = OpenAIChatClient(
|
||||
ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
)
|
||||
|
||||
vectorizer = OpenAITextVectorizer(
|
||||
model="text-embedding-ada-002",
|
||||
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
|
||||
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url="redis://localhost:6379"),
|
||||
)
|
||||
|
||||
personal_provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_threads_agents",
|
||||
application_id="threads_demo_app",
|
||||
agent_id="agent_personal",
|
||||
user_id="threads_demo_user",
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
)
|
||||
|
||||
personal_agent = client.create_agent(
|
||||
name="PersonalAssistant",
|
||||
instructions="You are a personal assistant that helps with personal tasks.",
|
||||
context_providers=personal_provider,
|
||||
)
|
||||
|
||||
work_provider = RedisProvider(
|
||||
redis_url="redis://localhost:6379",
|
||||
index_name="redis_threads_agents",
|
||||
application_id="threads_demo_app",
|
||||
agent_id="agent_work",
|
||||
user_id="threads_demo_user",
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
)
|
||||
|
||||
work_agent = client.create_agent(
|
||||
name="WorkAssistant",
|
||||
instructions="You are a work assistant that helps with professional tasks.",
|
||||
context_providers=work_provider,
|
||||
)
|
||||
|
||||
# Store personal information
|
||||
query = "Remember that I like to exercise at 6 AM and prefer outdoor activities."
|
||||
print(f"User to Personal Agent: {query}")
|
||||
result = await personal_agent.run(query)
|
||||
print(f"Personal Agent: {result}\n")
|
||||
|
||||
# Store work information
|
||||
query = "Remember that I have team meetings every Tuesday at 2 PM."
|
||||
print(f"User to Work Agent: {query}")
|
||||
result = await work_agent.run(query)
|
||||
print(f"Work Agent: {result}\n")
|
||||
|
||||
# Test memory isolation
|
||||
query = "What do you know about my schedule?"
|
||||
print(f"User to Personal Agent: {query}")
|
||||
result = await personal_agent.run(query)
|
||||
print(f"Personal Agent: {result}\n")
|
||||
|
||||
print(f"User to Work Agent: {query}")
|
||||
result = await work_agent.run(query)
|
||||
print(f"Work Agent: {result}\n")
|
||||
|
||||
# Clean up the Redis index (shared)
|
||||
await work_provider.redis_index.delete()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Redis Thread Scoping Examples ===\n")
|
||||
await example_global_thread_scope()
|
||||
await example_per_operation_thread_scope()
|
||||
await example_multiple_agents()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -8,3 +8,4 @@ This folder contains examples demonstrating different ways to manage conversatio
|
||||
|------|-------------|
|
||||
| [`custom_chat_message_store_thread.py`](custom_chat_message_store_thread.py) | Demonstrates how to implement a custom `ChatMessageStore` for persisting conversation history. Shows how to create a custom store with serialization/deserialization capabilities and integrate it with agents for thread management across multiple sessions. |
|
||||
| [`suspend_resume_thread.py`](suspend_resume_thread.py) | Shows how to suspend and resume conversation threads, allowing you to save the state of a conversation and continue it later. This is useful for long-running conversations or when you need to persist conversation state across application restarts. |
|
||||
| [`redis_chat_message_store_thread.py`](redis_chat_message_store_thread.py) | Comprehensive examples of using the Redis-backed `RedisChatMessageStore` for persistent conversation storage. Covers basic usage, user session management, conversation persistence across app restarts, thread serialization, and automatic message trimming. Requires Redis server and demonstrates production-ready patterns for scalable chat applications. |
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import AgentThread
|
||||
from agent_framework._threads import deserialize_thread_state
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.redis import RedisChatMessageStore
|
||||
|
||||
|
||||
async def example_basic_redis_store() -> None:
|
||||
"""Basic example of using Redis chat message store."""
|
||||
print("=== Basic Redis Chat Message Store Example ===")
|
||||
|
||||
# Create Redis store with auto-generated thread ID
|
||||
redis_store = RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
# thread_id will be auto-generated if not provided
|
||||
)
|
||||
|
||||
print(f"Created store with thread ID: {redis_store.thread_id}")
|
||||
|
||||
# Create thread with Redis store
|
||||
thread = AgentThread(message_store=redis_store)
|
||||
|
||||
# Create agent
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
name="RedisBot",
|
||||
instructions="You are a helpful assistant that remembers our conversation using Redis.",
|
||||
)
|
||||
|
||||
# Have a conversation
|
||||
print("\n--- Starting conversation ---")
|
||||
query1 = "Hello! My name is Alice and I love pizza."
|
||||
print(f"User: {query1}")
|
||||
response1 = await agent.run(query1, thread=thread)
|
||||
print(f"Agent: {response1.text}")
|
||||
|
||||
query2 = "What do you remember about me?"
|
||||
print(f"User: {query2}")
|
||||
response2 = await agent.run(query2, thread=thread)
|
||||
print(f"Agent: {response2.text}")
|
||||
|
||||
# Show messages are stored in Redis
|
||||
messages = await redis_store.list_messages()
|
||||
print(f"\nTotal messages in Redis: {len(messages)}")
|
||||
|
||||
# Cleanup
|
||||
await redis_store.clear()
|
||||
await redis_store.aclose()
|
||||
print("Cleaned up Redis data\n")
|
||||
|
||||
|
||||
async def example_user_session_management() -> None:
|
||||
"""Example of managing user sessions with Redis."""
|
||||
print("=== User Session Management Example ===")
|
||||
|
||||
user_id = "alice_123"
|
||||
session_id = f"session_{uuid4()}"
|
||||
|
||||
# Create Redis store for specific user session
|
||||
def create_user_session_store():
|
||||
return RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
thread_id=f"user_{user_id}_{session_id}",
|
||||
max_messages=10 # Keep only last 10 messages
|
||||
)
|
||||
|
||||
# Create agent with factory pattern
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
name="SessionBot",
|
||||
instructions="You are a helpful assistant. Keep track of user preferences.",
|
||||
chat_message_store_factory=create_user_session_store,
|
||||
)
|
||||
|
||||
# Start conversation
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
print(f"Started session for user {user_id}")
|
||||
if hasattr(thread.message_store, 'thread_id'):
|
||||
print(f"Thread ID: {thread.message_store.thread_id}") # type: ignore[union-attr]
|
||||
|
||||
# Simulate conversation
|
||||
queries = [
|
||||
"Hi, I'm Alice and I prefer vegetarian food.",
|
||||
"What restaurants would you recommend?",
|
||||
"I also love Italian cuisine.",
|
||||
"Can you remember my food preferences?"
|
||||
]
|
||||
|
||||
for i, query in enumerate(queries, 1):
|
||||
print(f"\n--- Message {i} ---")
|
||||
print(f"User: {query}")
|
||||
response = await agent.run(query, thread=thread)
|
||||
print(f"Agent: {response.text}")
|
||||
|
||||
# Show persistent storage
|
||||
if thread.message_store:
|
||||
messages = await thread.message_store.list_messages() # type: ignore[union-attr]
|
||||
print(f"\nMessages stored for user {user_id}: {len(messages)}")
|
||||
|
||||
# Cleanup
|
||||
if thread.message_store:
|
||||
await thread.message_store.clear() # type: ignore[union-attr]
|
||||
await thread.message_store.aclose() # type: ignore[union-attr]
|
||||
print("Cleaned up session data\n")
|
||||
|
||||
|
||||
async def example_conversation_persistence() -> None:
|
||||
"""Example of conversation persistence across application restarts."""
|
||||
print("=== Conversation Persistence Example ===")
|
||||
|
||||
conversation_id = "persistent_chat_001"
|
||||
|
||||
# Phase 1: Start conversation
|
||||
print("--- Phase 1: Starting conversation ---")
|
||||
store1 = RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
thread_id=conversation_id,
|
||||
)
|
||||
|
||||
thread1 = AgentThread(message_store=store1)
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
name="PersistentBot",
|
||||
instructions="You are a helpful assistant. Remember our conversation history.",
|
||||
)
|
||||
|
||||
# Start conversation
|
||||
query1 = "Hello! I'm working on a Python project about machine learning."
|
||||
print(f"User: {query1}")
|
||||
response1 = await agent.run(query1, thread=thread1)
|
||||
print(f"Agent: {response1.text}")
|
||||
|
||||
query2 = "I'm specifically interested in neural networks."
|
||||
print(f"User: {query2}")
|
||||
response2 = await agent.run(query2, thread=thread1)
|
||||
print(f"Agent: {response2.text}")
|
||||
|
||||
print(f"Stored {len(await store1.list_messages())} messages in Redis")
|
||||
await store1.aclose()
|
||||
|
||||
# Phase 2: Resume conversation (simulating app restart)
|
||||
print("\n--- Phase 2: Resuming conversation (after 'restart') ---")
|
||||
store2 = RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
thread_id=conversation_id, # Same thread ID
|
||||
)
|
||||
|
||||
thread2 = AgentThread(message_store=store2)
|
||||
|
||||
# Continue conversation - agent should remember context
|
||||
query3 = "What was I working on before?"
|
||||
print(f"User: {query3}")
|
||||
response3 = await agent.run(query3, thread=thread2)
|
||||
print(f"Agent: {response3.text}")
|
||||
|
||||
query4 = "Can you suggest some Python libraries for neural networks?"
|
||||
print(f"User: {query4}")
|
||||
response4 = await agent.run(query4, thread=thread2)
|
||||
print(f"Agent: {response4.text}")
|
||||
|
||||
print(f"Total messages after resuming: {len(await store2.list_messages())}")
|
||||
|
||||
# Cleanup
|
||||
await store2.clear()
|
||||
await store2.aclose()
|
||||
print("Cleaned up persistent data\n")
|
||||
|
||||
|
||||
async def example_thread_serialization() -> None:
|
||||
"""Example of thread state serialization and deserialization."""
|
||||
print("=== Thread Serialization Example ===")
|
||||
|
||||
# Create initial thread with Redis store
|
||||
original_store = RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
thread_id="serialization_test",
|
||||
max_messages=50,
|
||||
)
|
||||
|
||||
original_thread = AgentThread(message_store=original_store)
|
||||
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
name="SerializationBot",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
# Have initial conversation
|
||||
print("--- Initial conversation ---")
|
||||
query1 = "Hello! I'm testing serialization."
|
||||
print(f"User: {query1}")
|
||||
response1 = await agent.run(query1, thread=original_thread)
|
||||
print(f"Agent: {response1.text}")
|
||||
|
||||
# Serialize thread state
|
||||
serialized_thread = await original_thread.serialize()
|
||||
print(f"\nSerialized thread state: {serialized_thread}")
|
||||
|
||||
# Close original connection
|
||||
await original_store.aclose()
|
||||
|
||||
# Deserialize thread state (simulating loading from database/file)
|
||||
print("\n--- Deserializing thread state ---")
|
||||
|
||||
# Create a new thread with the same Redis store type
|
||||
# This ensures the correct store type is used for deserialization
|
||||
restored_store = RedisChatMessageStore(redis_url="redis://localhost:6379")
|
||||
restored_thread = AgentThread(message_store=restored_store)
|
||||
|
||||
# Deserialize the thread state into the properly typed thread
|
||||
await deserialize_thread_state(restored_thread, serialized_thread)
|
||||
|
||||
# Continue conversation with restored thread
|
||||
query2 = "Do you remember what I said about testing?"
|
||||
print(f"User: {query2}")
|
||||
response2 = await agent.run(query2, thread=restored_thread)
|
||||
print(f"Agent: {response2.text}")
|
||||
|
||||
# Cleanup
|
||||
if restored_thread.message_store:
|
||||
await restored_thread.message_store.clear() # type: ignore[union-attr]
|
||||
await restored_thread.message_store.aclose() # type: ignore[union-attr]
|
||||
print("Cleaned up serialization test data\n")
|
||||
|
||||
|
||||
async def example_message_limits() -> None:
|
||||
"""Example of automatic message trimming with limits."""
|
||||
print("=== Message Limits Example ===")
|
||||
|
||||
# Create store with small message limit
|
||||
store = RedisChatMessageStore(
|
||||
redis_url="redis://localhost:6379",
|
||||
thread_id="limits_test",
|
||||
max_messages=3, # Keep only 3 most recent messages
|
||||
)
|
||||
|
||||
thread = AgentThread(message_store=store)
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
name="LimitBot",
|
||||
instructions="You are a helpful assistant with limited memory.",
|
||||
)
|
||||
|
||||
# Send multiple messages to test trimming
|
||||
messages = [
|
||||
"Message 1: Hello!",
|
||||
"Message 2: How are you?",
|
||||
"Message 3: What's the weather?",
|
||||
"Message 4: Tell me a joke.",
|
||||
"Message 5: This should trigger trimming.",
|
||||
]
|
||||
|
||||
for i, query in enumerate(messages, 1):
|
||||
print(f"\n--- Sending message {i} ---")
|
||||
print(f"User: {query}")
|
||||
response = await agent.run(query, thread=thread)
|
||||
print(f"Agent: {response.text}")
|
||||
|
||||
stored_messages = await store.list_messages()
|
||||
print(f"Messages in store: {len(stored_messages)}")
|
||||
if len(stored_messages) > 0:
|
||||
print(f"Oldest message: {stored_messages[0].text[:30]}...")
|
||||
|
||||
# Final check
|
||||
final_messages = await store.list_messages()
|
||||
print(f"\nFinal message count: {len(final_messages)} (should be <= 6: 3 messages × 2 per exchange)")
|
||||
|
||||
# Cleanup
|
||||
await store.clear()
|
||||
await store.aclose()
|
||||
print("Cleaned up limits test data\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run all Redis chat message store examples."""
|
||||
print("Redis Chat Message Store Examples")
|
||||
print("=" * 50)
|
||||
print("Prerequisites:")
|
||||
print("- Redis server running on localhost:6379")
|
||||
print("- OPENAI_API_KEY environment variable set")
|
||||
print("=" * 50)
|
||||
|
||||
# Check prerequisites
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("ERROR: OPENAI_API_KEY environment variable not set")
|
||||
return
|
||||
|
||||
try:
|
||||
# Test Redis connection
|
||||
test_store = RedisChatMessageStore(redis_url="redis://localhost:6379")
|
||||
connection_ok = await test_store.ping()
|
||||
await test_store.aclose()
|
||||
if not connection_ok:
|
||||
raise Exception("Redis ping failed")
|
||||
print("✓ Redis connection successful\n")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Cannot connect to Redis: {e}")
|
||||
print("Please ensure Redis is running on localhost:6379")
|
||||
return
|
||||
|
||||
try:
|
||||
# Run all examples
|
||||
await example_basic_redis_store()
|
||||
await example_user_session_management()
|
||||
await example_conversation_persistence()
|
||||
await example_thread_serialization()
|
||||
await example_message_limits()
|
||||
|
||||
print("All examples completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error running examples: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user