mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
@@ -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)
|
||||
+141
@@ -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())
|
||||
+97
@@ -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 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 |
|
||||
|------|-------------|
|
||||
| [`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 full‑text 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) 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 `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,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())
|
||||
Reference in New Issue
Block a user