Python: restructure: Python samples into progressive 01-05 layout (#3862)

* restructure: Python samples into progressive 01-05 layout

- 01-get-started/: 6 numbered steps (hello agent → hosting)
- 02-agents/: all agent concept samples (tools, middleware, providers, etc.)
- 03-workflows/: ALL existing workflow samples preserved as-is
- 04-hosting/: azure-functions, durabletask, a2a
- 05-end-to-end/: demos, evaluation, hosted agents
- Old files moved to _to_delete/ for review
- Added AGENTS.md with structure documentation
- autogen-migration/ and semantic-kernel-migration/ preserved at root

* fix: switch to AzureOpenAI Foundry, fix CI failures

- Switch all 01-get-started samples to AzureOpenAIResponsesClient with
  Azure AI Foundry project endpoint (AZURE_AI_PROJECT_ENDPOINT +
  AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + AzureCliCredential)
- Add _to_delete/ and 05-end-to-end/ to pyrightconfig.samples.json excludes
- Fix test paths in packages/ that referenced old getting_started/ dirs:
  durabletask conftest + streaming test, azurefunctions conftest,
  devui conftest + capture_messages + openai_sdk_integration
- Fix workflow_as_agent_human_in_the_loop.py import (sibling import)
- Update hosting READMEs and tool comment paths
- Replace root README.md with new structure overview
- Update AGENTS.md to document Azure OpenAI Foundry as default provider

* cleanup: remove _to_delete folder, copy resource files to active dirs

All files in _to_delete/ were either:
- Exact duplicates of files in the new structure (240 files)
- Same file with only comment path updates (100 files)
- One import-fix diff (workflow_as_agent_human_in_the_loop.py)
- One superseded minimal_sample.py

Resource files (sample.pdf, countries.json, employees.pdf, weather.json)
copied to 02-agents/sample_assets/ and 02-agents/resources/ since active
samples reference them.

* fix: address PR review comments, centralize resources, remove root duplicates

- Fix type annotation in 04_memory.py (string union -> proper types)
- Fix old sample paths in observability files
- Fix grammar/spelling in observability samples
- Move sample_assets/ and resources/ to shared/ folder
- Remove 8 duplicate observability files from 02-agents root
- Update resource path references in multimodal_input and provider samples

* fix: update broken links from old getting_started paths to new structure

- Update relative paths in READMEs: getting_started/ → 01-get-started/,
  02-agents/, 03-workflows/, 04-hosting/, 05-end-to-end/
- Fix absolute GitHub URLs in package READMEs
- Fix broken link in ollama package README

* fix: convert absolute GitHub URLs to relative paths for link checker

Absolute URLs to python/samples/ on main branch 404 until PR merges.
Converted to relative paths that linkspector can verify locally.

* fix: update link for handoff sample moved to orchestrations/

* fix: update chatkit-integration README path from demos/ to 05-end-to-end/

* fix: update broken links in orchestrations README to match flat directory structure
This commit is contained in:
Eduard van Valkenburg
2026-02-12 17:36:36 +00:00
committed by GitHub
parent 69dcfe31ee
commit a2856d3b92
536 changed files with 3816 additions and 1632 deletions
@@ -0,0 +1,179 @@
# Context Provider Examples
Context providers enable agents to maintain memory, retrieve relevant information, and enhance conversations with external context. The Agent Framework supports various context providers for different use cases, from simple in-memory storage to advanced persistent solutions with search capabilities.
This folder contains examples demonstrating how to use different context providers with the Agent Framework.
## Overview
Context providers implement two key methods:
- **`invoking`**: Called before the agent processes a request. Provides additional context, instructions, or retrieved information to enhance the agent's response.
- **`invoked`**: Called after the agent generates a response. Allows for storing information, updating memory, or performing post-processing.
## Examples
### Simple Context Provider
| File | Description | Installation |
|------|-------------|--------------|
| [`simple_context_provider.py`](simple_context_provider.py) | Demonstrates building a custom context provider that extracts and stores user information (name and age) from conversations. Shows how to use structured output to extract data and provide dynamic instructions based on stored context. | No additional package required - uses core `agent-framework` |
**Install:**
```bash
pip install agent-framework-azure-ai
```
### Azure AI Search
| File | Description |
|------|-------------|
| [`azure_ai_search/azure_ai_with_search_context_agentic.py`](azure_ai_search/azure_ai_with_search_context_agentic.py) | **Agentic mode** (recommended for most scenarios): Uses Knowledge Bases in Azure AI Search for query planning and multi-hop reasoning. Provides more accurate results through intelligent retrieval. Slightly slower with more token consumption. |
| [`azure_ai_search/azure_ai_with_search_context_semantic.py`](azure_ai_search/azure_ai_with_search_context_semantic.py) | **Semantic mode** (fast queries): Fast hybrid search combining vector and keyword search with semantic ranking. Best for scenarios where speed is critical. |
**Install:**
```bash
pip install agent-framework-azure-ai-search agent-framework-azure-ai
```
**Prerequisites:**
- Azure AI Search service with a search index
- Azure AI Foundry project with a model deployment
- For agentic mode: Azure OpenAI resource for Knowledge Base model calls
- Environment variables: `AZURE_SEARCH_ENDPOINT`, `AZURE_SEARCH_INDEX_NAME`, `AZURE_AI_PROJECT_ENDPOINT`
**Key Concepts:**
- **Agentic mode**: Intelligent retrieval with multi-hop reasoning, better for complex queries
- **Semantic mode**: Fast hybrid search with semantic ranking, better for simple queries and speed
### Mem0
The [mem0](mem0/) folder contains examples using Mem0, a self-improving memory layer that enables applications to have long-term memory capabilities.
| File | Description |
|------|-------------|
| [`mem0/mem0_basic.py`](mem0/mem0_basic.py) | Basic example storing and retrieving user preferences across different conversation threads. |
| [`mem0/mem0_threads.py`](mem0/mem0_threads.py) | Advanced thread scoping strategies: global scope (memories shared), per-operation scope (memories isolated), and multiple agents with different memory configurations. |
| [`mem0/mem0_oss.py`](mem0/mem0_oss.py) | Using Mem0 Open Source self-hosted version as the context provider. |
**Install:**
```bash
pip install agent-framework-mem0
```
**Prerequisites:**
- Mem0 API key from [app.mem0.ai](https://app.mem0.ai/) OR self-host [Mem0 Open Source](https://docs.mem0.ai/open-source/overview)
- For Mem0 Platform: `MEM0_API_KEY` environment variable
- For Mem0 OSS: `OPENAI_API_KEY` for embedding generation
**Key Concepts:**
- **Global Scope**: Memories shared across all conversation threads
- **Thread Scope**: Memories isolated per conversation thread
- **Memory Association**: Records can be associated with `user_id`, `agent_id`, `thread_id`, or `application_id`
See the [mem0 README](mem0/README.md) for detailed documentation.
### Redis
The [redis](redis/) folder contains examples using Redis (RediSearch) for persistent, searchable memory with full-text and optional hybrid vector search.
| File | Description |
|------|-------------|
| [`redis/redis_basics.py`](redis/redis_basics.py) | Standalone provider usage and agent integration. Demonstrates writing messages, full-text/hybrid search, persisting preferences, and tool output memory. |
| [`redis/redis_conversation.py`](redis/redis_conversation.py) | Conversational examples showing memory persistence across sessions. |
| [`redis/redis_threads.py`](redis/redis_threads.py) | Thread scoping: global scope, per-operation scope, and multiple agents with isolated memory via different `agent_id` values. |
**Install:**
```bash
pip install agent-framework-redis
```
**Prerequisites:**
- Running Redis with RediSearch (Redis Stack or managed service)
- **Docker**: `docker run --name redis -p 6379:6379 -d redis:8.0.3`
- **Redis Cloud**: [redis.io/cloud](https://redis.io/cloud/)
- **Azure Managed Redis**: [Azure quickstart](https://learn.microsoft.com/azure/redis/quickstart-create-managed-redis)
- Optional: `OPENAI_API_KEY` for vector embeddings (hybrid search)
**Key Concepts:**
- **Full-text search**: Fast keyword-based retrieval
- **Hybrid vector search**: Optional embeddings for semantic search (`vectorizer_choice="openai"` or `"hf"`)
- **Memory scoping**: Partition by `application_id`, `agent_id`, `user_id`, or `thread_id`
- **Thread scoping**: `scope_to_per_operation_thread_id=True` isolates memory per operation
See the [redis README](redis/README.md) for detailed documentation.
## Choosing a Context Provider
| Provider | Use Case | Persistence | Search | Complexity |
|----------|----------|-------------|--------|------------|
| **Simple/Custom** | Learning, prototyping, simple memory needs | No (in-memory) | No | Low |
| **Azure AI Search** | RAG, document search, enterprise knowledge bases | Yes | Hybrid + Semantic | Medium |
| **Mem0** | Long-term user memory, preferences, personalization | Yes (cloud/self-hosted) | Semantic | Low-Medium |
| **Redis** | Fast retrieval, session memory, full-text + vector search | Yes | Full-text + Hybrid | Medium |
## Common Patterns
### 1. User Preference Memory
Store and retrieve user preferences, settings, or personal information across sessions.
- **Examples**: `simple_context_provider.py`, `mem0/mem0_basic.py`, `redis/redis_basics.py`
### 2. Document Retrieval (RAG)
Retrieve relevant documents or knowledge base articles to answer questions.
- **Examples**: `azure_ai_search/azure_ai_with_search_context_*.py`
### 3. Conversation History
Maintain conversation context across multiple turns and sessions.
- **Examples**: `redis/redis_conversation.py`, `mem0/mem0_threads.py`
### 4. Thread Scoping
Isolate memory per conversation thread or share globally across threads.
- **Examples**: `mem0/mem0_threads.py`, `redis/redis_threads.py`
### 5. Multi-Agent Memory
Different agents with isolated or shared memory configurations.
- **Examples**: `mem0/mem0_threads.py`, `redis/redis_threads.py`
## Building Custom Context Providers
To create a custom context provider, implement the `ContextProvider` protocol:
```python
from agent_framework import ContextProvider, Context, Message
from collections.abc import MutableSequence, Sequence
from typing import Any
class MyContextProvider(ContextProvider):
async def invoking(
self,
messages: Message | MutableSequence[Message],
**kwargs: Any
) -> Context:
"""Provide context before the agent processes the request."""
# Return additional instructions, messages, or context
return Context(instructions="Additional instructions here")
async def invoked(
self,
request_messages: Message | Sequence[Message],
response_messages: Message | Sequence[Message] | None = None,
invoke_exception: Exception | None = None,
**kwargs: Any,
) -> None:
"""Process the response after the agent generates it."""
# Store information, update memory, etc.
pass
def serialize(self) -> str:
"""Serialize the provider state for persistence."""
return "{}"
```
See `simple_context_provider.py` for a complete example.
## Additional Resources
- [Agent Framework Documentation](https://github.com/microsoft/agent-framework)
- [Azure AI Search Documentation](https://learn.microsoft.com/azure/search/)
- [Mem0 Documentation](https://docs.mem0.ai/)
- [Redis Documentation](https://redis.io/docs/)
@@ -0,0 +1,276 @@
# Copyright (c) Microsoft. All rights reserved.
"""
This sample demonstrates how to use an AggregateContextProvider to combine multiple context providers.
The AggregateContextProvider is a convenience class that allows you to aggregate multiple
ContextProviders into a single provider. It delegates events to all providers and combines
their context before returning.
You can use this implementation as-is, or implement your own aggregation logic.
"""
import asyncio
import sys
from collections.abc import MutableSequence, Sequence
from contextlib import AsyncExitStack
from types import TracebackType
from typing import TYPE_CHECKING, Any, cast
from agent_framework import Agent, Context, ContextProvider, Message
from agent_framework.azure import AzureAIClient
from azure.identity.aio import AzureCliCredential
if TYPE_CHECKING:
from agent_framework import FunctionTool
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover
# region AggregateContextProvider
class AggregateContextProvider(ContextProvider):
"""A ContextProvider that contains multiple context providers.
It delegates events to multiple context providers and aggregates responses from those
events before returning. This allows you to combine multiple context providers into a
single provider.
Examples:
.. code-block:: python
from agent_framework import Agent
# Create multiple context providers
provider1 = CustomContextProvider1()
provider2 = CustomContextProvider2()
provider3 = CustomContextProvider3()
# Combine them using AggregateContextProvider
aggregate = AggregateContextProvider([provider1, provider2, provider3])
# Pass the aggregate to the agent
agent = Agent(client=client, name="assistant", context_provider=aggregate)
# You can also add more providers later
provider4 = CustomContextProvider4()
aggregate.add(provider4)
"""
def __init__(self, context_providers: ContextProvider | Sequence[ContextProvider] | None = None) -> None:
"""Initialize the AggregateContextProvider with context providers.
Args:
context_providers: The context provider(s) to add.
"""
if isinstance(context_providers, ContextProvider):
self.providers = [context_providers]
else:
self.providers = cast(list[ContextProvider], context_providers) or []
self._exit_stack: AsyncExitStack | None = None
def add(self, context_provider: ContextProvider) -> None:
"""Add a new context provider.
Args:
context_provider: The context provider to add.
"""
self.providers.append(context_provider)
@override
async def thread_created(self, thread_id: str | None = None) -> None:
await asyncio.gather(*[x.thread_created(thread_id) for x in self.providers])
@override
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
contexts = await asyncio.gather(*[provider.invoking(messages, **kwargs) for provider in self.providers])
instructions: str = ""
return_messages: list[Message] = []
tools: list["FunctionTool"] = []
for ctx in contexts:
if ctx.instructions:
instructions += ctx.instructions
if ctx.messages:
return_messages.extend(ctx.messages)
if ctx.tools:
tools.extend(ctx.tools)
return Context(instructions=instructions, messages=return_messages, tools=tools)
@override
async def invoked(
self,
request_messages: Message | Sequence[Message],
response_messages: Message | Sequence[Message] | None = None,
invoke_exception: Exception | None = None,
**kwargs: Any,
) -> None:
await asyncio.gather(*[
x.invoked(
request_messages=request_messages,
response_messages=response_messages,
invoke_exception=invoke_exception,
**kwargs,
)
for x in self.providers
])
@override
async def __aenter__(self) -> "Self":
"""Enter the async context manager and set up all providers.
Returns:
The AggregateContextProvider instance for chaining.
"""
self._exit_stack = AsyncExitStack()
await self._exit_stack.__aenter__()
# Enter all context providers
for provider in self.providers:
await self._exit_stack.enter_async_context(provider)
return self
@override
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Exit the async context manager and clean up all providers.
Args:
exc_type: The exception type if an exception occurred, None otherwise.
exc_val: The exception value if an exception occurred, None otherwise.
exc_tb: The exception traceback if an exception occurred, None otherwise.
"""
if self._exit_stack is not None:
await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
self._exit_stack = None
# endregion
# region Example Context Providers
class TimeContextProvider(ContextProvider):
"""A simple context provider that adds time-related instructions."""
@override
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
from datetime import datetime
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return Context(instructions=f"The current date and time is: {current_time}. ")
class PersonaContextProvider(ContextProvider):
"""A context provider that adds a persona to the agent."""
def __init__(self, persona: str):
self.persona = persona
@override
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
return Context(instructions=f"Your persona: {self.persona}. ")
class PreferencesContextProvider(ContextProvider):
"""A context provider that adds user preferences."""
def __init__(self):
self.preferences: dict[str, str] = {}
@override
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
if not self.preferences:
return Context()
prefs_str = ", ".join(f"{k}: {v}" for k, v in self.preferences.items())
return Context(instructions=f"User preferences: {prefs_str}. ")
@override
async def invoked(
self,
request_messages: Message | Sequence[Message],
response_messages: Message | Sequence[Message] | None = None,
invoke_exception: Exception | None = None,
**kwargs: Any,
) -> None:
# Simple example: extract and store preferences from user messages
# In a real implementation, you might use structured extraction
msgs = [request_messages] if isinstance(request_messages, Message) else list(request_messages)
for msg in msgs:
content = msg.text if hasattr(msg, "text") else ""
# Very simple extraction - in production, use LLM-based extraction
if isinstance(content, str) and "prefer" in content.lower() and ":" in content:
parts = content.split(":")
if len(parts) >= 2:
key = parts[0].strip().lower().replace("i prefer ", "")
value = parts[1].strip()
self.preferences[key] = value
# endregion
# region Main
async def main():
"""Demonstrate using AggregateContextProvider to combine multiple providers."""
async with AzureCliCredential() as credential:
client = AzureAIClient(credential=credential)
# Create individual context providers
time_provider = TimeContextProvider()
persona_provider = PersonaContextProvider("You are a helpful and friendly AI assistant named Max.")
preferences_provider = PreferencesContextProvider()
# Combine them using AggregateContextProvider
aggregate_provider = AggregateContextProvider([
time_provider,
persona_provider,
preferences_provider,
])
# Create the agent with the aggregate provider
async with Agent(
client=client,
instructions="You are a helpful assistant.",
context_provider=aggregate_provider,
) as agent:
# Create a new thread for the conversation
thread = agent.get_new_thread()
# First message - the agent should include time and persona context
print("User: Hello! Who are you?")
result = await agent.run("Hello! Who are you?", thread=thread)
print(f"Agent: {result}\n")
# Set a preference
print("User: I prefer language: formal English")
result = await agent.run("I prefer language: formal English", thread=thread)
print(f"Agent: {result}\n")
# Ask something - the agent should now include the preference
print("User: Can you tell me a fun fact?")
result = await agent.run("Can you tell me a fun fact?", thread=thread)
print(f"Agent: {result}\n")
# Show what the aggregate provider is tracking
print(f"\nPreferences tracked: {preferences_provider.preferences}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,264 @@
# Azure AI Search Context Provider Examples
Azure AI Search context provider enables Retrieval Augmented Generation (RAG) with your agents by retrieving relevant documents from Azure AI Search indexes. It supports two search modes optimized for different use cases.
This folder contains examples demonstrating how to use the Azure AI Search context provider with the Agent Framework.
## Examples
| File | Description |
|------|-------------|
| [`azure_ai_with_search_context_agentic.py`](azure_ai_with_search_context_agentic.py) | **Agentic mode** (recommended for most scenarios): Uses Knowledge Bases in Azure AI Search for query planning and multi-hop reasoning. Provides more accurate results through intelligent retrieval with automatic query reformulation. Slightly slower with more token consumption for query planning. [Learn more](https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720) |
| [`azure_ai_with_search_context_semantic.py`](azure_ai_with_search_context_semantic.py) | **Semantic mode** (fast queries): Fast hybrid search combining vector and keyword search with semantic ranking. Returns raw search results as context. Best for scenarios where speed is critical and simple retrieval is sufficient. |
## Installation
```bash
pip install agent-framework-azure-ai-search agent-framework-azure-ai
```
## Prerequisites
### Required Resources
1. **Azure AI Search service** with a search index containing your documents
- [Create Azure AI Search service](https://learn.microsoft.com/azure/search/search-create-service-portal)
- [Create and populate a search index](https://learn.microsoft.com/azure/search/search-what-is-an-index)
2. **Azure AI Foundry project** with a model deployment
- [Create Azure AI Foundry project](https://learn.microsoft.com/azure/ai-studio/how-to/create-projects)
- Deploy a model (e.g., GPT-4o)
3. **For Agentic mode only**: Azure OpenAI resource for Knowledge Base model calls
- [Create Azure OpenAI resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource)
- Note: This is separate from your Azure AI Foundry project endpoint
### Authentication
Both examples support two authentication methods:
- **API Key**: Set `AZURE_SEARCH_API_KEY` environment variable
- **Entra ID (Managed Identity)**: Uses `DefaultAzureCredential` when API key is not provided
Run `az login` if using Entra ID authentication.
## Configuration
### Environment Variables
**Common (both modes):**
- `AZURE_SEARCH_ENDPOINT`: Your Azure AI Search endpoint (e.g., `https://myservice.search.windows.net`)
- `AZURE_SEARCH_INDEX_NAME`: Name of your search index
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: Model deployment name (e.g., `gpt-4o`, defaults to `gpt-4o`)
- `AZURE_SEARCH_API_KEY`: _(Optional)_ Your search API key - if not provided, uses DefaultAzureCredential
**Agentic mode only:**
- `AZURE_SEARCH_KNOWLEDGE_BASE_NAME`: Name of your Knowledge Base in Azure AI Search
- `AZURE_OPENAI_RESOURCE_URL`: Your Azure OpenAI resource URL (e.g., `https://myresource.openai.azure.com`)
- **Important**: This is different from `AZURE_AI_PROJECT_ENDPOINT` - Knowledge Base needs the OpenAI endpoint for model calls
### Example .env file
**For Semantic Mode:**
```env
AZURE_SEARCH_ENDPOINT=https://myservice.search.windows.net
AZURE_SEARCH_INDEX_NAME=my-index
AZURE_AI_PROJECT_ENDPOINT=https://<resource-name>.services.ai.azure.com/api/projects/<project-name>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Optional - omit to use Entra ID
AZURE_SEARCH_API_KEY=your-search-key
```
**For Agentic Mode (add these to semantic mode variables):**
```env
AZURE_SEARCH_KNOWLEDGE_BASE_NAME=my-knowledge-base
AZURE_OPENAI_RESOURCE_URL=https://myresource.openai.azure.com
```
## Search Modes Comparison
| Feature | Semantic Mode | Agentic Mode |
|---------|--------------|--------------|
| **Speed** | Fast | Slower (query planning overhead) |
| **Token Usage** | Lower | Higher (query reformulation) |
| **Retrieval Strategy** | Hybrid search + semantic ranking | Multi-hop reasoning with Knowledge Base |
| **Query Handling** | Direct search | Automatic query reformulation |
| **Best For** | Simple queries, speed-critical apps | Complex queries, multi-document reasoning |
| **Additional Setup** | None | Requires Knowledge Base + OpenAI resource |
### When to Use Semantic Mode
- **Simple queries** where direct keyword/vector search is sufficient
- **Speed is critical** and you need low latency
- **Straightforward retrieval** from single documents
- **Lower token costs** are important
### When to Use Agentic Mode
- **Complex queries** requiring multi-hop reasoning
- **Cross-document analysis** where information spans multiple sources
- **Ambiguous queries** that benefit from automatic reformulation
- **Higher accuracy** is more important than speed
- You need **intelligent query planning** and document synthesis
## How the Examples Work
### Semantic Mode Flow
1. User query is sent to Azure AI Search
2. Hybrid search (vector + keyword) retrieves relevant documents
3. Semantic ranking reorders results for relevance
4. Top-k documents are returned as context
5. Agent generates response using retrieved context
### Agentic Mode Flow
1. User query is sent to the Knowledge Base
2. Knowledge Base plans the retrieval strategy
3. Multiple search queries may be executed (multi-hop)
4. Retrieved information is synthesized
5. Enhanced context is provided to the agent
6. Agent generates response with comprehensive context
## Code Example
### Semantic Mode
```python
from agent_framework import Agent
from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider
from azure.identity.aio import DefaultAzureCredential
# Create search provider with semantic mode (default)
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key, # Or use credential for Entra ID
mode="semantic", # Default mode
top_k=3, # Number of documents to retrieve
)
# Create agent with search context
async with AzureAIAgentClient(credential=DefaultAzureCredential()) as client:
async with Agent(
client=client,
model=model_deployment,
context_provider=search_provider,
) as agent:
response = await agent.run("What information is in the knowledge base?")
```
### Agentic Mode
```python
from agent_framework.azure import AzureAISearchContextProvider
# Create search provider with agentic mode
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key,
mode="agentic", # Enable agentic retrieval
knowledge_base_name=knowledge_base_name,
azure_openai_resource_url=azure_openai_resource_url,
top_k=5,
)
# Use with agent (same as semantic mode)
async with Agent(
client=client,
model=model_deployment,
context_provider=search_provider,
) as agent:
response = await agent.run("Analyze and compare topics across documents")
```
## Running the Examples
1. **Set up environment variables** (see Configuration section above)
2. **Ensure you have an Azure AI Search index** with documents:
```bash
# Verify your index exists
curl -X GET "https://myservice.search.windows.net/indexes/my-index?api-version=2024-07-01" \
-H "api-key: YOUR_API_KEY"
```
3. **For agentic mode**: Create a Knowledge Base in Azure AI Search
- [Knowledge Base documentation](https://learn.microsoft.com/azure/search/knowledge-store-create-portal)
4. **Run the examples**:
```bash
# Semantic mode (fast, simple)
python azure_ai_with_search_context_semantic.py
# Agentic mode (intelligent, complex)
python azure_ai_with_search_context_agentic.py
```
## Key Parameters
### Common Parameters
- `endpoint`: Azure AI Search service endpoint
- `index_name`: Name of the search index
- `api_key`: API key for authentication (optional, can use credential instead)
- `credential`: Azure credential for Entra ID auth (e.g., `DefaultAzureCredential()`)
- `mode`: Search mode - `"semantic"` (default) or `"agentic"`
- `top_k`: Number of documents to retrieve (default: 3 for semantic, 5 for agentic)
### Semantic Mode Parameters
- `semantic_configuration`: Name of semantic configuration in your index (optional)
- `query_type`: Query type - `"semantic"` for semantic search (default)
### Agentic Mode Parameters
- `knowledge_base_name`: Name of your Knowledge Base (required)
- `azure_openai_resource_url`: Azure OpenAI resource URL (required)
- `max_search_queries`: Maximum number of search queries to generate (default: 3)
## Troubleshooting
### Common Issues
1. **Authentication errors**
- Ensure `AZURE_SEARCH_API_KEY` is set, or run `az login` for Entra ID auth
- Verify your credentials have search permissions
2. **Index not found**
- Verify `AZURE_SEARCH_INDEX_NAME` matches your index name exactly
- Check that the index exists and contains documents
3. **Agentic mode errors**
- Ensure `AZURE_SEARCH_KNOWLEDGE_BASE_NAME` is correctly configured
- Verify `AZURE_OPENAI_RESOURCE_URL` points to your Azure OpenAI resource (not AI Foundry endpoint)
- Check that your OpenAI resource has the necessary model deployments
4. **No results returned**
- Verify your index has documents with vector embeddings (for semantic/hybrid search)
- Check that your queries match the content in your index
- Try increasing `top_k` parameter
5. **Slow responses in agentic mode**
- This is expected - agentic mode trades speed for accuracy
- Reduce `max_search_queries` if needed
- Consider semantic mode for speed-critical applications
## Performance Tips
- **Use semantic mode** as the default for most scenarios - it's fast and effective
- **Switch to agentic mode** when you need multi-hop reasoning or complex queries
- **Adjust `top_k`** based on your needs - higher values provide more context but increase token usage
- **Enable semantic configuration** in your index for better semantic ranking
- **Use Entra ID authentication** in production for better security
## Additional Resources
- [Azure AI Search Documentation](https://learn.microsoft.com/azure/search/)
- [Azure AI Foundry Documentation](https://learn.microsoft.com/azure/ai-studio/)
- [RAG with Azure AI Search](https://learn.microsoft.com/azure/search/retrieval-augmented-generation-overview)
- [Semantic Search in Azure AI Search](https://learn.microsoft.com/azure/search/semantic-search-overview)
- [Knowledge Bases in Azure AI Search](https://learn.microsoft.com/azure/search/knowledge-store-concept-intro)
- [Agentic Retrieval Blog Post](https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720)
@@ -0,0 +1,141 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample demonstrates how to use Azure AI Search with agentic mode for RAG
(Retrieval Augmented Generation) with Azure AI agents.
**Agentic mode** is recommended for most scenarios:
- Uses Knowledge Bases in Azure AI Search for query planning
- Performs multi-hop reasoning across documents
- Provides more accurate results through intelligent retrieval
- Slightly slower with more token consumption for query planning
- See: https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720
For simple queries where speed is critical, use semantic mode instead (see azure_ai_with_search_context_semantic.py).
Prerequisites:
1. An Azure AI Search service
2. An Azure AI Foundry project with a model deployment
3. Either an existing Knowledge Base OR a search index (to auto-create a KB)
Environment variables:
- AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint
- AZURE_SEARCH_API_KEY: (Optional) API key - if not provided, uses DefaultAzureCredential
- AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name (e.g., "gpt-4o")
For using an existing Knowledge Base (recommended):
- AZURE_SEARCH_KNOWLEDGE_BASE_NAME: Your Knowledge Base name
For auto-creating a Knowledge Base from an index:
- AZURE_SEARCH_INDEX_NAME: Your search index name
- AZURE_OPENAI_RESOURCE_URL: Azure OpenAI resource URL (e.g., "https://myresource.openai.azure.com")
"""
# Sample queries to demonstrate agentic RAG
USER_INPUTS = [
"What information is available in the knowledge base?",
"Analyze and compare the main topics from different documents",
"What connections can you find across different sections?",
]
async def main() -> None:
"""Main function demonstrating Azure AI Search agentic mode."""
# Get configuration from environment
search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"]
search_key = os.environ.get("AZURE_SEARCH_API_KEY")
project_endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
model_deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o")
# Agentic mode requires exactly ONE of: knowledge_base_name OR index_name
# Option 1: Use existing Knowledge Base (recommended)
knowledge_base_name = os.environ.get("AZURE_SEARCH_KNOWLEDGE_BASE_NAME")
# Option 2: Auto-create KB from index (requires azure_openai_resource_url)
index_name = os.environ.get("AZURE_SEARCH_INDEX_NAME")
azure_openai_resource_url = os.environ.get("AZURE_OPENAI_RESOURCE_URL")
# Create Azure AI Search context provider with agentic mode (recommended for accuracy)
print("Using AGENTIC mode (Knowledge Bases with query planning, recommended)\n")
print("This mode is slightly slower but provides more accurate results.\n")
# Configure based on whether using existing KB or auto-creating from index
if knowledge_base_name:
# Use existing Knowledge Base - simplest approach
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
api_key=search_key,
credential=AzureCliCredential() if not search_key else None,
mode="agentic",
knowledge_base_name=knowledge_base_name,
# Optional: Configure retrieval behavior
knowledge_base_output_mode="extractive_data", # or "answer_synthesis"
retrieval_reasoning_effort="minimal", # or "medium", "low"
)
else:
# Auto-create Knowledge Base from index
if not index_name:
raise ValueError("Set AZURE_SEARCH_KNOWLEDGE_BASE_NAME or AZURE_SEARCH_INDEX_NAME")
if not azure_openai_resource_url:
raise ValueError("AZURE_OPENAI_RESOURCE_URL required when using index_name")
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key,
credential=AzureCliCredential() if not search_key else None,
mode="agentic",
azure_openai_resource_url=azure_openai_resource_url,
model_deployment_name=model_deployment,
# Optional: Configure retrieval behavior
knowledge_base_output_mode="extractive_data", # or "answer_synthesis"
retrieval_reasoning_effort="minimal", # or "medium", "low"
top_k=3,
)
# Create agent with search context provider
async with (
search_provider,
AzureAIAgentClient(
project_endpoint=project_endpoint,
model_deployment_name=model_deployment,
credential=AzureCliCredential(),
) as client,
Agent(
client=client,
name="SearchAgent",
instructions=(
"You are a helpful assistant with advanced reasoning capabilities. "
"Use the provided context from the knowledge base to answer complex "
"questions that may require synthesizing information from multiple sources."
),
context_provider=search_provider,
) as agent,
):
print("=== Azure AI Agent with Search Context (Agentic Mode) ===\n")
for user_input in USER_INPUTS:
print(f"User: {user_input}")
print("Agent: ", end="", flush=True)
# Stream response
async for chunk in agent.run(user_input, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample demonstrates how to use Azure AI Search with semantic mode for RAG
(Retrieval Augmented Generation) with Azure AI agents.
**Semantic mode** is the recommended default mode:
- Fast hybrid search combining vector and keyword search
- Uses semantic ranking for improved relevance
- Returns raw search results as context
- Best for most RAG use cases
Prerequisites:
1. An Azure AI Search service with a search index
2. An Azure AI Foundry project with a model deployment
3. Set the following environment variables:
- AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint
- AZURE_SEARCH_API_KEY: (Optional) Your search API key - if not provided, uses DefaultAzureCredential for Entra ID
- AZURE_SEARCH_INDEX_NAME: Your search index name
- AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name (e.g., "gpt-4o")
"""
# Sample queries to demonstrate RAG
USER_INPUTS = [
"What information is available in the knowledge base?",
"Summarize the main topics from the documents",
"Find specific details about the content",
]
async def main() -> None:
"""Main function demonstrating Azure AI Search semantic mode."""
# Get configuration from environment
search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"]
search_key = os.environ.get("AZURE_SEARCH_API_KEY")
index_name = os.environ["AZURE_SEARCH_INDEX_NAME"]
project_endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
model_deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o")
# Create Azure AI Search context provider with semantic mode (recommended, fast)
print("Using SEMANTIC mode (hybrid search + semantic ranking, fast)\n")
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key, # Use api_key for API key auth, or credential for managed identity
credential=AzureCliCredential() if not search_key else None,
mode="semantic", # Default mode
top_k=3, # Retrieve top 3 most relevant documents
)
# Create agent with search context provider
async with (
search_provider,
AzureAIAgentClient(
project_endpoint=project_endpoint,
model_deployment_name=model_deployment,
credential=AzureCliCredential(),
) as client,
Agent(
client=client,
name="SearchAgent",
instructions=(
"You are a helpful assistant. Use the provided context from the "
"knowledge base to answer questions accurately."
),
context_provider=search_provider,
) as agent,
):
print("=== Azure AI Agent with Search Context (Semantic Mode) ===\n")
for user_input in USER_INPUTS:
print(f"User: {user_input}")
print("Agent: ", end="", flush=True)
# Stream response
async for chunk in agent.run(user_input, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,55 @@
# Mem0 Context Provider Examples
[Mem0](https://mem0.ai/) is a self-improving memory layer for Large Language Models that enables applications to have long-term memory capabilities. The Agent Framework's Mem0 context provider integrates with Mem0's API to provide persistent memory across conversation sessions.
This folder contains examples demonstrating how to use the Mem0 context provider with the Agent Framework for persistent memory and context management across conversations.
## Examples
| File | Description |
|------|-------------|
| [`mem0_basic.py`](mem0_basic.py) | Basic example of using Mem0 context provider to store and retrieve user preferences across different conversation threads. |
| [`mem0_threads.py`](mem0_threads.py) | Advanced example demonstrating different thread scoping strategies with Mem0. Covers global thread scope (memories shared across all operations), per-operation thread scope (memories isolated per thread), and multiple agents with different memory configurations for personal vs. work contexts. |
| [`mem0_oss.py`](mem0_oss.py) | Example of using the Mem0 Open Source self-hosted version as the context provider. Demonstrates setup and configuration for local deployment. |
## Prerequisites
### Required Resources
1. [Mem0 API Key](https://app.mem0.ai/) - Sign up for a Mem0 account and get your API key - _or_ self-host [Mem0 Open Source](https://docs.mem0.ai/open-source/overview)
2. Azure AI project endpoint (used in these examples)
3. Azure CLI authentication (run `az login`)
## Configuration
### Environment Variables
Set the following environment variables:
**For Mem0 Platform:**
- `MEM0_API_KEY`: Your Mem0 API key (alternatively, pass it as `api_key` parameter to `Mem0Provider`). Not required if you are self-hosting [Mem0 Open Source](https://docs.mem0.ai/open-source/overview)
**For Mem0 Open Source:**
- `OPENAI_API_KEY`: Your OpenAI API key (used by Mem0 OSS for embedding generation and automatic memory extraction)
**For Azure AI:**
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment
## Key Concepts
### Memory Scoping
The Mem0 context provider supports different scoping strategies:
- **Global Scope** (`scope_to_per_operation_thread_id=False`): Memories are shared across all conversation threads
- **Thread Scope** (`scope_to_per_operation_thread_id=True`): Memories are isolated per conversation thread
### Memory Association
Mem0 records can be associated with different identifiers:
- `user_id`: Associate memories with a specific user
- `agent_id`: Associate memories with a specific agent
- `thread_id`: Associate memories with a specific conversation thread
- `application_id`: Associate memories with an application context
@@ -0,0 +1,82 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import uuid
from agent_framework import tool
from agent_framework.azure import AzureAIAgentClient
from agent_framework.mem0 import Mem0Provider
from azure.identity.aio import AzureCliCredential
# 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 retrieve_company_report(company_code: str, detailed: bool) -> str:
if company_code != "CNTS":
raise ValueError("Company code not found")
if not detailed:
return "CNTS is a company that specializes in technology."
return (
"CNTS is a company that specializes in technology. "
"It had a revenue of $10 million in 2022. It has 100 employees."
)
async def main() -> None:
"""Example of memory usage with Mem0 context provider."""
print("=== Mem0 Context Provider Example ===")
# Each record in Mem0 should be associated with agent_id or user_id or application_id or thread_id.
# In this example, we associate Mem0 records with user_id.
user_id = str(uuid.uuid4())
# For Azure authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
# For Mem0 authentication, set Mem0 API key via "api_key" parameter or MEM0_API_KEY environment variable.
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="FriendlyAssistant",
instructions="You are a friendly assistant.",
tools=retrieve_company_report,
context_provider=Mem0Provider(user_id=user_id),
) as agent,
):
# First ask the agent to retrieve a company report with no previous context.
# The agent will not be able to invoke the tool, since it doesn't know
# the company code or the report format, so it should ask for clarification.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Now tell the agent the company code and the report format that you want to use
# and it should be able to invoke the tool and return the report.
query = "I always work with CNTS and I always want a detailed report format. Please remember and retrieve it."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Mem0 processes and indexes memories asynchronously.
# Wait for memories to be indexed before querying in a new thread.
# In production, consider implementing retry logic or using Mem0's
# eventual consistency handling instead of a fixed delay.
print("Waiting for memories to be processed...")
await asyncio.sleep(12) # Empirically determined delay for Mem0 indexing
print("\nRequest within a new thread:")
# Create a new thread for the agent.
# The new thread has no context of the previous conversation.
thread = agent.get_new_thread()
# Since we have the mem0 component in the thread, the agent should be able to
# retrieve the company report without asking for clarification, as it will
# be able to remember the user preferences from Mem0 component.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
print(f"Agent: {result}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import uuid
from agent_framework import tool
from agent_framework.azure import AzureAIAgentClient
from agent_framework.mem0 import Mem0Provider
from azure.identity.aio import AzureCliCredential
from mem0 import AsyncMemory
# 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 retrieve_company_report(company_code: str, detailed: bool) -> str:
if company_code != "CNTS":
raise ValueError("Company code not found")
if not detailed:
return "CNTS is a company that specializes in technology."
return (
"CNTS is a company that specializes in technology. "
"It had a revenue of $10 million in 2022. It has 100 employees."
)
async def main() -> None:
"""Example of memory usage with local Mem0 OSS context provider."""
print("=== Mem0 Context Provider Example ===")
# Each record in Mem0 should be associated with agent_id or user_id or application_id or thread_id.
# In this example, we associate Mem0 records with user_id.
user_id = str(uuid.uuid4())
# For Azure authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
# By default, local Mem0 authenticates to your OpenAI using the OPENAI_API_KEY environment variable.
# See the Mem0 documentation for other LLM providers and authentication options.
local_mem0_client = AsyncMemory()
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="FriendlyAssistant",
instructions="You are a friendly assistant.",
tools=retrieve_company_report,
context_provider=Mem0Provider(user_id=user_id, mem0_client=local_mem0_client),
) as agent,
):
# First ask the agent to retrieve a company report with no previous context.
# The agent will not be able to invoke the tool, since it doesn't know
# the company code or the report format, so it should ask for clarification.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Now tell the agent the company code and the report format that you want to use
# and it should be able to invoke the tool and return the report.
query = "I always work with CNTS and I always want a detailed report format. Please remember and retrieve it."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
print("\nRequest within a new thread:")
# Create a new thread for the agent.
# The new thread has no context of the previous conversation.
thread = agent.get_new_thread()
# Since we have the mem0 component in the thread, the agent should be able to
# retrieve the company report without asking for clarification, as it will
# be able to remember the user preferences from Mem0 component.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
print(f"Agent: {result}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,167 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import uuid
from agent_framework import tool
from agent_framework.azure import AzureAIAgentClient
from agent_framework.mem0 import Mem0Provider
from azure.identity.aio import AzureCliCredential
# 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_user_preferences(user_id: str) -> str:
"""Mock function to get user preferences."""
preferences = {
"user123": "Prefers concise responses and technical details",
"user456": "Likes detailed explanations with examples",
}
return preferences.get(user_id, "No specific preferences found")
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())
user_id = "user123"
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="GlobalMemoryAssistant",
instructions="You are an assistant that remembers user preferences across conversations.",
tools=get_user_preferences,
context_provider=Mem0Provider(
user_id=user_id,
thread_id=global_thread_id,
scope_to_per_operation_thread_id=False, # Share memories across all threads
),
) as global_agent,
):
# Store some preferences in the global scope
query = "Remember that I prefer technical responses with code examples when discussing programming."
print(f"User: {query}")
result = await global_agent.run(query)
print(f"Agent: {result}\n")
# Create a new thread - but memories should still be accessible due to global scope
new_thread = global_agent.get_new_thread()
query = "What do you know about my preferences?"
print(f"User (new thread): {query}")
result = await global_agent.run(query, thread=new_thread)
print(f"Agent: {result}\n")
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)
user_id = "user123"
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="ScopedMemoryAssistant",
instructions="You are an assistant with thread-scoped memory.",
tools=get_user_preferences,
context_provider=Mem0Provider(
user_id=user_id,
scope_to_per_operation_thread_id=True, # Isolate memories per thread
),
) as scoped_agent,
):
# Create a specific thread for this scoped provider
dedicated_thread = scoped_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 scoped_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 scoped_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 scoped_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 scoped_agent.run(query, thread=dedicated_thread)
print(f"Agent: {result}\n")
async def example_multiple_agents() -> None:
"""Example 3: Multiple agents with different thread configurations."""
print("3. Multiple Agents with Different Thread Configurations:")
print("-" * 40)
agent_id_1 = "agent_personal"
agent_id_2 = "agent_work"
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="PersonalAssistant",
instructions="You are a personal assistant that helps with personal tasks.",
context_provider=Mem0Provider(
agent_id=agent_id_1,
),
) as personal_agent,
AzureAIAgentClient(credential=credential).as_agent(
name="WorkAssistant",
instructions="You are a work assistant that helps with professional tasks.",
context_provider=Mem0Provider(
agent_id=agent_id_2,
),
) as work_agent,
):
# 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")
async def main() -> None:
"""Run all Mem0 thread management examples."""
print("=== Mem0 Thread Management Example ===\n")
await example_global_thread_scope()
await example_per_operation_thread_scope()
await example_multiple_agents()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,113 @@
# Redis Context Provider Examples
The Redis context provider enables persistent, searchable memory for your agents using Redis (RediSearch). It supports fulltext 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 |
|------|-------------|
| [`azure_redis_conversation.py`](azure_redis_conversation.py) | Demonstrates conversation persistence with RedisChatMessageStore and Azure Redis with Azure AD (Entra ID) authentication using credential provider. |
| [`redis_basics.py`](redis_basics.py) | Shows standalone provider usage and agent integration. Demonstrates writing messages to Redis, retrieving context via fulltext or hybrid vector search, and persisting preferences across threads. Also includes a simple tool example whose outputs are remembered. |
| [`redis_conversation.py`](redis_conversation.py) | Simple example showing conversation persistence with RedisChatMessageStore using traditional connection string authentication. |
| [`redis_threads.py`](redis_threads.py) | Demonstrates thread scoping. Includes: (1) global thread scope with a fixed `thread_id` shared across operations; (2) peroperation 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 fulltext 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 `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.
- Peroperation 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,124 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure Managed Redis Chat Message Store with Azure AD Authentication
This example demonstrates how to use Azure Managed Redis with Azure AD authentication
to persist conversational details using RedisChatMessageStore.
Requirements:
- Azure Managed Redis instance with Azure AD authentication enabled
- Azure credentials configured (az login or managed identity)
- agent-framework-redis: pip install agent-framework-redis
- azure-identity: pip install azure-identity
Environment Variables:
- AZURE_REDIS_HOST: Your Azure Managed Redis host (e.g., myredis.redis.cache.windows.net)
- OPENAI_API_KEY: Your OpenAI API key
- OPENAI_CHAT_MODEL_ID: OpenAI model (e.g., gpt-4o-mini)
- AZURE_USER_OBJECT_ID: Your Azure AD User Object ID for authentication
"""
import asyncio
import os
from agent_framework.openai import OpenAIChatClient
from agent_framework.redis import RedisChatMessageStore
from azure.identity.aio import AzureCliCredential
from redis.credentials import CredentialProvider
class AzureCredentialProvider(CredentialProvider):
"""Credential provider for Azure AD authentication with Redis Enterprise."""
def __init__(self, azure_credential: AzureCliCredential, user_object_id: str):
self.azure_credential = azure_credential
self.user_object_id = user_object_id
async def get_credentials_async(self) -> tuple[str] | tuple[str, str]:
"""Get Azure AD token for Redis authentication.
Returns (username, token) where username is the Azure user's Object ID.
"""
token = await self.azure_credential.get_token("https://redis.azure.com/.default")
return (self.user_object_id, token.token)
async def main() -> None:
redis_host = os.environ.get("AZURE_REDIS_HOST")
if not redis_host:
print("ERROR: Set AZURE_REDIS_HOST environment variable")
return
# For Azure Redis with Entra ID, username must be your Object ID
user_object_id = os.environ.get("AZURE_USER_OBJECT_ID")
if not user_object_id:
print("ERROR: Set AZURE_USER_OBJECT_ID environment variable")
print("Get your Object ID from the Azure Portal")
return
# Create Azure CLI credential provider (uses 'az login' credentials)
azure_credential = AzureCliCredential()
credential_provider = AzureCredentialProvider(azure_credential, user_object_id)
thread_id = "azure_test_thread"
# Factory for creating Azure Redis chat message store
def chat_message_store_factory():
return RedisChatMessageStore(
credential_provider=credential_provider,
host=redis_host,
port=10000,
ssl=True,
thread_id=thread_id,
key_prefix="chat_messages",
max_messages=100,
)
# Create chat client
client = OpenAIChatClient()
# Create agent with Azure Redis store
agent = client.as_agent(
name="AzureRedisAssistant",
instructions="You are a helpful assistant.",
chat_message_store_factory=chat_message_store_factory,
)
# Conversation
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 I have a meeting at 3pm tomorrow"
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)
# Cleanup
await azure_credential.close()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,250 @@
# 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 asyncio
import os
from agent_framework import Message, tool
from agent_framework.openai import OpenAIChatClient
from agent_framework_redis._provider import RedisProvider
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
# 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 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 = [
Message("user", ["runA CONVO: User Message"]),
Message("assistant", ["runA CONVO: Assistant Message"]),
Message("system", ["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.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.invoking([Message("system", ["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(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.as_agent(
name="MemoryEnhancedAssistant",
instructions=(
"You are a helpful assistant. Personalize replies using provided context. "
"Before answering, always check for stored context"
),
tools=[],
context_provider=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(model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
agent = client.as_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_provider=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,115 @@
# Copyright (c) Microsoft. All rights reserved.
"""Redis Context Provider: Basic usage and agent integration
This example demonstrates how to use the Redis ChatMessageStoreProtocol 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 asyncio
import os
from agent_framework.openai import OpenAIChatClient
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:
"""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,
)
def chat_message_store_factory():
return 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(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.as_agent(
name="MemoryEnhancedAssistant",
instructions=(
"You are a helpful assistant. Personalize replies using provided context. "
"Before answering, always check for stored context"
),
tools=[],
context_provider=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 I have a meeting at 3pm tomorro"
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,251 @@
# 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.openai import OpenAIChatClient
from agent_framework_redis._provider import RedisProvider
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
# 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(
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.as_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_provider=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(
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.as_agent(
name="ScopedMemoryAssistant",
instructions="You are an assistant with thread-scoped memory.",
context_provider=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(
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.as_agent(
name="PersonalAssistant",
instructions="You are a personal assistant that helps with personal tasks.",
context_provider=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.as_agent(
name="WorkAssistant",
instructions="You are a work assistant that helps with professional tasks.",
context_provider=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())
@@ -0,0 +1,122 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import MutableSequence, Sequence
from typing import Any
from agent_framework import Agent, Context, ContextProvider, Message, SupportsChatGetResponse
from agent_framework.azure import AzureAIClient
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, client: SupportsChatGetResponse, 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 = 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: Message | Sequence[Message],
response_messages: Message | Sequence[Message] | 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 == "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
instructions="Extract the user's name and age from the message if present. "
"If not present return nulls.",
options={"response_format": UserInfo},
)
# Update user info with extracted data
try:
extracted = result.value
if self.user_info.name is None and extracted.name:
self.user_info.name = extracted.name
if self.user_info.age is None and extracted.age:
self.user_info.age = extracted.age
except Exception:
pass # Failed to extract, continue without updating
except Exception:
pass # Failed to extract, continue without updating
async def invoking(self, messages: Message | MutableSequence[Message], **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:
client = AzureAIClient(credential=credential)
# Create the memory provider
memory_provider = UserInfoMemory(client)
# Create the agent with memory
async with Agent(
client=client,
instructions="You are a friendly assistant. Always address the user by their name.",
context_provider=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())