mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Python: Azure Functions feature branch (#1916)
* Python: Add Scaffolding for Durable AzureFunctions package to Agent Framework (#1823) * Add scafolding * update readme * add code owners and label * update owners * .NET: Durable extension: initial src and unit tests (#1900) * Python: Add Durable Agent Wrapper code (#1913) * add initial changes * Move code and add single sample * Update logger * Remove unused code * address PR comments * cleanup code and address comments --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> * Azure Functions .NET samples (#1939) * Python: Add Unit tests for Azurefunctions package (#1976) * Add Unit tests for Azurefunctions * remove duplicate import * .NET: [Feature Branch] Migrate state schema updates and support for agents as MCP tools (#1979) * Python: Add more samples for Azure Functions (#1980) * Move all samples * fix comments * remove dead lines * Make samples simpler * .NET: [Feature Branch] Durable Task extension integration tests (#2017) * .NET: [Feature Branch] Update OpenAI config for integration tests (#2063) * Python: Add Integration tests for AzureFunctions (#2020) * Add Integration tests * Remove DTS extension * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add pyi file for type safety * Add samples in readme * Updated all readme instructions * Address comments * Update readmes * Fix requirements * Address comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * .NET: [Feature Branch] Update dotnet-build-and-test.yml to support integration tests (#2070) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix DTS startup issue and improve logging (#2103) * .NET: [Feature Branch] Introduce Azure OpenAI config for .NET pipeline (#2106) Also fixes an issue where we were trying to start docker containers for integration tests on Windows, which doesn't work. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix uv.lock after merge * Python: Add README for Azure Functions samples setup (#2100) * Add README for Azure Functions samples setup Added setup instructions for Azure Functions samples, including environment setup, virtual environment creation, and running samples. * Update python/samples/getting_started/azure_functions/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Laveesh Rohra <larohra@microsoft.com> * Fix or remove broken markdown file links (#2115) * .NET: [Feature Branch] Update HTTP API to be consistent across languages (#2118) * Python: Fix AzureFunctions Integration Tests (#2116) * Add Identity Auth to samples * Update python/samples/getting_started/azure_functions/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/samples/getting_started/azure_functions/01_single_agent/function_app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/samples/getting_started/azure_functions/02_multi_agent/function_app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Python: Fix Http Schema (#2112) * Rename to threadid * Respond in plain text * Make snake-case * Add http prefix * rename to wait-for-response * Add query param check * address comments * .NET: Remove IsPackable=false in preparation for nuget release (#2142) * Python: Move `azurefunctions` to `azure` for import (#2141) * Move import to Azure * fix mypy * Update python/packages/azurefunctions/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add missing types * Address comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/azurefunctions/pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/azurefunctions/agent_framework_azurefunctions/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix imports * Address PR feedback from westey-m (#2150) - Adds a link from the /dotnet/samples/README.md to /dotnet/samples/AzureFunctions - Make DurableAgentThread deserialization internal for future-proofing - Update JSON serialization logic to address recently discovered issues with source generator serialization * Address comments (#2160) --------- Co-authored-by: Laveesh Rohra <larohra@microsoft.com> Co-authored-by: Chris Gillum <cgillum@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Anirudh Garg <anirudhg@microsoft.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
5537b1da79
commit
67a8147151
@@ -0,0 +1,82 @@
|
||||
# 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/{thread_id}`.
|
||||
- Shows how to reset stored callback events with `DELETE /api/agents/{agentName}/callbacks/{thread_id}`.
|
||||
- Works alongside the standard `/api/agents/{agentName}/run` endpoint so you can correlate callback
|
||||
telemetry with agent responses.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Complete the shared environment setup steps in `../README.md`, including creating a virtual environment, installing dependencies, and configuring Azure OpenAI credentials and storage settings.
|
||||
|
||||
> **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
|
||||
|
||||
Send a prompt to the agent:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/CallbackAgent/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Tell me a short joke"}'
|
||||
```
|
||||
|
||||
> **Note:** The run endpoint waits for the agent response by default. To return immediately, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body.
|
||||
|
||||
Poll callback telemetry (replace `<conversationId>` with the value from the POST response):
|
||||
|
||||
```bash
|
||||
curl http://localhost:7071/api/agents/CallbackAgent/callbacks/<conversationId>
|
||||
```
|
||||
|
||||
Reset stored events:
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:7071/api/agents/CallbackAgent/callbacks/<conversationId>
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
When you call `GET /api/agents/CallbackAgent/callbacks/{thread_id}` 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",
|
||||
"thread_id": "<thread_id>",
|
||||
"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",
|
||||
"thread_id": "<thread_id>",
|
||||
"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/{thread_id} : retrieve callback telemetry
|
||||
### - DELETE /api/agents/{agentName}/callbacks/{thread_id} : clear stored callback events
|
||||
|
||||
@baseUrl = http://localhost:7071
|
||||
@agentName = CallbackAgent
|
||||
@agentRoute = {{baseUrl}}/api/agents/{{agentName}}
|
||||
@thread_id = test-thread-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.",
|
||||
"thread_id": "{{thread_id}}"
|
||||
}
|
||||
|
||||
### Inspect callback telemetry
|
||||
GET {{agentRoute}}/callbacks/{{thread_id}}
|
||||
|
||||
### Clear stored callback telemetry for the thread
|
||||
DELETE {{agentRoute}}/callbacks/{{thread_id}}
|
||||
@@ -0,0 +1,183 @@
|
||||
"""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 (
|
||||
AgentCallbackContext,
|
||||
AgentFunctionApp,
|
||||
AgentResponseCallbackProtocol,
|
||||
AzureOpenAIChatClient,
|
||||
)
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 1. Maintain an in-memory store for callback events keyed by thread ID (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),
|
||||
}
|
||||
)
|
||||
thread_id = context.thread_id or ""
|
||||
callback_events[thread_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)),
|
||||
}
|
||||
)
|
||||
thread_id = context.thread_id or ""
|
||||
callback_events[thread_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]:
|
||||
thread_id = context.thread_id
|
||||
return {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"agent_name": context.agent_name,
|
||||
"thread_id": thread_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(credential=AzureCliCredential()).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/{thread_id}", methods=["GET"])
|
||||
async def get_callback_events(req: func.HttpRequest) -> func.HttpResponse:
|
||||
"""Return all callback events collected for a thread."""
|
||||
|
||||
thread_id = req.route_params.get("thread_id", "")
|
||||
events = callback_events.get(thread_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/{thread_id}", methods=["DELETE"])
|
||||
async def reset_callback_events(req: func.HttpRequest) -> func.HttpResponse:
|
||||
"""Clear the stored callback events for a thread."""
|
||||
|
||||
thread_id = req.route_params.get("thread_id", "")
|
||||
callback_events.pop(thread_id, None)
|
||||
return func.HttpResponse(status_code=204)
|
||||
|
||||
|
||||
"""
|
||||
Expected output when querying `GET /api/agents/CallbackAgent/callbacks/{thread_id}`:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
[
|
||||
{
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"agent_name": "CallbackAgent",
|
||||
"thread_id": "<thread_id>",
|
||||
"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",
|
||||
"thread_id": "<thread_id>",
|
||||
"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,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"maxTelemetryItemsPerSecond": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "%TASKHUB_NAME%"
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "python",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
||||
"TASKHUB_NAME": "default",
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "<AZURE_OPENAI_CHAT_DEPLOYMENT_NAME>",
|
||||
"AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
agent-framework-azurefunctions
|
||||
azure-identity
|
||||
Reference in New Issue
Block a user