Python: Add more samples for Azure Functions (#1980)

* Move all samples

* fix comments

* remove dead lines

* Make samples simpler
This commit is contained in:
Laveesh Rohra
2025-11-07 10:37:03 -08:00
committed by GitHub
Unverified
parent 40b6deff96
commit 0aa8d30d7f
40 changed files with 2155 additions and 46 deletions
@@ -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)"
}
}
@@ -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