Deploy Microsoft Agent Framework agents as Azure Durable Function Apps using Durable Entities
Project description
Agent Framework using Durable Functions (Python)
A Python implementation for building stateful, durable AI agents using Microsoft Agent Framework and Azure Durable Functions. This implementation leverages Durable Entities to provide automatic state persistence, conversation history tracking, and reliable agent execution.
Features
Core Capabilities
- Stateful Agent Execution: Agents run inside Durable Entities with automatic state persistence
- Conversation History: Full conversation history tracking per conversation ID
- Durable by Design: Built-in reliability, automatic retries, and error handling
- Zero Boilerplate: Simple API - just define your agent and tools
- Full Azure Functions Integration: Inherits from DFApp, so you can add timers, queues, blobs, etc.
- Production Ready: Built on Azure Durable Functions for enterprise-scale reliability
Architecture Highlights
- Entity-Based Pattern: Uses Durable Entities (not orchestrations) for better state management
- Direct Execution: Agents run directly inside entities - no middleware layer needed
- Conversation Retrieval: GET endpoint to retrieve conversation state by ID
- Native Agent Framework Types: Uses
ChatMessageandAgentRunResponseobjects for type safety - Automatic Serialization: State is automatically persisted and restored using agent_framework's
to_dict()/from_dict() - Signal-Based Operations: Efficient entity signaling for agent execution
Quick Start
Prerequisites
- Python 3.10+
- Azure Functions Core Tools v4
- Azure subscription (for deployment)
Installation
pip install agent-framework-azurefunctions
Or add to your requirements.txt:
agent-framework-azurefunctions
For development installation from source:
pip install -e .
Create Your First Agent
1. Create a new Azure Functions project
func init my-agent-app --python
cd my-agent-app
2. Create function_app.py
import os
from durableagent import AgentFunctionApp
from agent_framework.azure import AzureOpenAIAssistantsClient
from azure.identity import AzureCliCredential
# Define your tools (optional)
def get_weather(location: str) -> dict:
"""Get the weather for a location."""
return {
"location": location,
"temperature": 72,
"conditions": "Sunny"
}
# Create your agent
agent = AzureOpenAIAssistantsClient(
endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4"),
credential=AzureCliCredential(),
).create_agent(
name="WeatherAgent",
instructions="You are a helpful weather assistant.",
tools=[get_weather],
)
# Create the function app - that's it!
app = AgentFunctionApp(agents=[agent])
3. Configure environment variables
Create a .env file or set in local.settings.json:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python",
"AZURE_OPENAI_ENDPOINT": "<https://your-endpoint.openai.azure.com>",
"AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4"
}
}
4. Run locally
func start
5. Test your agent
curl -X POST http://localhost:7071/api/agents/WeatherAgent/run \
-H "Content-Type: application/json" \
-d '{
"message": "What is the weather like in Seattle?",
"sessionId": "user-123"
}'
Response:
{
"response": "The weather in Seattle is currently 72°F and sunny.",
"message": "What is the weather like in Seattle?",
"sessionId": "user-123",
"status": "success",
"message_count": 1
}
6. Retrieve conversation state
curl http://localhost:7071/api/agents/WeatherAgent/user-123
Response:
{
"message_count": 1,
"conversation_history": [
{
"role": "user",
"content": "What is the weather like in Seattle?",
"timestamp": "2025-01-15T10:30:00Z"
},
{
"role": "assistant",
"content": "The weather in Seattle is currently 72°F and sunny.",
"timestamp": "2025-01-15T10:30:05Z",
"agent_response": {
"text": "The weather in Seattle is currently 72°F and sunny.",
"kind": "agent_message",
"tool_calls": [],
"usage": {
"prompt_tokens": 45,
"completion_tokens": 12,
"total_tokens": 57
}
}
}
],
"last_response": "The weather in Seattle is currently 72°F and sunny.",
"agent_type": "Agent"
}
Note:
- Conversation history uses native
ChatMessageobjects from agent_framework - Assistant messages include full
AgentRunResponsemetadata inadditional_properties.agent_response - This preserves all framework details:
kind,tool_calls,usage(tokens), and other fields - The state is serialized using agent_framework's
to_dict()and restored usingfrom_dict()
Multi-Agent Support
The framework supports multiple agents in a single app, giving you two flexible options:
Option 1: Pass a list of agents during initialization
from durableagent import AgentFunctionApp
from agent_framework.azure import AzureOpenAIAssistantsClient
# Create multiple agents with unique names
weather_agent = AzureOpenAIAssistantsClient(...).create_agent(
name="WeatherAgent",
instructions="You are a helpful weather assistant.",
tools=[get_weather],
)
math_agent = AzureOpenAIAssistantsClient(...).create_agent(
name="MathAgent",
instructions="You are a helpful math assistant.",
tools=[calculate],
)
# Register all agents at once
app = AgentFunctionApp(agents=[weather_agent, math_agent])
This creates separate routes for each agent:
POST /api/agents/WeatherAgent/runGET /api/agents/WeatherAgent/{sessionId}POST /api/agents/MathAgent/runGET /api/agents/MathAgent/{sessionId}
Note: Each agent must have a unique name attribute. The framework uses this name to create routes and identify agents.
Option 2: Add agents incrementally with add_agent()
from durableagent import AgentFunctionApp
# Start with an empty app
app = AgentFunctionApp()
# Add agents one at a time (agents use their name attribute for routing)
app.add_agent(weather_agent)
app.add_agent(math_agent)
Health Check with Multiple Agents
The health check endpoint returns information about all registered agents:
curl http://localhost:7071/api/health
Response:
{
"status": "healthy",
"agents": [
{"name": "WeatherAgent", "type": "AzureOpenAIAssistantsAgent"},
{"name": "MathAgent", "type": "AzureOpenAIAssistantsAgent"}
],
"agent_count": 2
}
See the 02_MultiAgent sample for a complete example.
API Reference
AgentFunctionApp
The main class for creating a durable agent function app.
class AgentFunctionApp(df.DFApp):
def __init__(
self,
agents: Optional[List[AgentProtocol]] = None,
http_auth_level: func.AuthLevel = func.AuthLevel.ANONYMOUS,
enable_health_check: bool = True,
default_callback: Optional[AgentResponseCallbackProtocol] = None,
)
Parameters:
agents: List of agent instances to register. Each agent must have anameattribute.http_auth_level: Authentication level for HTTP triggersenable_health_check: Enable built-in health check endpoint at/healthdefault_callback: Optional callback invoked for agents that don't have a specific per-agent callback
Note: If no agents are provided, they can be added later using add_agent().
Type Safety:
- The framework uses
AgentProtocoltype hints for full type safety - All agents from
agent_frameworkimplementAgentProtocol - Provides IntelliSense support and compile-time type checking
Automatically Creates (per agent):
- POST
/api/agents/{agent_name}/run: Send messages to the agent - GET
/api/agents/{agent_name}/{sessionId}: Retrieve conversation state - GET
/api/health: Health check endpoint showing all agents (if enabled)
Methods:
def add_agent(
self,
agent: AgentProtocol,
callback: Optional[AgentResponseCallbackProtocol] = None,
) -> None
Add an agent to the app after initialization. The agent's name attribute is used for routing.
Raises ValueError if:
- The agent doesn't have a
nameattribute - An agent with the same name is already registered
Response Callbacks:
Use AgentResponseCallbackProtocol implementations to observe streaming updates and final responses. Pass a default callback when constructing the app or provide per-agent overrides via the callback argument to add_agent. Callbacks receive an AgentCallbackContext that includes the agent name, correlation ID, conversation ID, and request message.
from durableagent import AgentFunctionApp
from durableagent.callbacks import AgentCallbackContext, AgentResponseCallbackProtocol
class ConsoleLogger(AgentResponseCallbackProtocol):
async def on_streaming_response_update(self, update, context: AgentCallbackContext) -> None:
print(f"[{context.agent_name}] chunk: {update.text}")
async def on_agent_response(self, response, context: AgentCallbackContext) -> None:
print(f"[{context.agent_name}] final: {response.text}")
app = AgentFunctionApp(default_callback=ConsoleLogger())
AgentState
State management class that uses native agent_framework types.
class AgentState:
def __init__(self)
def add_user_message(self, content: str, correlation_id: str, role: str = "user") -> None
def add_assistant_message(self, content: str, agent_response: AgentRunResponse) -> None
def get_state(self) -> Dict[str, Any]
def restore_state(self, state: Dict[str, Any]) -> None
def reset(self) -> None
Key Features:
- Uses
ChatMessageobjects from agent_framework for conversation history - Stores full
AgentRunResponsemetadata in assistant messages - Automatic serialization using
to_dict()/from_dict() - Preserves all agent framework response details (kind, tool_calls, usage/tokens, etc.)
Example:
from durableagent import AgentState
from agent_framework import ChatMessage, AgentRunResponse
state = AgentState()
# Add user message
state.add_user_message("Hello!", correlation_id="corr-123")
# Add assistant response with full metadata
response = AgentRunResponse(
messages=[ChatMessage(role='assistant', text='Hi there!')],
response_id='123'
)
state.add_assistant_message("Hi there!", response)
# Serialize for persistence
serialized = state.get_state()
# Restore from persistence
restored = AgentState()
restored.restore_state(serialized)
AgentSessionId
A session identifier that wraps Azure Durable Functions EntityId for type-safe session management.
class AgentSessionId:
def __init__(self, name: str, key: str)
@staticmethod
def with_random_key(name: str) -> AgentSessionId
def to_entity_id(self) -> df.EntityId
@staticmethod
def from_entity_id(entity_id: df.EntityId) -> AgentSessionId
@staticmethod
def parse(session_id_string: str) -> AgentSessionId
Examples:
from durableagent import AgentSessionId
# Create with specific key
session_id = AgentSessionId(name="AgentEntity", key="user-123")
# Create with random GUID
session_id = AgentSessionId.with_random_key(name="AgentEntity")
# Convert to EntityId for Durable Functions APIs
entity_id = session_id.to_entity_id()
# Parse from string format (@name@key)
session_id = AgentSessionId.parse("@AgentEntity@user-123")
# String representation
print(session_id) # @AgentEntity@user-123
Request/Response Format
POST /api/agents/{agent_name}/run - Send Message
URL: POST /api/agents/{agent_name}/run
Request:
{
"message": "Your message to the agent",
"sessionId": "optional-session-id",
"role": "user",
"enable_tool_calls": true,
"response_schema": {}
}
Note:
- Only
messageis required. IfsessionIdis not provided, a random session ID will be generated. sessionKeyis also supported as an alias forsessionId
Response:
{
"response": "Agent's response",
"message": "Your original message",
"sessionId": "session-id",
"status": "success",
"message_count": 5
}
GET /api/agents/{agent_name}/{sessionId} - Get State
URL: GET /api/agents/{agent_name}/{sessionId}
Response:
{
"message_count": 5,
"conversation_history": [
{
"role": "user",
"content": "Message",
"timestamp": "2025-01-15T10:30:00Z"
},
{
"role": "assistant",
"content": "Response text",
"timestamp": "2025-01-15T10:30:05Z",
"agent_response": {
"text": "Response text",
"kind": "agent_message",
"tool_calls": [...],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150
}
}
}
],
"last_response": "Last agent response",
"agent_type": "Agent"
}
Conversation History Structure:
- User messages: Include
role,content,timestamp - Assistant messages: Include
role,content,timestamp, andagent_responseagent_responsecontains the full Microsoft Agent Framework response object- Includes metadata:
kind,tool_calls,usage(tokens), and other framework fields - Preserves all response details for analysis and debugging
Advanced Usage
Multiple Agents in One App
from durableagent import AgentFunctionApp
# Create multiple agents
weather_agent = create_weather_agent()
news_agent = create_news_agent()
# Each agent gets its own route
weather_app = AgentFunctionApp(agent=weather_agent, agent_name="WeatherAgent")
news_app = AgentFunctionApp(agent=news_agent, agent_name="NewsAgent")
Adding Custom Azure Functions
Since AgentFunctionApp inherits from DFApp, you can add any Azure Functions:
import azure.functions as func
from durableagent import AgentFunctionApp
app = AgentFunctionApp(agent=my_agent)
# Add a timer trigger
@app.timer_trigger(schedule="0 */5 * * * *", arg_name="timer")
def periodic_task(timer: func.TimerRequest):
print("Runs every 5 minutes")
# Add a queue trigger
@app.queue_trigger(arg_name="msg", queue_name="tasks", connection="AzureWebJobsStorage")
def process_queue(msg: func.QueueMessage):
print(f"Processing: {msg.get_body()}")
# Add a blob trigger
@app.blob_trigger(arg_name="blob", path="uploads/{name}", connection="AzureWebJobsStorage")
def process_blob(blob: func.InputStream):
print(f"Processing blob: {blob.name}")
# Add custom HTTP endpoints
@app.route(route="status", methods=["GET"])
def get_status(req: func.HttpRequest):
return func.HttpResponse("Running", status_code=200)
Using Different Agent Clients
Azure OpenAI Assistants API
from agent_framework.azure import AzureOpenAIAssistantsClient
from azure.identity import AzureCliCredential
agent = AzureOpenAIAssistantsClient(
endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
deployment_name="gpt-4",
credential=AzureCliCredential(),
).create_agent(
name="MyAgent",
instructions="You are a helpful assistant.",
tools=[my_tool],
)
Azure AI Agent Service
from azure.ai.projects.aio import AzureAIAgentClient
from azure.identity.aio import DefaultAzureCredential
agent = AzureAIAgentClient(
async_credential=DefaultAzureCredential()
).create_agent(
name="MyAgent",
instructions="You are a helpful assistant.",
tools=[my_tool],
)
Custom Authentication
import azure.functions as func
from durableagent import AgentFunctionApp
app = AgentFunctionApp(
agent=my_agent,
http_auth_level=func.AuthLevel.FUNCTION # Requires function key
)
Architecture
How It Works
- HTTP Trigger receives user message with optional conversation_id
- Durable Entity is signaled with the message
- Agent runs directly inside the entity, returning an
AgentRunResponse - Response is extracted (text or structured data) and returned to caller
- Full AgentRunResponse is stored in state with all metadata
- State is persisted automatically (conversation history as ChatMessages, message count)
- GET endpoint allows retrieving full conversation state with all response details
Why Durable Entities?
Traditional orchestrations require middleware to intercept tool calls and execute them as activities. Durable Entities provide a simpler pattern:
- ✅ State is automatically durable - no middleware needed
- ✅ Operations run inside the entity - natural execution model
- ✅ Better for long-running conversations - entities are designed for state
- ✅ Simpler code - less boilerplate
- ✅ Direct agent execution - no interception layer
Entity Operations
The framework creates entities with these operations:
run_agent: Execute agent with a messageget_state: Retrieve conversation statereset: Clear conversation history
Deployment
Deploy to Azure
1. Create Azure resources
# Create resource group
az group create --name myResourceGroup --location eastus
# Create storage account
az storage account create \
--name mystorageaccount \
--resource-group myResourceGroup \
--location eastus
# Create function app
az functionapp create \
--name myagentapp \
--resource-group myResourceGroup \
--consumption-plan-location eastus \
--runtime python \
--runtime-version 3.11 \
--functions-version 4 \
--storage-account mystorageaccount
2. Configure app settings
az functionapp config appsettings set \
--name myagentapp \
--resource-group myResourceGroup \
--settings \
AZURE_OPENAI_ENDPOINT="your-endpoint" \
AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4"
3. Deploy
func azure functionapp publish myagentapp
Monitoring
Enable Application Insights for monitoring:
az monitor app-insights component create \
--app myagentapp-insights \
--location eastus \
--resource-group myResourceGroup
# Link to function app
az functionapp config appsettings set \
--name myagentapp \
--resource-group myResourceGroup \
--settings \
APPINSIGHTS_INSTRUMENTATIONKEY="your-key"
Examples
See the samples/ directory for complete examples:
basic_agent.py: Simple weather agent with multiple toolsfull_featured_app.py: Advanced example with custom functions, timers, queues, and morefunction_app_example/: Complete deployable project template
Troubleshooting
Common Issues
Agent not responding:
- Check that Azure OpenAI endpoint and deployment are correct
- Verify authentication credentials (run
az login) - Check function app logs with
func start --verbose
State not persisting:
- Ensure AzureWebJobsStorage is configured
- For local development, use Azurite storage emulator
- Check entity state with GET endpoint
Import errors:
- Ensure all dependencies are installed:
pip install -r requirements.txt - Verify Python version is 3.9 or higher
Logging
The framework uses Python's standard logging. To see detailed logs:
func start --verbose
Or configure logging level:
import logging
logging.basicConfig(level=logging.DEBUG)
Contributing
Contributions are welcome! Please see the main repository for contribution guidelines.
License
See LICENSE file in the repository root.
Related Projects
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_framework_azurefunctions-0.0.2b1.tar.gz.
File metadata
- Download URL: agent_framework_azurefunctions-0.0.2b1.tar.gz
- Upload date:
- Size: 44.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a60626938e4858c52bd86f8d957dbc756d894cab47118ade436102953a61e5d8
|
|
| MD5 |
fe1b4bdedc1dbce713fc328d7baddd46
|
|
| BLAKE2b-256 |
9a8af0002c62c6cdc4162f1bf9a9fa2cef9cbe339b8474132dff705eff19fbcf
|
File details
Details for the file agent_framework_azurefunctions-0.0.2b1-py3-none-any.whl.
File metadata
- Download URL: agent_framework_azurefunctions-0.0.2b1-py3-none-any.whl
- Upload date:
- Size: 29.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
402377d24a82f962ba76e57cc4cce166e96bf66bf6316be6bdf2d3926fb4277a
|
|
| MD5 |
6ae81e41d12423ab3939016ff1e8e5fa
|
|
| BLAKE2b-256 |
242d8454ace492eaa68b5f837edcc8d782f074fd07ff64ff54a1495fd9badf75
|