mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add more samples for Azure Functions (#1980)
* Move all samples * fix comments * remove dead lines * Make samples simpler
This commit is contained in:
committed by
GitHub
Unverified
parent
40b6deff96
commit
0aa8d30d7f
@@ -0,0 +1,109 @@
|
||||
# Callback Telemetry Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Extension for Agent Framework's response callbacks to observe
|
||||
streaming updates and final agent responses in real time. The `ConversationAuditTrail` callback
|
||||
records each chunk received from the Azure OpenAI agent and exposes the collected events through
|
||||
an HTTP API that can be polled by a web client or dashboard.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Registers a default `AgentResponseCallbackProtocol` implementation that logs streaming and final
|
||||
responses.
|
||||
- Persists callback events in an in-memory store and exposes them via
|
||||
`GET /api/agents/{agentName}/callbacks/{conversationId}`.
|
||||
- Shows how to reset stored callback events with `DELETE /api/agents/{agentName}/callbacks/{conversationId}`.
|
||||
- Works alongside the standard `/api/agents/{agentName}/run` endpoint so you can correlate callback
|
||||
telemetry with agent responses.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- Azure Functions Core Tools v4
|
||||
- Access to an Azure OpenAI deployment (configure the environment variables listed in
|
||||
`local.settings.json` or export them in your shell)
|
||||
- Dependencies from `requirements.txt` installed in your environment
|
||||
|
||||
> **Note:** The sample stores callback events in memory for simplicity. For production scenarios you
|
||||
> should persist events to Application Insights, Azure Storage, Cosmos DB, or another durable store.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. Create and activate a virtual environment:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
2. Install dependencies (from the repository root or this directory):
|
||||
|
||||
```powershell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Copy `local.settings.json.template` to `local.settings.json` and update the values (or export them as environment variables) with your Azure resources.
|
||||
|
||||
4. Start the Functions host:
|
||||
|
||||
```powershell
|
||||
func start
|
||||
```
|
||||
|
||||
5. Use the [`demo.http`](./demo.http) file (VS Code REST Client) or any HTTP client to:
|
||||
- Send a message to the agent: `POST /api/agents/CallbackAgent/run`
|
||||
- Query callback telemetry: `GET /api/agents/CallbackAgent/callbacks/{conversationId}`
|
||||
- Clear stored events: `DELETE /api/agents/CallbackAgent/callbacks/{conversationId}`
|
||||
|
||||
Example workflow after the host starts:
|
||||
|
||||
```text
|
||||
POST /api/agents/CallbackAgent/run # send a conversation message
|
||||
GET /api/agents/CallbackAgent/callbacks/test-session # inspect streaming + final events
|
||||
DELETE /api/agents/CallbackAgent/callbacks/test-session # reset telemetry for the session
|
||||
```
|
||||
|
||||
The GET endpoint returns an array of events captured by the callback, including timestamps,
|
||||
streaming chunk previews, and the final response metadata. This makes it easy to build real-time
|
||||
UI updates or audit logs on top of Durable Agents.
|
||||
|
||||
## Expected Output
|
||||
|
||||
When you call `GET /api/agents/CallbackAgent/callbacks/{conversationId}` after sending a request to the agent,
|
||||
the API returns a list of streaming and final callback events similar to the following:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"agent_name": "CallbackAgent",
|
||||
"conversation_id": "<conversationId>",
|
||||
"correlation_id": "<guid>",
|
||||
"request_message": "Tell me a short joke",
|
||||
"event_type": "stream",
|
||||
"update_kind": "text",
|
||||
"text": "Sure, here's a joke..."
|
||||
},
|
||||
{
|
||||
"timestamp": "2024-01-01T00:00:01Z",
|
||||
"agent_name": "CallbackAgent",
|
||||
"conversation_id": "<conversationId>",
|
||||
"correlation_id": "<guid>",
|
||||
"request_message": "Tell me a short joke",
|
||||
"event_type": "final",
|
||||
"response_text": "Why did the cloud...",
|
||||
"usage": {
|
||||
"type": "usage_details",
|
||||
"input_token_count": 159,
|
||||
"output_token_count": 29,
|
||||
"total_token_count": 188
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
### Callback Sample - API Tests
|
||||
### Use with VS Code REST Client or another HTTP testing tool.
|
||||
###
|
||||
### Endpoints introduced in this sample:
|
||||
### - POST /api/agents/{agentName}/run : send a message to the agent
|
||||
### - GET /api/agents/{agentName}/callbacks/{conversationId} : retrieve callback telemetry
|
||||
### - DELETE /api/agents/{agentName}/callbacks/{conversationId} : clear stored callback events
|
||||
|
||||
@baseUrl = http://localhost:7071
|
||||
@agentName = CallbackAgent
|
||||
@agentRoute = {{baseUrl}}/api/agents/{{agentName}}
|
||||
@conversationId = test-stream-00
|
||||
|
||||
### Health Check
|
||||
GET {{baseUrl}}/api/health
|
||||
|
||||
### Send message (callbacks will capture streaming + final response)
|
||||
POST {{agentRoute}}/run
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Generate a short weather update for Paris and mention streaming callbacks.",
|
||||
"sessionId": "{{conversationId}}"
|
||||
}
|
||||
|
||||
### Inspect callback telemetry
|
||||
GET {{agentRoute}}/callbacks/{{conversationId}}
|
||||
|
||||
### Clear stored callback telemetry for the conversation
|
||||
DELETE {{agentRoute}}/callbacks/{{conversationId}}
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Capture agent response callbacks inside Azure Functions.
|
||||
|
||||
Components used in this sample:
|
||||
- AzureOpenAIChatClient to build an agent that streams interim updates.
|
||||
- AgentFunctionApp with a default AgentResponseCallbackProtocol implementation.
|
||||
- Azure Functions HTTP triggers that expose callback telemetry via REST.
|
||||
|
||||
Prerequisites: set `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, and either
|
||||
`AZURE_OPENAI_API_KEY` or authenticate with Azure CLI before starting the Functions host."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, DefaultDict
|
||||
|
||||
import azure.functions as func
|
||||
from agent_framework import AgentRunResponseUpdate
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
|
||||
from agent_framework.azurefunctions import AgentFunctionApp, AgentCallbackContext, AgentResponseCallbackProtocol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 1. Maintain an in-memory store for callback events (replace with durable storage in production).
|
||||
CallbackStore = DefaultDict[str, list[dict[str, Any]]]
|
||||
callback_events: CallbackStore = defaultdict(list)
|
||||
|
||||
|
||||
def _serialize_usage(usage: Any) -> Any:
|
||||
"""Best-effort serialization for agent usage metadata."""
|
||||
|
||||
if usage is None:
|
||||
return None
|
||||
|
||||
model_dump = getattr(usage, "model_dump", None)
|
||||
if callable(model_dump):
|
||||
return model_dump()
|
||||
|
||||
to_dict = getattr(usage, "to_dict", None)
|
||||
if callable(to_dict):
|
||||
return to_dict()
|
||||
|
||||
return str(usage)
|
||||
|
||||
|
||||
class ConversationAuditTrail(AgentResponseCallbackProtocol):
|
||||
"""Callback that records streaming chunks and final responses for later inspection."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._logger = logging.getLogger("durableagent.samples.callbacks.audit")
|
||||
|
||||
async def on_streaming_response_update(
|
||||
self,
|
||||
update: AgentRunResponseUpdate,
|
||||
context: AgentCallbackContext,
|
||||
) -> None:
|
||||
event = self._build_base_event(context)
|
||||
event.update(
|
||||
{
|
||||
"event_type": "stream",
|
||||
"update_kind": getattr(update, "kind", "text"),
|
||||
"text": getattr(update, "text", None),
|
||||
}
|
||||
)
|
||||
conversation_id = context.conversation_id or ""
|
||||
callback_events[conversation_id].append(event)
|
||||
|
||||
preview = event.get("text") or event.get("update_kind")
|
||||
self._logger.info(
|
||||
"[%s][%s] streaming chunk: %s",
|
||||
context.agent_name,
|
||||
context.correlation_id,
|
||||
preview,
|
||||
)
|
||||
|
||||
async def on_agent_response(self, response, context: AgentCallbackContext) -> None:
|
||||
event = self._build_base_event(context)
|
||||
event.update(
|
||||
{
|
||||
"event_type": "final",
|
||||
"response_text": getattr(response, "text", None),
|
||||
"usage": _serialize_usage(getattr(response, "usage_details", None)),
|
||||
}
|
||||
)
|
||||
conversation_id = context.conversation_id or ""
|
||||
callback_events[conversation_id].append(event)
|
||||
|
||||
self._logger.info(
|
||||
"[%s][%s] final response recorded",
|
||||
context.agent_name,
|
||||
context.correlation_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_base_event(context: AgentCallbackContext) -> dict[str, Any]:
|
||||
return {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"agent_name": context.agent_name,
|
||||
"conversation_id": context.conversation_id,
|
||||
"correlation_id": context.correlation_id,
|
||||
"request_message": context.request_message,
|
||||
}
|
||||
|
||||
|
||||
# 2. Create the agent that will emit streaming updates and final responses.
|
||||
callback_agent = AzureOpenAIChatClient().create_agent(
|
||||
name="CallbackAgent",
|
||||
instructions=(
|
||||
"You are a friendly assistant that narrates actions while responding. "
|
||||
"Keep answers concise and acknowledge when callbacks capture streaming updates."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# 3. Register the agent inside AgentFunctionApp with a default callback instance.
|
||||
audit_callback = ConversationAuditTrail()
|
||||
app = AgentFunctionApp(enable_health_check=True, default_callback=audit_callback)
|
||||
app.add_agent(callback_agent)
|
||||
|
||||
|
||||
@app.function_name("get_callback_events")
|
||||
@app.route(route="agents/{agent_name}/callbacks/{conversationId}", methods=["GET"])
|
||||
async def get_callback_events(req: func.HttpRequest) -> func.HttpResponse:
|
||||
"""Return all callback events collected for a conversation."""
|
||||
|
||||
conversation_id = req.route_params.get("conversationId", "")
|
||||
events = callback_events.get(conversation_id, [])
|
||||
return func.HttpResponse(
|
||||
json.dumps(events, indent=2),
|
||||
status_code=200,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
|
||||
@app.function_name("reset_callback_events")
|
||||
@app.route(route="agents/{agent_name}/callbacks/{conversationId}", methods=["DELETE"])
|
||||
async def reset_callback_events(req: func.HttpRequest) -> func.HttpResponse:
|
||||
"""Clear the stored callback events for a conversation."""
|
||||
|
||||
conversation_id = req.route_params.get("conversationId", "")
|
||||
callback_events.pop(conversation_id, None)
|
||||
return func.HttpResponse(status_code=204)
|
||||
|
||||
|
||||
"""
|
||||
Expected output when querying `GET /api/agents/CallbackAgent/callbacks/{conversationId}`:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
[
|
||||
{
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"agent_name": "CallbackAgent",
|
||||
"conversation_id": "<conversationId>",
|
||||
"correlation_id": "<guid>",
|
||||
"request_message": "Tell me a short joke",
|
||||
"event_type": "stream",
|
||||
"update_kind": "text",
|
||||
"text": "Sure, here's a joke..."
|
||||
},
|
||||
{
|
||||
"timestamp": "2024-01-01T00:00:01Z",
|
||||
"agent_name": "CallbackAgent",
|
||||
"conversation_id": "<conversationId>",
|
||||
"correlation_id": "<guid>",
|
||||
"request_message": "Tell me a short joke",
|
||||
"event_type": "final",
|
||||
"response_text": "Why did the cloud...",
|
||||
"usage": {
|
||||
"type": "usage_details",
|
||||
"input_token_count": 159,
|
||||
"output_token_count": 29,
|
||||
"total_token_count": 188
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"maxTelemetryItemsPerSecond": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "python",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "<AZURE_OPENAI_CHAT_DEPLOYMENT_NAME>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
agent-framework-azurefunctions
|
||||
azure-identity
|
||||
Reference in New Issue
Block a user