chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Context Provider Samples
|
||||
|
||||
These samples demonstrate how to use context providers to enrich agent conversations with external knowledge — from custom logic to Azure AI Search (RAG) and memory services.
|
||||
|
||||
## Samples
|
||||
|
||||
| File / Folder | Description |
|
||||
|---------------|-------------|
|
||||
| [`simple_context_provider.py`](simple_context_provider.py) | Implement a custom context provider by extending `ContextProvider` to extract and inject structured user information across turns. |
|
||||
| [`azure_ai_foundry_memory.py`](azure_ai_foundry_memory.py) | Use `FoundryMemoryProvider` to add semantic memory — automatically retrieves, searches, and stores memories via Azure AI Foundry. |
|
||||
| [`file_access_data_processing/`](file_access_data_processing/) | Use `FileAccessProvider` with `FileSystemAgentFileStore` to give an agent read/write/search access to a folder of CSV data files. See its own [README](file_access_data_processing/README.md). |
|
||||
| [`azure_ai_search/`](azure_ai_search/) | Retrieval Augmented Generation (RAG) with Azure AI Search in semantic and agentic modes. See its own [README](azure_ai_search/README.md). |
|
||||
| [`mem0/`](mem0/) | Memory-powered context using the Mem0 integration (open-source and managed). See its own [README](mem0/README.md). |
|
||||
| [`redis/`](redis/) | Redis-backed context providers for conversation memory and sessions. See its own [README](redis/README.md). |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**For `simple_context_provider.py`:**
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: Model deployment name
|
||||
- Azure CLI authentication (`az login`)
|
||||
|
||||
**For `azure_ai_foundry_memory.py`:**
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: Chat/responses model deployment name
|
||||
- `AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME`: Embedding model deployment name (e.g., `text-embedding-ada-002`)
|
||||
- Azure CLI authentication (`az login`)
|
||||
|
||||
**For `file_access_data_processing/`:**
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: Chat model deployment name
|
||||
- Azure CLI authentication (`az login`)
|
||||
|
||||
See each subfolder's README for provider-specific prerequisites.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from agent_framework import Agent, InMemoryHistoryProvider
|
||||
from agent_framework.foundry import FoundryChatClient, FoundryMemoryProvider
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
MemoryStoreDefaultDefinition,
|
||||
MemoryStoreDefaultOptions,
|
||||
)
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
Azure AI Agent with Foundry Memory Context Provider Example
|
||||
|
||||
This sample demonstrates using the FoundryMemoryProvider as a context provider
|
||||
to add semantic memory capabilities to your agents. The provider automatically:
|
||||
1. Retrieves static (user profile) memories on first run
|
||||
2. Searches for contextual memories based on conversation
|
||||
3. Updates the memory store with new conversation messages
|
||||
|
||||
The sample creates a temporary memory store with user profile enabled (and chat summary
|
||||
disabled), scopes memories to a specific user ID ("user_123"), and sets update_delay=0
|
||||
so memories are stored immediately (in production, use a delay to batch updates and
|
||||
reduce costs). Conversation history is intentionally not stored (neither service-side
|
||||
via ``store=False`` nor client-side via ``load_messages=False`` on the history provider),
|
||||
so that follow-up responses demonstrate the agent relying solely on Foundry Memory
|
||||
rather than chat history. The memory store is deleted at the end of the run.
|
||||
|
||||
Prerequisites:
|
||||
1. Set FOUNDRY_PROJECT_ENDPOINT environment variable
|
||||
2. Set FOUNDRY_MODEL for the chat/responses model
|
||||
3. Set AZURE_OPENAI_EMBEDDING_MODEL for the embedding model
|
||||
4. Deploy both a chat model (e.g. gpt-4) and an embedding model (e.g. text-embedding-3-small)
|
||||
"""
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=endpoint, credential=credential) as project_client,
|
||||
):
|
||||
# Generate a unique memory store name to avoid conflicts
|
||||
memory_store_name = f"agent_framework_memory_{datetime.now(timezone.utc).strftime('%Y%m%d')}"
|
||||
# Specify memory store options
|
||||
options = MemoryStoreDefaultOptions(
|
||||
chat_summary_enabled=False,
|
||||
user_profile_enabled=True,
|
||||
user_profile_details="Avoid irrelevant or sensitive data, such as age, financials, precise location, and credentials",
|
||||
)
|
||||
memory_store_definition = MemoryStoreDefaultDefinition(
|
||||
chat_model=os.environ["FOUNDRY_MODEL"],
|
||||
embedding_model=os.environ["AZURE_OPENAI_EMBEDDING_MODEL"],
|
||||
options=options,
|
||||
)
|
||||
print(f"Creating memory store '{memory_store_name}'...")
|
||||
try:
|
||||
# Create a memory store
|
||||
memory_store = await project_client.beta.memory_stores.create(
|
||||
name=memory_store_name,
|
||||
description="Memory store for Agent Framework with FoundryMemoryProvider",
|
||||
definition=memory_store_definition,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to create memory store: {e}")
|
||||
return
|
||||
|
||||
print(f"Created memory store: {memory_store.name} ({memory_store.id})")
|
||||
print(f"Description: {memory_store.description}\n")
|
||||
print("==========================================")
|
||||
|
||||
# Create the chat client
|
||||
client = FoundryChatClient(project_client=project_client)
|
||||
# Create the Foundry Memory context provider
|
||||
memory_provider = FoundryMemoryProvider(
|
||||
project_client=project_client,
|
||||
memory_store_name=memory_store.name,
|
||||
scope="user_123", # Scope memories to a specific user, if not set, the session_id
|
||||
# will be used as scope, which means memories are only shared within the same session
|
||||
update_delay=0, # Do not wait to update memories after each interaction (for demo purposes)
|
||||
# In production, consider setting a delay to batch updates and reduce costs
|
||||
)
|
||||
|
||||
# Create an agent with the memory context provider
|
||||
async with Agent(
|
||||
name="MemoryAgent",
|
||||
client=client,
|
||||
instructions="""You are a helpful assistant that remembers past conversations.
|
||||
The memories from previous interactions are automatically provided to you.""",
|
||||
context_providers=[memory_provider, InMemoryHistoryProvider(load_messages=False)],
|
||||
default_options={"store": False},
|
||||
) as agent:
|
||||
try:
|
||||
# note that we will use the service side storage, nor load messsages from the history provider,
|
||||
# but we include it to demonstrate that it can be used alongside the Foundry provider for other use cases.
|
||||
session = agent.create_session()
|
||||
|
||||
# First interaction - establish some preferences
|
||||
print("=== First conversation ===")
|
||||
query1 = "I prefer dark roast coffee and I'm allergic to nuts"
|
||||
print(f"User: {query1}")
|
||||
result1 = await agent.run(query1, session=session)
|
||||
print(f"Agent: {result1}\n")
|
||||
|
||||
# Wait for memories to be processed
|
||||
print("Waiting for memories to be stored...")
|
||||
await asyncio.sleep(8)
|
||||
|
||||
# Second interaction - test memory recall
|
||||
print("=== Second conversation ===")
|
||||
query2 = "Can you recommend a coffee and snack for me?"
|
||||
print(f"User: {query2}")
|
||||
result2 = await agent.run(query2, session=session)
|
||||
print(f"Agent: {result2}\n")
|
||||
|
||||
# Third interaction - continue the conversation
|
||||
print("=== Third conversation ===")
|
||||
query3 = "What do you remember about my preferences?"
|
||||
print(f"User: {query3}")
|
||||
result3 = await agent.run(query3, session=session)
|
||||
print(f"Agent: {result3}\n")
|
||||
|
||||
print(f"Stored memories from: {memory_store.name} ({memory_store.id})")
|
||||
res = await project_client.beta.memory_stores.search_memories(name=memory_store.name, scope="user_123")
|
||||
for memory in res.memories:
|
||||
print(f"Memory: {memory.memory_item.content}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
finally:
|
||||
await project_client.beta.memory_stores.delete(memory_store_name)
|
||||
print("==========================================")
|
||||
print("Memory store deleted")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Example output:
|
||||
Creating memory store 'agent_framework_memory_20260223'...
|
||||
Created memory store: agent_framework_memory_20260223 (memstore_57c1f95bb4040c6d00RVOP71Q8tS23opIc4G4ZE8DuALiBFx44)
|
||||
Description: Memory store for Agent Framework with FoundryMemoryProvider
|
||||
|
||||
==========================================
|
||||
=== First conversation ===
|
||||
User: I prefer dark roast coffee and I'm allergic to nuts
|
||||
Agent: Got it—I’ll remember: you prefer dark roast coffee, and you’re allergic to nuts.
|
||||
|
||||
Waiting for memories to be stored...
|
||||
=== Second conversation ===
|
||||
User: Can you recommend a coffee and snack for me?
|
||||
Agent: For coffee: **dark roast drip or Americano** (choose a **dark roast** like French/Italian roast). If you like it smoother, try a **dark-roast cold brew**.
|
||||
|
||||
For a snack (nut-free): **Greek yogurt with berries**, or a **cheese stick + whole-grain crackers**. If you want something sweet: **dark chocolate (check “may contain nuts” warnings)**.
|
||||
|
||||
=== Third conversation ===
|
||||
User: What do you remember about my preferences?
|
||||
Agent: - You’re allergic to nuts.
|
||||
- You prefer dark roast coffee.
|
||||
|
||||
Stored memories from: agent_framework_memory_20260223 (memstore_57c1f95bb4040c6d00RVOP71Q8tS23opIc4G4ZE8DuALiBFx44)
|
||||
Memory: The user is allergic to nuts.
|
||||
Memory: The user prefers dark roast coffee.
|
||||
==========================================
|
||||
Memory store deleted
|
||||
"""
|
||||
@@ -0,0 +1,284 @@
|
||||
# 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 |
|
||||
|------|-------------|
|
||||
| [`search_context_agentic.py`](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://learn.microsoft.com/azure/search/agentic-retrieval-overview) |
|
||||
| [`search_context_semantic.py`](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-foundry
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
### API versions (stable vs preview)
|
||||
|
||||
The provider auto-detects which build of `azure-search-documents` is installed — nothing to
|
||||
configure in code:
|
||||
|
||||
- **Stable / GA** — `pip install azure-search-documents` (`>=12.0.0`) → api-version `2026-04-01`.
|
||||
- **Preview** — `pip install --pre azure-search-documents` (e.g. `12.1.0b1`) → api-version `2026-05-01-preview`.
|
||||
|
||||
The installed build picks its own api-version, so newer releases work without code changes.
|
||||
|
||||
Agentic `knowledge_base_output_mode="answer_synthesis"` and `retrieval_reasoning_effort` of
|
||||
`"low"`/`"medium"` ship **only** in the preview build. On a stable build the provider uses
|
||||
extractive output with minimal reasoning effort and raises an actionable error if a preview-only
|
||||
option is requested. To enable them, just install the preview build (`pip install --pre
|
||||
azure-search-documents`) — no code change.
|
||||
|
||||
## 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
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: 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 `FOUNDRY_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
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://<resource-name>.services.ai.azure.com/api/projects/<project-name>
|
||||
FOUNDRY_MODEL=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 AzureAISearchContextProvider
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
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 FoundryChatClient(
|
||||
project_endpoint=project_endpoint,
|
||||
model=model_deployment,
|
||||
credential=DefaultAzureCredential(),
|
||||
) as client:
|
||||
async with Agent(
|
||||
client=client,
|
||||
context_providers=[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_providers=[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 in Azure AI Search](https://learn.microsoft.com/azure/search/agentic-retrieval-overview)
|
||||
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.azure import AzureAISearchContextProvider
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
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 AzureCliCredential
|
||||
- FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
|
||||
- FOUNDRY_MODEL: 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["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
model_deployment = os.environ.get("FOUNDRY_MODEL", "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(
|
||||
source_id="search_provider",
|
||||
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. "answer_synthesis" output mode and
|
||||
# "medium"/"low" reasoning effort require the preview build of azure-search-documents
|
||||
# (`pip install --pre azure-search-documents`); the provider auto-detects the build.
|
||||
knowledge_base_output_mode="extractive_data", # or "answer_synthesis" (preview build only)
|
||||
retrieval_reasoning_effort="minimal", # or "medium", "low" (preview build only)
|
||||
)
|
||||
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(
|
||||
source_id="search_provider",
|
||||
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=model_deployment,
|
||||
# Optional: Configure retrieval behavior. "answer_synthesis" output mode and
|
||||
# "medium"/"low" reasoning effort require the preview build of azure-search-documents
|
||||
# (`pip install --pre azure-search-documents`); the provider auto-detects the build.
|
||||
knowledge_base_output_mode="extractive_data", # or "answer_synthesis" (preview build only)
|
||||
retrieval_reasoning_effort="minimal", # or "medium", "low" (preview build only)
|
||||
top_k=3,
|
||||
)
|
||||
|
||||
# Create agent with search context provider
|
||||
async with (
|
||||
search_provider,
|
||||
Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=project_endpoint,
|
||||
model=model_deployment,
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
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_providers=[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)
|
||||
for content in chunk.contents:
|
||||
if content.annotations:
|
||||
print(f"\n[Sources: {content.annotations}]", end="", flush=True)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.azure import AzureAISearchContextProvider
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.openai import OpenAIEmbeddingClient
|
||||
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 AzureCliCredential for Entra ID
|
||||
- AZURE_SEARCH_INDEX_NAME: Your search index name
|
||||
- FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
|
||||
- FOUNDRY_MODEL: Your model deployment name (e.g., "gpt-4o")
|
||||
- AZURE_OPENAI_EMBEDDING_MODEL: (Optional) Your Azure OpenAI embedding deployment for hybrid search
|
||||
- AZURE_OPENAI_ENDPOINT: (Optional) Your Azure OpenAI resource URL, required if using Azure OpenAI embeddings
|
||||
"""
|
||||
|
||||
# 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."""
|
||||
|
||||
credential = AzureCliCredential()
|
||||
|
||||
# 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["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
model_deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o")
|
||||
openai_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
|
||||
embedding_deployment = os.environ.get("AZURE_OPENAI_EMBEDDING_MODEL")
|
||||
|
||||
embedding_client = None
|
||||
if openai_endpoint and embedding_deployment:
|
||||
embedding_client = OpenAIEmbeddingClient(
|
||||
azure_endpoint=openai_endpoint,
|
||||
model=embedding_deployment,
|
||||
credential=credential,
|
||||
)
|
||||
|
||||
# Create Azure AI Search context provider with semantic mode (recommended, fast)
|
||||
print("Using SEMANTIC mode (hybrid search + semantic ranking, fast)\n")
|
||||
search_provider = AzureAISearchContextProvider(
|
||||
source_id="search_provider",
|
||||
endpoint=search_endpoint,
|
||||
index_name=index_name,
|
||||
api_key=search_key, # Use api_key for API key auth, or credential for managed identity
|
||||
credential=credential if not search_key else None,
|
||||
mode="semantic", # Default mode
|
||||
top_k=3, # Retrieve top 3 most relevant documents
|
||||
embedding_function=embedding_client, # Provide embedding function for hybrid search
|
||||
vector_field_name="DescriptionVector"
|
||||
if embedding_client
|
||||
else None, # Set vector field for hybrid search if using embeddings
|
||||
)
|
||||
|
||||
# Create agent with search context provider
|
||||
async with (
|
||||
search_provider,
|
||||
Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=project_endpoint,
|
||||
model=model_deployment,
|
||||
credential=credential,
|
||||
),
|
||||
name="SearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Use the provided context from the "
|
||||
"knowledge base to answer questions accurately."
|
||||
),
|
||||
context_providers=[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,43 @@
|
||||
# CodeAct context providers
|
||||
|
||||
Demonstrates the provider-owned CodeAct flow with two backends:
|
||||
|
||||
| File | Backend | Notes |
|
||||
|------|---------|-------|
|
||||
| [`code_act.py`](code_act.py) | [Hyperlight](https://github.com/hyperlight-dev/hyperlight) WASM sandbox via `HyperlightCodeActProvider` | Hardened sandbox with WASM isolation; sandbox tools called via `call_tool(...)`. |
|
||||
| [`monty_code_act.py`](monty_code_act.py) | [Monty](https://github.com/pydantic/monty) Rust-based Python interpreter via `MontyCodeActProvider` (alpha) | Cross-platform pure interpreter; sandbox tools can be called as typed async functions (`await compute(...)`) or via `call_tool(...)`. |
|
||||
|
||||
Both providers inject an `execute_code` tool into the agent and keep the
|
||||
registered sandbox tools (`compute`, `fetch_data`) hidden from the model — the
|
||||
model invokes them from inside the sandbox.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install agent-framework agent-framework-hyperlight --pre # Hyperlight sample
|
||||
pip install agent-framework agent-framework-monty --pre # Monty sample
|
||||
```
|
||||
|
||||
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
|
||||
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
|
||||
> at runtime when it tries to create the sandbox.
|
||||
>
|
||||
> Monty is cross-platform and has no hypervisor/WASM backend dependency, but it
|
||||
> interprets a Python subset (e.g. `os`/network/subprocess access is blocked).
|
||||
> `agent-framework-monty` is an alpha package and is not yet part of
|
||||
> `agent-framework[all]`; install it explicitly with `--pre`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
|
||||
- A deployed model (`FOUNDRY_MODEL`)
|
||||
- Azure CLI authenticated (`az login`)
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
python code_act.py # Hyperlight
|
||||
python monty_code_act.py # Monty
|
||||
```
|
||||
|
||||
See the source files for the full annotated examples.
|
||||
@@ -0,0 +1,187 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.hyperlight import HyperlightCodeActProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""This sample demonstrates the provider-owned Hyperlight CodeAct flow.
|
||||
|
||||
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
|
||||
registers them only with `HyperlightCodeActProvider`. The model therefore sees a
|
||||
single `execute_code` tool and must call the provider-owned tools from inside
|
||||
the sandbox with `call_tool(...)`.
|
||||
"""
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_CYAN = "\033[36m"
|
||||
_YELLOW = "\033[33m"
|
||||
_GREEN = "\033[32m"
|
||||
_DIM = "\033[2m"
|
||||
_RESET = "\033[0m"
|
||||
|
||||
|
||||
class _ColoredFormatter(logging.Formatter):
|
||||
"""Dim logger output so it does not compete with sample prints."""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
return f"{_DIM}{super().format(record)}{_RESET}"
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logging.getLogger().handlers[0].setFormatter(
|
||||
_ColoredFormatter("[%(asctime)s] %(levelname)s: %(message)s"),
|
||||
)
|
||||
|
||||
|
||||
@function_middleware
|
||||
async def log_function_calls(
|
||||
context: FunctionInvocationContext,
|
||||
call_next: Callable[[], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Log tool calls, including readable execute_code blocks."""
|
||||
import time
|
||||
|
||||
function_name = context.function.name
|
||||
arguments = context.arguments if isinstance(context.arguments, dict) else {}
|
||||
|
||||
if function_name == "execute_code" and "code" in arguments:
|
||||
print(f"\n{_YELLOW}{'─' * 60}")
|
||||
print("▶ execute_code")
|
||||
print(f"{'─' * 60}{_RESET}")
|
||||
print(arguments["code"])
|
||||
print(f"{_YELLOW}{'─' * 60}{_RESET}")
|
||||
else:
|
||||
pairs = ", ".join(f"{name}={value!r}" for name, value in arguments.items())
|
||||
print(f"\n{_YELLOW}▶ {function_name}({pairs}){_RESET}")
|
||||
|
||||
start = time.perf_counter()
|
||||
await call_next()
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result = context.result
|
||||
if function_name == "execute_code" and isinstance(result, list):
|
||||
for output in result:
|
||||
if output.type == "text" and output.text:
|
||||
print(f"{_GREEN}stdout:\n{output.text}{_RESET}")
|
||||
elif output.type == "error" and output.error_details:
|
||||
print(f"{_YELLOW}stderr:\n{output.error_details}{_RESET}")
|
||||
else:
|
||||
print(f"{_YELLOW}◀ {function_name} → {result!r}{_RESET}")
|
||||
|
||||
print(f"{_DIM} ({elapsed:.4f}s){_RESET}")
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def compute(
|
||||
operation: Annotated[
|
||||
Literal["add", "subtract", "multiply", "divide"],
|
||||
"Math operation: add, subtract, multiply, or divide.",
|
||||
],
|
||||
a: Annotated[float, "First numeric operand."],
|
||||
b: Annotated[float, "Second numeric operand."],
|
||||
) -> float:
|
||||
"""Perform a math operation for sandboxed code."""
|
||||
operations = {
|
||||
"add": a + b,
|
||||
"subtract": a - b,
|
||||
"multiply": a * b,
|
||||
"divide": a / b if b else float("inf"),
|
||||
}
|
||||
return operations[operation]
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
async def fetch_data(
|
||||
table: Annotated[str, "Name of the simulated table to query."],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Fetch records from a named table."""
|
||||
await asyncio.sleep(0.5)
|
||||
data: dict[str, list[dict[str, Any]]] = {
|
||||
"users": [
|
||||
{"id": 1, "name": "Alice", "role": "admin"},
|
||||
{"id": 2, "name": "Bob", "role": "user"},
|
||||
{"id": 3, "name": "Charlie", "role": "admin"},
|
||||
],
|
||||
"products": [
|
||||
{"id": 101, "name": "Widget", "price": 9.99},
|
||||
{"id": 102, "name": "Gadget", "price": 19.99},
|
||||
],
|
||||
}
|
||||
return data.get(table, [])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the provider-owned Hyperlight CodeAct sample."""
|
||||
# 1. Create the Hyperlight-backed provider and register sandbox tools on it.
|
||||
codeact = HyperlightCodeActProvider(
|
||||
tools=[compute, fetch_data],
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
# 2. Create the client and the agent.
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
name="HyperlightCodeActProviderAgent",
|
||||
instructions="You are a helpful assistant.",
|
||||
context_providers=[codeact],
|
||||
middleware=[log_function_calls],
|
||||
)
|
||||
|
||||
# 3. Run a request that should use execute_code plus provider-owned tools.
|
||||
query = (
|
||||
"Fetch all users, find admins, multiply 7*(3*2), and print the users, "
|
||||
"admins, and multiplication result. Use execute_code and call_tool(...) "
|
||||
"inside the sandbox."
|
||||
)
|
||||
print(f"{_CYAN}{'=' * 60}")
|
||||
print("Hyperlight CodeAct provider sample")
|
||||
print(f"{'=' * 60}{_RESET}")
|
||||
print(f"{_CYAN}User: {query}{_RESET}")
|
||||
result = await agent.run(query)
|
||||
print(f"{_CYAN}Agent: {result.text}{_RESET}")
|
||||
|
||||
|
||||
"""
|
||||
Sample output (shape only):
|
||||
|
||||
============================================================
|
||||
Hyperlight CodeAct provider sample
|
||||
============================================================
|
||||
User: Fetch all users, find admins, multiply 7*(3*2), ...
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
▶ execute_code
|
||||
────────────────────────────────────────────────────────────
|
||||
users = call_tool("fetch_data", table="users")
|
||||
admins = [user for user in users if user["role"] == "admin"]
|
||||
result = call_tool("compute", operation="multiply", a=7, b=6)
|
||||
print("Users:", users)
|
||||
print("Admins:", admins)
|
||||
print("7 * 6 =", result)
|
||||
────────────────────────────────────────────────────────────
|
||||
stdout:
|
||||
Users: [...]
|
||||
Admins: [...]
|
||||
7 * 6 = 42.0
|
||||
(0.0xxx s)
|
||||
Agent: ...
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,201 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_monty import MontyCodeActProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""This sample demonstrates the provider-owned Monty CodeAct flow.
|
||||
|
||||
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
|
||||
registers them only with `MontyCodeActProvider`. The model therefore sees a
|
||||
single `execute_code` tool and calls the provider-owned tools from inside the
|
||||
sandbox - either as typed async functions (`await compute(...)`) or via the
|
||||
generic `call_tool(...)` fallback.
|
||||
|
||||
`MontyCodeActProvider` uses [pydantic-monty](https://github.com/pydantic/monty),
|
||||
a Rust-based Python interpreter, so it runs cross-platform with no
|
||||
hypervisor/WASM backend dependency.
|
||||
|
||||
Note: `agent-framework-monty` is an alpha package and is not yet part of
|
||||
`agent-framework[all]`. Install it explicitly with:
|
||||
|
||||
pip install agent-framework agent-framework-monty --pre
|
||||
|
||||
It is imported as `agent_framework_monty` (no lazy-loading namespace yet).
|
||||
"""
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_CYAN = "\033[36m"
|
||||
_YELLOW = "\033[33m"
|
||||
_GREEN = "\033[32m"
|
||||
_DIM = "\033[2m"
|
||||
_RESET = "\033[0m"
|
||||
|
||||
|
||||
class _ColoredFormatter(logging.Formatter):
|
||||
"""Dim logger output so it does not compete with sample prints."""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
return f"{_DIM}{super().format(record)}{_RESET}"
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logging.getLogger().handlers[0].setFormatter(
|
||||
_ColoredFormatter("[%(asctime)s] %(levelname)s: %(message)s"),
|
||||
)
|
||||
|
||||
|
||||
@function_middleware
|
||||
async def log_function_calls(
|
||||
context: FunctionInvocationContext,
|
||||
call_next: Callable[[], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Log tool calls, including readable execute_code blocks."""
|
||||
import time
|
||||
|
||||
function_name = context.function.name
|
||||
arguments = context.arguments if isinstance(context.arguments, dict) else {}
|
||||
|
||||
if function_name == "execute_code" and "code" in arguments:
|
||||
print(f"\n{_YELLOW}{'─' * 60}")
|
||||
print("▶ execute_code")
|
||||
print(f"{'─' * 60}{_RESET}")
|
||||
print(arguments["code"])
|
||||
print(f"{_YELLOW}{'─' * 60}{_RESET}")
|
||||
else:
|
||||
pairs = ", ".join(f"{name}={value!r}" for name, value in arguments.items())
|
||||
print(f"\n{_YELLOW}▶ {function_name}({pairs}){_RESET}")
|
||||
|
||||
start = time.perf_counter()
|
||||
await call_next()
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
result = context.result
|
||||
if function_name == "execute_code" and isinstance(result, list):
|
||||
for output in result:
|
||||
if output.type == "text" and output.text:
|
||||
print(f"{_GREEN}stdout:\n{output.text}{_RESET}")
|
||||
elif output.type == "error" and output.error_details:
|
||||
print(f"{_YELLOW}stderr:\n{output.error_details}{_RESET}")
|
||||
else:
|
||||
print(f"{_YELLOW}◀ {function_name} → {result!r}{_RESET}")
|
||||
|
||||
print(f"{_DIM} ({elapsed:.4f}s){_RESET}")
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def compute(
|
||||
operation: Annotated[
|
||||
Literal["add", "subtract", "multiply", "divide"],
|
||||
"Math operation: add, subtract, multiply, or divide.",
|
||||
],
|
||||
a: Annotated[float, "First numeric operand."],
|
||||
b: Annotated[float, "Second numeric operand."],
|
||||
) -> float:
|
||||
"""Perform a math operation for sandboxed code."""
|
||||
operations = {
|
||||
"add": a + b,
|
||||
"subtract": a - b,
|
||||
"multiply": a * b,
|
||||
"divide": a / b if b else float("inf"),
|
||||
}
|
||||
return operations[operation]
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
async def fetch_data(
|
||||
table: Annotated[str, "Name of the simulated table to query."],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Fetch records from a named table."""
|
||||
await asyncio.sleep(0.5)
|
||||
data: dict[str, list[dict[str, Any]]] = {
|
||||
"users": [
|
||||
{"id": 1, "name": "Alice", "role": "admin"},
|
||||
{"id": 2, "name": "Bob", "role": "user"},
|
||||
{"id": 3, "name": "Charlie", "role": "admin"},
|
||||
],
|
||||
"products": [
|
||||
{"id": 101, "name": "Widget", "price": 9.99},
|
||||
{"id": 102, "name": "Gadget", "price": 19.99},
|
||||
],
|
||||
}
|
||||
return data.get(table, [])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the provider-owned Monty CodeAct sample."""
|
||||
# 1. Create the Monty-backed provider and register sandbox tools on it.
|
||||
codeact = MontyCodeActProvider(
|
||||
tools=[compute, fetch_data],
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
# 2. Create the client and the agent.
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
name="MontyCodeActProviderAgent",
|
||||
instructions="You are a helpful assistant.",
|
||||
context_providers=[codeact],
|
||||
middleware=[log_function_calls],
|
||||
)
|
||||
|
||||
# 3. Run a request that should use execute_code plus provider-owned tools.
|
||||
query = (
|
||||
"Fetch all users, find admins, multiply 7*(3*2), and print the users, "
|
||||
"admins, and multiplication result. Use a single execute_code call. "
|
||||
"You may call the registered tools directly as typed async functions "
|
||||
"(`await compute(operation='multiply', a=7, b=6)`) or via "
|
||||
"`call_tool('compute', ...)`."
|
||||
)
|
||||
print(f"{_CYAN}{'=' * 60}")
|
||||
print("Monty CodeAct provider sample")
|
||||
print(f"{'=' * 60}{_RESET}")
|
||||
print(f"{_CYAN}User: {query}{_RESET}")
|
||||
result = await agent.run(query)
|
||||
print(f"{_CYAN}Agent: {result.text}{_RESET}")
|
||||
|
||||
|
||||
"""
|
||||
Sample output (shape only):
|
||||
|
||||
============================================================
|
||||
Monty CodeAct provider sample
|
||||
============================================================
|
||||
User: Fetch all users, find admins, multiply 7*(3*2), ...
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
▶ execute_code
|
||||
────────────────────────────────────────────────────────────
|
||||
users = await fetch_data(table="users")
|
||||
admins = [u for u in users if u["role"] == "admin"]
|
||||
result = await compute(operation="multiply", a=7, b=6)
|
||||
print("Users:", users)
|
||||
print("Admins:", admins)
|
||||
print("7 * 6 =", result)
|
||||
────────────────────────────────────────────────────────────
|
||||
stdout:
|
||||
Users: [...]
|
||||
Admins: [...]
|
||||
7 * 6 = 42.0
|
||||
(0.5xxx s)
|
||||
Agent: ...
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
# File Access Data Processing
|
||||
|
||||
This sample demonstrates how to give an `Agent` access to a folder of data files
|
||||
by attaching `FileAccessProvider` (backed by `FileSystemAgentFileStore`) as a
|
||||
context provider.
|
||||
|
||||
The agent is given a `working/` folder containing `sales.csv` — ~50 rows of
|
||||
sales transaction data — and is driven through a short scripted conversation
|
||||
that exercises every tool the provider exposes:
|
||||
|
||||
| Step | Prompt | Tool(s) used |
|
||||
|---|---|---|
|
||||
| 1 | "What files do you have access to?" | `file_access_ls` |
|
||||
| 2 | "Read sales.csv and summarize…" | `file_access_read` |
|
||||
| 3 | "Calculate the total revenue per region…" | (uses previously read data) |
|
||||
| 4 | "Save a markdown report named `region_totals.md`…" | `file_access_write` |
|
||||
| 5 | "List the files again so I can confirm…" | `file_access_ls` |
|
||||
|
||||
After the run, the sample prints the final contents of `working/` so the
|
||||
written file is easy to spot.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint. |
|
||||
| `FOUNDRY_MODEL` | Chat model deployment name (e.g. `gpt-4o`). |
|
||||
|
||||
Run `az login` before executing the sample so `AzureCliCredential` can
|
||||
authenticate.
|
||||
|
||||
## Running the sample
|
||||
|
||||
From `python/`:
|
||||
|
||||
```bash
|
||||
uv run --package agent-framework-core python samples/02-agents/context_providers/file_access_data_processing/data_processing.py
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
python samples/02-agents/context_providers/file_access_data_processing/data_processing.py
|
||||
```
|
||||
|
||||
## Sample data
|
||||
|
||||
`working/sales.csv` contains January–March 2025 sales transactions with these
|
||||
columns:
|
||||
|
||||
| Column | Description |
|
||||
|---|---|
|
||||
| `date` | Transaction date (YYYY-MM-DD) |
|
||||
| `product` | Product name |
|
||||
| `category` | Product category (Electronics, Furniture, Stationery) |
|
||||
| `quantity` | Units sold |
|
||||
| `unit_price` | Price per unit |
|
||||
| `region` | Sales region (North, South, West) |
|
||||
| `salesperson` | Name of the salesperson |
|
||||
|
||||
The sample writes `region_totals.md` into the same folder. Delete it between
|
||||
runs if you want a clean state.
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Sample: use ``FileAccessProvider`` to give an agent access to a folder of CSV data files.
|
||||
|
||||
This sample demonstrates how to attach :class:`FileAccessProvider` (backed by
|
||||
:class:`FileSystemAgentFileStore`) to an ``Agent`` so the model can read input
|
||||
data, perform analysis, and write summary output back to the same folder via
|
||||
the ``file_access_*`` tools.
|
||||
|
||||
The file-access tools all require approval (``approval_mode="always_require"``),
|
||||
so a base ``Agent`` installs :class:`ToolApprovalMiddleware` to drive the
|
||||
approval handshake. Because this sample is non-interactive, it auto-approves
|
||||
every file-access tool via
|
||||
:meth:`FileAccessProvider.all_tools_auto_approval_rule`.
|
||||
|
||||
The sibling ``working/`` folder contains ``sales.csv`` — ~50 rows of sales
|
||||
transactions (date, product, category, quantity, unit_price, region,
|
||||
salesperson). The agent is asked, in a single session, to: list available
|
||||
files, inspect the data, compute regional totals, and save a markdown summary.
|
||||
|
||||
Prerequisites:
|
||||
- ``FOUNDRY_PROJECT_ENDPOINT``: Your Azure AI Foundry project endpoint.
|
||||
- ``FOUNDRY_MODEL``: Chat model deployment name.
|
||||
- Run ``az login`` before executing the sample.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import Agent, FileAccessProvider, FileSystemAgentFileStore, ToolApprovalMiddleware
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load python/.env (python-dotenv walks up from this file by default). Pass
|
||||
# override=True so values from .env take precedence over any pre-existing OS
|
||||
# environment variables — without this, OS-level values silently win.
|
||||
load_dotenv(override=True)
|
||||
|
||||
INSTRUCTIONS = """
|
||||
You are a data analyst assistant. You have access to a folder of data files via
|
||||
the file_access_* tools.
|
||||
|
||||
## Getting started
|
||||
- Start by listing available files with file_access_ls to see what data
|
||||
is available. Files may be organized into subdirectories — use
|
||||
file_access_ls to discover folders and explore the tree level
|
||||
by level.
|
||||
- Read the files to understand their structure and contents.
|
||||
|
||||
## Working with data
|
||||
- When asked to analyze data, read the relevant files first, then perform the
|
||||
analysis.
|
||||
- Show your analysis clearly with tables, summaries, and key insights.
|
||||
- When calculations are needed, work through them step by step and show your
|
||||
reasoning.
|
||||
|
||||
## Writing output
|
||||
- When asked to produce output files (e.g., reports, summaries, filtered data),
|
||||
use file_access_write to write them.
|
||||
- Use appropriate file formats: CSV for tabular data, Markdown for reports.
|
||||
- Confirm what you wrote and where.
|
||||
|
||||
## Important
|
||||
- Never modify or delete the original input data files unless explicitly asked
|
||||
to do so.
|
||||
- If asked about data you haven't read yet, read it first before answering.
|
||||
- Always explain your reasoning between tool calls so the user can follow along.
|
||||
"""
|
||||
|
||||
PROMPTS = [
|
||||
"What files do you have access to?",
|
||||
"Read sales.csv and summarize what columns it contains and how many rows it has.",
|
||||
"Calculate the total revenue (quantity * unit_price) per region and show the result as a table.",
|
||||
(
|
||||
"Save a markdown report named region_totals.md that contains the regional totals "
|
||||
"and a one-paragraph summary of which region performed best."
|
||||
),
|
||||
"List the files again so I can confirm region_totals.md was created.",
|
||||
]
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1. Resolve the working directory bundled alongside this script.
|
||||
working_dir = Path(__file__).parent / "working"
|
||||
|
||||
# 2. Build the chat client.
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# 3. Wire up the file access provider against a file-system-backed store
|
||||
# rooted at the sample's working/ folder. The provider injects its
|
||||
# default instructions plus exposes six file_access_* tools to the
|
||||
# agent for the duration of each run.
|
||||
file_access = FileAccessProvider(store=FileSystemAgentFileStore(working_dir))
|
||||
|
||||
# 4. Create the agent and attach the provider. The file-access tools all
|
||||
# require approval (approval_mode="always_require"). Developers can
|
||||
# present these to the user for approval, or like in this case, auto-approve
|
||||
# them via FileAccessProvider.all_tools_auto_approval_rule. Note that
|
||||
# to use tool approval rules, the agent must have ToolApprovalMiddleware
|
||||
# in its middleware stack.
|
||||
async with Agent(
|
||||
client=client,
|
||||
name="DataAnalyst",
|
||||
description="A data analyst assistant that reads, analyzes, and processes data files.",
|
||||
instructions=INSTRUCTIONS,
|
||||
context_providers=[file_access],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[FileAccessProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
# 5. Run all prompts inside one session so the conversation remains
|
||||
# coherent across turns.
|
||||
session = agent.create_session()
|
||||
for prompt in PROMPTS:
|
||||
print(f"\nUser: {prompt}")
|
||||
response = await agent.run(prompt, session=session)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
# 6. Show the final folder contents so the side effects of the run are
|
||||
# visible to the reader.
|
||||
print("\nFinal contents of working/:")
|
||||
for path in sorted(working_dir.iterdir()):
|
||||
print(f" - {path.name} ({path.stat().st_size} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
# Sample output (truncated):
|
||||
#
|
||||
# User: What files do you have access to?
|
||||
# Assistant: I can see one file in the working directory: sales.csv.
|
||||
#
|
||||
# User: Read sales.csv and summarize what columns it contains and how many rows it has.
|
||||
# Assistant: sales.csv has 50 data rows and 7 columns: date, product, category,
|
||||
# quantity, unit_price, region, salesperson.
|
||||
#
|
||||
# User: Calculate the total revenue (quantity * unit_price) per region and show the result as a table.
|
||||
# Assistant:
|
||||
# | Region | Total Revenue |
|
||||
# |--------|---------------|
|
||||
# | North | $X,XXX.XX |
|
||||
# | South | $X,XXX.XX |
|
||||
# | West | $X,XXX.XX |
|
||||
#
|
||||
# User: Save a markdown report named region_totals.md ...
|
||||
# Assistant: I wrote region_totals.md to the working folder.
|
||||
#
|
||||
# User: List the files again so I can confirm region_totals.md was created.
|
||||
# Assistant: The working folder now contains: region_totals.md, sales.csv.
|
||||
#
|
||||
# Final contents of working/:
|
||||
# - region_totals.md (NNN bytes)
|
||||
# - sales.csv (3175 bytes)
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
date,product,category,quantity,unit_price,region,salesperson
|
||||
2025-01-03,Laptop Pro 15,Electronics,2,1299.99,North,Alice
|
||||
2025-01-05,Ergonomic Chair,Furniture,5,349.50,South,Bob
|
||||
2025-01-07,Wireless Mouse,Electronics,12,24.99,North,Alice
|
||||
2025-01-08,Standing Desk,Furniture,1,599.00,West,Carol
|
||||
2025-01-10,USB-C Hub,Electronics,8,45.99,North,David
|
||||
2025-01-12,Monitor 27in,Electronics,3,429.00,South,Bob
|
||||
2025-01-14,Desk Lamp,Furniture,6,79.95,West,Carol
|
||||
2025-01-15,Keyboard Mech,Electronics,4,149.99,North,Alice
|
||||
2025-01-17,Filing Cabinet,Furniture,2,189.00,South,David
|
||||
2025-01-20,Webcam HD,Electronics,10,89.99,West,Bob
|
||||
2025-01-22,Laptop Pro 15,Electronics,1,1299.99,South,Carol
|
||||
2025-01-24,Ergonomic Chair,Furniture,3,349.50,North,Alice
|
||||
2025-01-25,Notebook Pack,Stationery,20,12.99,South,David
|
||||
2025-01-27,Wireless Mouse,Electronics,15,24.99,West,Carol
|
||||
2025-01-28,Whiteboard,Stationery,4,129.00,North,Bob
|
||||
2025-01-30,Standing Desk,Furniture,2,599.00,South,Alice
|
||||
2025-02-02,USB-C Hub,Electronics,6,45.99,West,David
|
||||
2025-02-04,Monitor 27in,Electronics,2,429.00,North,Carol
|
||||
2025-02-05,Desk Lamp,Furniture,8,79.95,South,Bob
|
||||
2025-02-07,Keyboard Mech,Electronics,5,149.99,West,Alice
|
||||
2025-02-09,Filing Cabinet,Furniture,1,189.00,North,David
|
||||
2025-02-11,Webcam HD,Electronics,7,89.99,South,Carol
|
||||
2025-02-13,Laptop Pro 15,Electronics,3,1299.99,West,Bob
|
||||
2025-02-15,Notebook Pack,Stationery,30,12.99,North,Alice
|
||||
2025-02-17,Ergonomic Chair,Furniture,4,349.50,South,David
|
||||
2025-02-19,Wireless Mouse,Electronics,20,24.99,North,Carol
|
||||
2025-02-20,Whiteboard,Stationery,2,129.00,West,Bob
|
||||
2025-02-22,Standing Desk,Furniture,1,599.00,North,Alice
|
||||
2025-02-24,USB-C Hub,Electronics,10,45.99,South,David
|
||||
2025-02-26,Monitor 27in,Electronics,4,429.00,West,Carol
|
||||
2025-02-28,Desk Lamp,Furniture,3,79.95,North,Bob
|
||||
2025-03-02,Keyboard Mech,Electronics,6,149.99,South,Alice
|
||||
2025-03-04,Filing Cabinet,Furniture,3,189.00,West,David
|
||||
2025-03-06,Webcam HD,Electronics,9,89.99,North,Carol
|
||||
2025-03-08,Laptop Pro 15,Electronics,2,1299.99,South,Bob
|
||||
2025-03-10,Notebook Pack,Stationery,25,12.99,West,Alice
|
||||
2025-03-12,Ergonomic Chair,Furniture,6,349.50,North,David
|
||||
2025-03-14,Wireless Mouse,Electronics,18,24.99,South,Carol
|
||||
2025-03-15,Whiteboard,Stationery,5,129.00,North,Bob
|
||||
2025-03-17,Standing Desk,Furniture,3,599.00,West,Alice
|
||||
2025-03-19,USB-C Hub,Electronics,7,45.99,North,David
|
||||
2025-03-21,Monitor 27in,Electronics,5,429.00,South,Carol
|
||||
2025-03-23,Desk Lamp,Furniture,4,79.95,West,Bob
|
||||
2025-03-25,Keyboard Mech,Electronics,3,149.99,North,Alice
|
||||
2025-03-27,Filing Cabinet,Furniture,2,189.00,South,David
|
||||
2025-03-28,Webcam HD,Electronics,11,89.99,West,Carol
|
||||
2025-03-29,Laptop Pro 15,Electronics,1,1299.99,North,Bob
|
||||
2025-03-30,Notebook Pack,Stationery,15,12.99,South,Alice
|
||||
2025-03-31,Ergonomic Chair,Furniture,2,349.50,West,David
|
||||
|
@@ -0,0 +1,47 @@
|
||||
# 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_sessions.py`](mem0_sessions.py) | Example demonstrating different memory scoping strategies with Mem0. Covers user-scoped memory (memories shared across all sessions for the same user), agent-scoped memory (memories isolated per agent), 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:**
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI project endpoint
|
||||
- `FOUNDRY_MODEL`: The name of your model deployment
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Memory Scoping
|
||||
|
||||
The Mem0 context provider supports scoping via identifiers:
|
||||
|
||||
- **User scope** (`user_id`): Associate memories with a specific user, shared across all sessions
|
||||
- **Agent scope** (`agent_id`): Isolate memories per agent persona
|
||||
- **Application scope** (`application_id`): Associate memories with an application context
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.mem0 import Mem0ContextProvider
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# 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_sessions.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.
|
||||
# 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,
|
||||
Agent(
|
||||
client=FoundryChatClient(credential=credential),
|
||||
name="FriendlyAssistant",
|
||||
instructions="You are a friendly assistant.",
|
||||
tools=retrieve_company_report,
|
||||
context_providers=[Mem0ContextProvider(source_id="mem0", 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(15) # Empirically determined delay for Mem0 indexing
|
||||
print("\nRequest within a new session:")
|
||||
# Create a new session for the agent.
|
||||
# The new session has no context of the previous conversation.
|
||||
session = agent.create_session()
|
||||
# Since we have the mem0 component in the session, 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, session=session)
|
||||
print(f"Agent: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.mem0 import Mem0ContextProvider
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from mem0 import AsyncMemory
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# 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_sessions.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.
|
||||
# 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,
|
||||
Agent(
|
||||
client=FoundryChatClient(credential=credential),
|
||||
name="FriendlyAssistant",
|
||||
instructions="You are a friendly assistant.",
|
||||
tools=retrieve_company_report,
|
||||
context_providers=[Mem0ContextProvider(source_id="mem0", 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("\nRequest within a new session:")
|
||||
# Create a new session for the agent.
|
||||
# The new session has no context of the previous conversation.
|
||||
session = agent.create_session()
|
||||
# Since we have the mem0 component in the session, 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.
|
||||
result = await agent.run(query, session=session)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,170 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.mem0 import Mem0ContextProvider
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# 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_sessions.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_user_scoped_memory() -> None:
|
||||
"""Example 1: User-scoped memory (memories shared across all sessions for the same user)."""
|
||||
print("1. User-Scoped Memory Example:")
|
||||
print("-" * 40)
|
||||
|
||||
user_id = "user123"
|
||||
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
Agent(
|
||||
client=FoundryChatClient(credential=credential),
|
||||
name="UserMemoryAssistant",
|
||||
instructions="You are an assistant that remembers user preferences across conversations.",
|
||||
tools=get_user_preferences,
|
||||
context_providers=[
|
||||
Mem0ContextProvider(
|
||||
source_id="mem0",
|
||||
user_id=user_id,
|
||||
)
|
||||
],
|
||||
) as user_agent,
|
||||
):
|
||||
# Store some preferences
|
||||
query = "Remember that I prefer technical responses with code examples when discussing programming."
|
||||
print(f"User: {query}")
|
||||
result = await user_agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Create a new session - memories should still be accessible via user_id scoping
|
||||
new_session = user_agent.create_session()
|
||||
query = "What do you know about my preferences?"
|
||||
print(f"User (new session): {query}")
|
||||
result = await user_agent.run(query, session=new_session)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
|
||||
async def example_agent_scoped_memory() -> None:
|
||||
"""Example 2: Agent-scoped memory (memories isolated per agent_id).
|
||||
|
||||
Note: Use different agent_id values to isolate memories between different
|
||||
agent personas, even when the user_id is the same.
|
||||
"""
|
||||
print("2. Agent-Scoped Memory Example:")
|
||||
print("-" * 40)
|
||||
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
Agent(
|
||||
client=FoundryChatClient(credential=credential),
|
||||
name="ScopedMemoryAssistant",
|
||||
instructions="You are an assistant with agent-scoped memory.",
|
||||
tools=get_user_preferences,
|
||||
context_providers=[
|
||||
Mem0ContextProvider(
|
||||
source_id="mem0",
|
||||
agent_id="scoped_assistant",
|
||||
)
|
||||
],
|
||||
) as scoped_agent,
|
||||
):
|
||||
query = (
|
||||
"Remember that I'm working on a Python project about data analysis "
|
||||
"and I prefer using pandas and matplotlib."
|
||||
)
|
||||
print(f"User: {query}")
|
||||
result = await scoped_agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
new_session = scoped_agent.create_session()
|
||||
query = "What do you know about my current project and preferences?"
|
||||
print(f"User (new session): {query}")
|
||||
result = await scoped_agent.run(query, session=new_session)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
|
||||
async def example_multiple_agents() -> None:
|
||||
"""Example 3: Multiple agents with different memory configurations."""
|
||||
print("3. Multiple Agents with Different Memory Configurations:")
|
||||
print("-" * 40)
|
||||
|
||||
agent_id_1 = "agent_personal"
|
||||
agent_id_2 = "agent_work"
|
||||
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
Agent(
|
||||
client=FoundryChatClient(credential=credential),
|
||||
name="PersonalAssistant",
|
||||
instructions="You are a personal assistant that helps with personal tasks.",
|
||||
context_providers=[
|
||||
Mem0ContextProvider(
|
||||
source_id="mem0",
|
||||
agent_id=agent_id_1,
|
||||
)
|
||||
],
|
||||
) as personal_agent,
|
||||
Agent(
|
||||
client=FoundryChatClient(credential=credential),
|
||||
name="WorkAssistant",
|
||||
instructions="You are a work assistant that helps with professional tasks.",
|
||||
context_providers=[
|
||||
Mem0ContextProvider(
|
||||
source_id="mem0",
|
||||
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 memory management examples."""
|
||||
print("=== Mem0 Memory Management Example ===\n")
|
||||
|
||||
await example_user_scoped_memory()
|
||||
await example_agent_scoped_memory()
|
||||
await example_multiple_agents()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,19 @@
|
||||
# Neo4j Context Providers
|
||||
|
||||
Neo4j offers two context providers for the Agent Framework, each serving a different purpose:
|
||||
|
||||
| | [Neo4j Memory](../neo4j_memory/README.md) | [Neo4j GraphRAG](../../../05-end-to-end/neo4j_graphrag/README.md) |
|
||||
|---|---|---|
|
||||
| **What it does** | Read-write memory — stores conversations, builds knowledge graphs, learns from interactions | Read-only retrieval from a pre-existing knowledge base with optional graph traversal |
|
||||
| **Data source** | Agent interactions (grows over time) | Pre-loaded documents and indexes |
|
||||
| **Python package** | [`neo4j-agent-memory`](https://pypi.org/project/neo4j-agent-memory/) | [`agent-framework-neo4j`](https://pypi.org/project/agent-framework-neo4j/) |
|
||||
| **Database setup** | Empty — creates its own schema | Requires pre-indexed documents with vector or fulltext indexes |
|
||||
| **Example use case** | "Remember my preferences", "What did we discuss last time?" | "Search our documents", "What risks does Acme Corp face?" |
|
||||
|
||||
## Which should I use?
|
||||
|
||||
**Use [Neo4j Memory](../neo4j_memory/README.md)** when your agent needs to remember things across sessions — user preferences, past conversations, extracted entities, and reasoning traces. The memory provider writes to the database on every interaction, building a knowledge graph that grows over time.
|
||||
|
||||
**Use [Neo4j GraphRAG](../../../05-end-to-end/neo4j_graphrag/README.md)** when your agent needs to search an existing knowledge base — documents, articles, product catalogs — and optionally enrich results by traversing graph relationships. The GraphRAG provider is read-only and does not modify your data.
|
||||
|
||||
You can use both together: GraphRAG for domain knowledge retrieval, Memory for personalization and learning.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Neo4j Memory Context Provider
|
||||
|
||||
[Neo4j Agent Memory](https://github.com/neo4j-labs/agent-memory) is a graph-native memory system for AI agents that stores conversations, builds knowledge graphs from interactions, and lets agents learn from their own reasoning — all backed by Neo4j.
|
||||
|
||||
For full documentation, installation instructions, code examples, and configuration details, see the [Neo4j Memory integration guide on Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/neo4j-memory).
|
||||
|
||||
For a runnable example, see the [retail assistant sample](https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant).
|
||||
|
||||
For help choosing between the Memory and GraphRAG providers, see the [Neo4j Context Providers overview](../neo4j/README.md).
|
||||
@@ -0,0 +1,123 @@
|
||||
# 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 RedisHistoryProvider 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 RedisContextProvider using traditional connection string authentication. |
|
||||
| [`redis_sessions.py`](redis_sessions.py) | Demonstrates memory scoping strategies. Includes: (1) global memory scope with `application_id`, `agent_id`, and `user_id` shared across operations; (2) hybrid vector search using a custom OpenAI vectorizer for richer context retrieval; 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. Azure AI Foundry project endpoint and Azure OpenAI Responses deployment
|
||||
4. 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
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT` (required): Azure AI Foundry project endpoint for `FoundryChatClient`
|
||||
- `FOUNDRY_MODEL` (required): Foundry model deployment name
|
||||
- `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`.
|
||||
- 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 `FoundryChatClient` for chat and, in some steps, optional OpenAI 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 Azure Foundry/OpenAI responses environment variables:
|
||||
|
||||
```bash
|
||||
export FOUNDRY_PROJECT_ENDPOINT="https://<resource>.services.ai.azure.com/api/projects/<project>"
|
||||
export FOUNDRY_MODEL="<deployment-name>"
|
||||
```
|
||||
|
||||
3) (Optional) Set your OpenAI key if using embeddings:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="<your key>"
|
||||
```
|
||||
|
||||
4) 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`, or `user_id` on the provider to filter memory.
|
||||
- Agent isolation: use different `agent_id` values to keep memories separated for different agent personas.
|
||||
|
||||
### 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`, or `user_id` is set; the provider requires a scope.
|
||||
- Verify `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL` are set for the chat client.
|
||||
- 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,140 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Azure Managed Redis History Provider with Azure AD Authentication
|
||||
|
||||
This example demonstrates how to use Azure Managed Redis with Azure AD authentication
|
||||
to persist conversation history using RedisHistoryProvider.
|
||||
|
||||
Key concepts:
|
||||
- RedisHistoryProvider = durable storage (where messages are persisted)
|
||||
- AgentSession = conversation identity (which conversation the messages belong to)
|
||||
|
||||
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)
|
||||
- FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
|
||||
- FOUNDRY_MODEL: Azure OpenAI Responses deployment name
|
||||
- AZURE_USER_OBJECT_ID: Your Azure AD User Object ID for authentication
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.redis import RedisHistoryProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from redis.credentials import CredentialProvider
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class AzureCredentialProvider(CredentialProvider):
|
||||
"""Credential provider for Azure AD authentication with Redis Enterprise."""
|
||||
|
||||
def __init__(self, azure_credential: AsyncAzureCliCredential, 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
|
||||
|
||||
# 1. Create Azure CLI credential provider (uses 'az login' credentials)
|
||||
azure_credential = AsyncAzureCliCredential()
|
||||
credential_provider = AzureCredentialProvider(azure_credential, user_object_id)
|
||||
|
||||
# 2. Create Azure Redis history provider (the durable storage backend)
|
||||
history_provider = RedisHistoryProvider(
|
||||
source_id="redis_memory",
|
||||
credential_provider=credential_provider,
|
||||
host=redis_host,
|
||||
port=10000,
|
||||
ssl=True,
|
||||
key_prefix="chat_messages",
|
||||
max_messages=100,
|
||||
)
|
||||
|
||||
# 3. Create chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# 4. Create agent with Azure Redis history provider
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="AzureRedisAssistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
context_providers=[history_provider],
|
||||
)
|
||||
|
||||
# 5. Create a session to provide conversation identity.
|
||||
# The session ID is used as the Redis key — all runs sharing the same session
|
||||
# will read/write the same conversation history in Redis.
|
||||
session = agent.create_session()
|
||||
|
||||
# 6. Conversation — each run passes the same session for continuity
|
||||
query = "Remember that I enjoy gumbo"
|
||||
result = await agent.run(query, session=session)
|
||||
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, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "What did I say to you just now?"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "Remember that I have a meeting at 3pm tomorrow"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "Tulips are red"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "What was the first thing I said to you this conversation?"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Cleanup
|
||||
await azure_credential.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,281 @@
|
||||
# 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 Agent, Message, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.redis import RedisContextProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Default Redis URL for local Redis Stack.
|
||||
# Override via the REDIS_URL environment variable for remote or authenticated instances.
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
|
||||
|
||||
# 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_sessions.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']}."
|
||||
)
|
||||
|
||||
|
||||
def create_chat_client() -> FoundryChatClient:
|
||||
"""Create a FoundryChatClient using a Foundry project endpoint."""
|
||||
return FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
|
||||
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 OPENAI_API_KEY to use the OpenAI vectorizer.
|
||||
# For chat responses, also set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL.
|
||||
|
||||
# We attach an embedding vectorizer so the provider can perform hybrid (text + vector)
|
||||
# retrieval. If you prefer text-only retrieval, instantiate RedisContextProvider 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_URL),
|
||||
)
|
||||
# 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 = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
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"]),
|
||||
]
|
||||
|
||||
# Use the provider's before_run/after_run API to store and retrieve messages.
|
||||
# In practice, the agent handles this automatically; this shows the low-level API.
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import AgentSession, SessionContext, SupportsAgentRun
|
||||
|
||||
session = AgentSession(session_id="runA")
|
||||
context = SessionContext(input_messages=messages)
|
||||
state = session.state
|
||||
|
||||
# Store messages via after_run
|
||||
await provider.after_run(agent=cast(SupportsAgentRun, None), session=session, context=context, state=state)
|
||||
|
||||
# Retrieve relevant memories via before_run
|
||||
query_context = SessionContext(input_messages=[Message("system", ["B: Assistant Message"])])
|
||||
await provider.before_run(agent=cast(SupportsAgentRun, None), session=session, context=query_context, state=state)
|
||||
|
||||
# Inspect retrieved memories that would be injected into instructions
|
||||
# (Debug-only output so you can verify retrieval works as expected.)
|
||||
print("Before Run Result:")
|
||||
print(query_context)
|
||||
|
||||
# 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_URL),
|
||||
)
|
||||
# Recreate a clean index so the next scenario starts fresh
|
||||
provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
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 = create_chat_client()
|
||||
# Create agent wired to the Redis context provider. The provider automatically
|
||||
# persists conversational details and surfaces relevant context on each turn.
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=[provider],
|
||||
)
|
||||
|
||||
# Teach a user preference; the agent writes this to the provider's memory
|
||||
query = "Remember that I enjoy glugenflorgle"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Ask the agent to recall the stored preference; it should retrieve from memory
|
||||
query = "What do I enjoy?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
# --- Agent + provider + tool: store and recall tool-derived context ---
|
||||
|
||||
print("\n3. Agent + provider + tool: store and recall tool-derived context")
|
||||
print("-" * 40)
|
||||
# Text-only provider (full-text search only). Omits vectorizer and related params.
|
||||
provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
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 = create_chat_client()
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=search_flights,
|
||||
context_providers=[provider],
|
||||
)
|
||||
# Invoke the tool; outputs become part of memory/context
|
||||
query = "Are there any flights from new york city (jfk) to la? Give me details"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
# Verify the agent can recall tool-derived context
|
||||
query = "Which flight did I ask about?"
|
||||
result = await agent.run(query)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
# Drop / delete the provider index in Redis
|
||||
await provider.redis_index.delete()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,124 @@
|
||||
# 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
|
||||
conversational details. Pass it as a constructor argument to create_agent.
|
||||
|
||||
Note: For session history persistence, see RedisHistoryProvider in the
|
||||
conversations/redis_history_provider.py sample. RedisContextProvider is for
|
||||
AI context (RAG, memories), while RedisHistoryProvider stores message history.
|
||||
|
||||
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 import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.redis import RedisContextProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Default Redis URL for local Redis Stack.
|
||||
# Override via the REDIS_URL environment variable for remote or authenticated instances.
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
|
||||
|
||||
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_URL),
|
||||
)
|
||||
|
||||
provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
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",
|
||||
)
|
||||
|
||||
# Create chat client for the agent
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
# Create agent wired to the Redis context provider. The provider automatically
|
||||
# persists conversational details and surfaces relevant context on each turn.
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="MemoryEnhancedAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=[provider],
|
||||
)
|
||||
|
||||
# Create a session to manage conversation state
|
||||
session = agent.create_session()
|
||||
|
||||
# Teach a user preference; the agent writes this to the provider's memory
|
||||
query = "Remember that I enjoy gumbo"
|
||||
result = await agent.run(query, session=session)
|
||||
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, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "What did I say to you just now?"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "Remember that I have a meeting at 3pm tomorro"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "Tulips are red"
|
||||
result = await agent.run(query, session=session)
|
||||
print("User: ", query)
|
||||
print("Agent: ", result)
|
||||
|
||||
query = "What was the first thing I said to you this conversation?"
|
||||
result = await agent.run(query, session=session)
|
||||
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,257 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Redis Context Provider: Memory scoping examples
|
||||
|
||||
This sample demonstrates how conversational memory can be scoped when using the
|
||||
Redis context provider. It covers three scenarios:
|
||||
|
||||
1) Global memory scope
|
||||
- Use application_id, agent_id, and user_id to share memories across
|
||||
all operations/sessions.
|
||||
|
||||
2) Hybrid vector search
|
||||
- Use a custom OpenAI vectorizer with the provider for hybrid vector search.
|
||||
Demonstrates combining full-text and semantic search for richer context
|
||||
retrieval.
|
||||
|
||||
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_sessions.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.redis import RedisContextProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from redisvl.extensions.cache.embeddings import EmbeddingsCache
|
||||
from redisvl.utils.vectorize import OpenAITextVectorizer
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Default Redis URL for local Redis Stack.
|
||||
# Override via the REDIS_URL environment variable for remote or authenticated instances.
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
|
||||
|
||||
# Please set OPENAI_API_KEY to use the OpenAI vectorizer.
|
||||
# For chat responses, also set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL.
|
||||
def create_chat_client() -> FoundryChatClient:
|
||||
"""Create a FoundryChatClient using a Foundry project endpoint."""
|
||||
return FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
|
||||
async def example_global_memory_scope() -> None:
|
||||
"""Example 1: Global memory scope (memories shared across all operations)."""
|
||||
print("1. Global Memory Scope Example:")
|
||||
print("-" * 40)
|
||||
|
||||
client = create_chat_client()
|
||||
|
||||
provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
index_name="redis_threads_global",
|
||||
application_id="threads_demo_app",
|
||||
agent_id="threads_demo_agent",
|
||||
user_id="threads_demo_user",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="GlobalMemoryAssistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Personalize replies using provided context. "
|
||||
"Before answering, always check for stored context containing information"
|
||||
),
|
||||
tools=[],
|
||||
context_providers=[provider],
|
||||
)
|
||||
|
||||
# Store a preference in the global scope
|
||||
query = "Remember that I prefer technical responses with code examples when discussing programming."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Create a new session - memories should still be accessible due to global scope
|
||||
new_session = agent.create_session()
|
||||
query = "What technical responses do I prefer?"
|
||||
print(f"User (new session): {query}")
|
||||
result = await agent.run(query, session=new_session)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Clean up the Redis index
|
||||
await provider.redis_index.delete()
|
||||
|
||||
|
||||
async def example_hybrid_vector_search() -> None:
|
||||
"""Example 2: Hybrid vector search with custom vectorizer.
|
||||
|
||||
Demonstrates using a custom OpenAI vectorizer for hybrid vector search,
|
||||
combining full-text and semantic search for richer context retrieval.
|
||||
"""
|
||||
print("2. Hybrid Vector Search Example:")
|
||||
print("-" * 40)
|
||||
|
||||
client = create_chat_client()
|
||||
|
||||
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_URL),
|
||||
)
|
||||
|
||||
provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
index_name="redis_threads_dynamic",
|
||||
application_id="threads_demo_app",
|
||||
agent_id="threads_demo_agent",
|
||||
user_id="threads_demo_user",
|
||||
redis_vectorizer=vectorizer,
|
||||
vector_field_name="vector",
|
||||
vector_algorithm="hnsw",
|
||||
vector_distance_metric="cosine",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="HybridSearchAssistant",
|
||||
instructions="You are an assistant with hybrid vector search for richer context retrieval.",
|
||||
context_providers=[provider],
|
||||
)
|
||||
|
||||
# Store some information
|
||||
query = "Remember that for this conversation, I'm working on a Python project about data analysis."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Test memory retrieval via hybrid search
|
||||
query = "What project am I working on?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Store more information
|
||||
query = "Also remember that I prefer using pandas and matplotlib for this project."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Test comprehensive memory retrieval
|
||||
query = "What do you know about my current project and preferences?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
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 memory configurations (isolated via agent_id) but within 1 index."""
|
||||
print("3. Multiple Agents with Different Memory Configurations:")
|
||||
print("-" * 40)
|
||||
|
||||
client = create_chat_client()
|
||||
|
||||
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_URL),
|
||||
)
|
||||
|
||||
personal_provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
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 = Agent(
|
||||
client=client,
|
||||
name="PersonalAssistant",
|
||||
instructions="You are a personal assistant that helps with personal tasks.",
|
||||
context_providers=[personal_provider],
|
||||
)
|
||||
|
||||
work_provider = RedisContextProvider(
|
||||
source_id="redis_context",
|
||||
redis_url=REDIS_URL,
|
||||
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 = Agent(
|
||||
client=client,
|
||||
name="WorkAssistant",
|
||||
instructions="You are a work assistant that helps with professional tasks.",
|
||||
context_providers=[work_provider],
|
||||
)
|
||||
|
||||
# Store personal information
|
||||
query = "Remember that I like to exercise at 6 AM and prefer outdoor activities."
|
||||
print(f"User to Personal Agent: {query}")
|
||||
result = await personal_agent.run(query)
|
||||
print(f"Personal Agent: {result}\n")
|
||||
|
||||
# Store work information
|
||||
query = "Remember that I have team meetings every Tuesday at 2 PM."
|
||||
print(f"User to Work Agent: {query}")
|
||||
result = await work_agent.run(query)
|
||||
print(f"Work Agent: {result}\n")
|
||||
|
||||
# Test memory isolation
|
||||
query = "What do you know about my schedule?"
|
||||
print(f"User to Personal Agent: {query}")
|
||||
result = await personal_agent.run(query)
|
||||
print(f"Personal Agent: {result}\n")
|
||||
|
||||
print(f"User to Work Agent: {query}")
|
||||
result = await work_agent.run(query)
|
||||
print(f"Work Agent: {result}\n")
|
||||
|
||||
# Clean up the Redis index (shared)
|
||||
await work_provider.redis_index.delete()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Redis Memory Scoping Examples ===\n")
|
||||
await example_global_memory_scope()
|
||||
await example_hybrid_vector_search()
|
||||
await example_multiple_agents()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import Agent, AgentSession, ContextProvider, SessionContext, SupportsChatGetResponse
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
name: str | None = None
|
||||
age: int | None = None
|
||||
|
||||
|
||||
class UserInfoMemory(ContextProvider):
|
||||
DEFAULT_SOURCE_ID = "user_info_memory"
|
||||
|
||||
def __init__(self, source_id: str = DEFAULT_SOURCE_ID, *, client: SupportsChatGetResponse, **kwargs: Any):
|
||||
"""Create the memory.
|
||||
|
||||
If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
|
||||
"""
|
||||
super().__init__(source_id)
|
||||
self._chat_client = client
|
||||
|
||||
async def after_run(
|
||||
self,
|
||||
*,
|
||||
agent: Any,
|
||||
session: AgentSession | None,
|
||||
context: SessionContext,
|
||||
state: dict[str, Any],
|
||||
) -> None:
|
||||
"""Extract user information from messages after each agent call."""
|
||||
# ensure you get all the messages you want to parse from, including the input in this case.
|
||||
request_messages = context.get_messages(include_input=True, include_response=True)
|
||||
# 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 (state["user_info"].name is None or state["user_info"].age is None) and user_messages:
|
||||
with suppress(Exception):
|
||||
# Use the chat client to extract structured information
|
||||
result = await self._chat_client.get_response(
|
||||
messages=request_messages, # type: ignore
|
||||
options={
|
||||
"instructions": "Extract the user's name and age from the message if present. "
|
||||
"If not present return nulls.",
|
||||
"response_format": UserInfo,
|
||||
},
|
||||
)
|
||||
|
||||
# Update user info with extracted data
|
||||
with suppress(Exception):
|
||||
extracted = result.value
|
||||
user_info = state["user_info"]
|
||||
if not isinstance(extracted, UserInfo) or not isinstance(user_info, UserInfo):
|
||||
return
|
||||
if user_info.name is None and extracted.name:
|
||||
user_info.name = extracted.name
|
||||
if user_info.age is None and extracted.age:
|
||||
user_info.age = extracted.age
|
||||
|
||||
async def before_run(
|
||||
self,
|
||||
*,
|
||||
agent: Any,
|
||||
session: AgentSession | None,
|
||||
context: SessionContext,
|
||||
state: dict[str, Any],
|
||||
) -> None:
|
||||
"""Provide user information context before each agent call."""
|
||||
state.setdefault("user_info", UserInfo())
|
||||
|
||||
context.extend_instructions(
|
||||
self.source_id,
|
||||
"Ask the user for their name and politely decline to answer any questions until they provide it."
|
||||
if state["user_info"].name is None
|
||||
else f"The user's name is {state['user_info'].name}.",
|
||||
)
|
||||
context.extend_instructions(
|
||||
self.source_id,
|
||||
"Ask the user for their age and politely decline to answer any questions until they provide it."
|
||||
if state["user_info"].age is None
|
||||
else f"The user's age is {state['user_info'].age}.",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
context_name = UserInfoMemory.DEFAULT_SOURCE_ID
|
||||
|
||||
# Create the memory provider
|
||||
memory_provider = UserInfoMemory(context_name, client=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_providers=[memory_provider],
|
||||
) as agent:
|
||||
# Create a new session for the conversation
|
||||
session = agent.create_session()
|
||||
|
||||
for msg in ["Hello, what is the square root of 9?", "My name is Ruaidhrí", "I am 20 years old"]:
|
||||
print(f"User: {msg}")
|
||||
print(f"Assistant: {await agent.run(msg, session=session)}")
|
||||
|
||||
# Access the memory component and inspect the memories
|
||||
print()
|
||||
print(f"MEMORY - User Name: {session.state[context_name]['user_info'].name}")
|
||||
print(f"MEMORY - User Age: {session.state[context_name]['user_info'].age}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user