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
This commit is contained in:
Laveesh Rohra
2025-11-12 09:56:19 -08:00
committed by GitHub
Unverified
parent ebab25b196
commit ff28066c9c
24 changed files with 692 additions and 436 deletions
@@ -7,7 +7,7 @@ This sample demonstrates how to use the Durable Extension for Agent Framework to
- Defining a simple agent with the Microsoft Agent Framework and wiring it into
an Azure Functions app via the Durable Extension for Agent Framework.
- Calling the agent through generated HTTP endpoints (`/api/agents/Joker/run`).
- Managing conversation state with session identifiers, so multiple clients can
- Managing conversation state with thread identifiers, so multiple clients can
interact with the agent concurrently without sharing context.
## Prerequisites
@@ -24,6 +24,13 @@ curl -X POST http://localhost:7071/api/agents/Joker/run \
-d "Tell me a short joke about cloud computing."
```
The agent responds with a JSON payload that includes the generated joke.
> **Note:** To return immediately with an HTTP 202 response instead of waiting for the agent output, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body. The default behavior waits for the response.
## Expected Output
When you send a POST request with plain-text input, the Functions host responds with an HTTP 202 and queues the request for the durable agent entity. A typical response body looks like the following:
Expected HTTP 202 payload:
```json
@@ -31,7 +38,7 @@ Expected HTTP 202 payload:
"status": "accepted",
"response": "Agent request accepted",
"message": "Tell me a short joke about cloud computing.",
"conversation_id": "<guid>",
"thread_id": "<guid>",
"correlation_id": "<guid>"
}
```
@@ -13,12 +13,10 @@ Content-Type: application/json
{
"message": "Add a security element to it.",
"sessionId": "session-003",
"waitForCompletion": true
"thread_id": "thread-001"
}
### Ask for a joke (plain text payload)
POST {{agentRoute}}/run
x-wait-for-completion: true
Give me a programming joke about race conditions.
@@ -6,7 +6,7 @@ This sample demonstrates how to use the Durable Extension for Agent Framework to
- Using the Microsoft Agent Framework to define multiple AI agents with unique names and instructions.
- Registering multiple agents with the Function app and running them using HTTP.
- Conversation management (via session IDs) for isolated interactions per agent.
- Conversation management (via thread IDs) for isolated interactions per agent.
- Two different methods for registering agents: list-based initialization and incremental addition.
## Prerequisites
@@ -15,6 +15,15 @@ Complete the common environment preparation steps described in `../README.md`, i
## Running the Sample
With the environment setup and function app running, you can test the sample by sending HTTP requests to the different agent endpoints.
You can use the `demo.http` file to send messages to the agents, or a command line tool like `curl` as shown below:
> **Note:** Each endpoint waits for the agent response by default. To receive an immediate HTTP 202 instead, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body.
### Test the Weather Agent
Bash (Linux/macOS/WSL):
Weather agent request:
```bash
@@ -30,7 +39,7 @@ Expected HTTP 202 payload:
"status": "accepted",
"response": "Agent request accepted",
"message": "What is the weather in Seattle?",
"conversation_id": "<guid>",
"thread_id": "<guid>",
"correlation_id": "<guid>"
}
```
@@ -50,7 +59,7 @@ Expected HTTP 202 payload:
"status": "accepted",
"response": "Agent request accepted",
"message": "Calculate a 20% tip on a $50 bill",
"conversation_id": "<guid>",
"thread_id": "<guid>",
"correlation_id": "<guid>"
}
```
@@ -36,8 +36,7 @@ Content-Type: application/json
{
"message": "What is the weather in Seattle?",
"sessionId": "weather-user-001",
"waitForCompletion": true
"thread_id": "weather-user-001"
}
###
@@ -51,8 +50,7 @@ Content-Type: application/json
{
"message": "Calculate a 20% tip on a $50 bill",
"sessionId": "math-user-001",
"waitForCompletion": true
"thread_id": "math-user-001"
}
###
@@ -10,8 +10,8 @@ an HTTP API that can be polled by a web client or dashboard.
- 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}`.
`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.
@@ -32,6 +32,8 @@ curl -X POST http://localhost:7071/api/agents/CallbackAgent/run \
-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
@@ -46,7 +48,7 @@ curl -X DELETE http://localhost:7071/api/agents/CallbackAgent/callbacks/<convers
## Expected Output
When you call `GET /api/agents/CallbackAgent/callbacks/{conversationId}` after sending a request to the agent,
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
@@ -54,7 +56,7 @@ the API returns a list of streaming and final callback events similar to the fol
{
"timestamp": "2024-01-01T00:00:00Z",
"agent_name": "CallbackAgent",
"conversation_id": "<conversationId>",
"thread_id": "<thread_id>",
"correlation_id": "<guid>",
"request_message": "Tell me a short joke",
"event_type": "stream",
@@ -64,7 +66,7 @@ the API returns a list of streaming and final callback events similar to the fol
{
"timestamp": "2024-01-01T00:00:01Z",
"agent_name": "CallbackAgent",
"conversation_id": "<conversationId>",
"thread_id": "<thread_id>",
"correlation_id": "<guid>",
"request_message": "Tell me a short joke",
"event_type": "final",
@@ -3,13 +3,13 @@
###
### 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
### - 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}}
@conversationId = test-stream-00
@thread_id = test-thread-00
### Health Check
GET {{baseUrl}}/api/health
@@ -20,11 +20,11 @@ Content-Type: application/json
{
"message": "Generate a short weather update for Paris and mention streaming callbacks.",
"sessionId": "{{conversationId}}"
"thread_id": "{{thread_id}}"
}
### Inspect callback telemetry
GET {{agentRoute}}/callbacks/{{conversationId}}
GET {{agentRoute}}/callbacks/{{thread_id}}
### Clear stored callback telemetry for the conversation
DELETE {{agentRoute}}/callbacks/{{conversationId}}
### Clear stored callback telemetry for the thread
DELETE {{agentRoute}}/callbacks/{{thread_id}}
@@ -24,7 +24,7 @@ from agent_framework.azurefunctions import AgentFunctionApp, AgentCallbackContex
logger = logging.getLogger(__name__)
# 1. Maintain an in-memory store for callback events (replace with durable storage in production).
# 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)
@@ -65,8 +65,8 @@ class ConversationAuditTrail(AgentResponseCallbackProtocol):
"text": getattr(update, "text", None),
}
)
conversation_id = context.conversation_id or ""
callback_events[conversation_id].append(event)
thread_id = context.thread_id or ""
callback_events[thread_id].append(event)
preview = event.get("text") or event.get("update_kind")
self._logger.info(
@@ -85,8 +85,8 @@ class ConversationAuditTrail(AgentResponseCallbackProtocol):
"usage": _serialize_usage(getattr(response, "usage_details", None)),
}
)
conversation_id = context.conversation_id or ""
callback_events[conversation_id].append(event)
thread_id = context.thread_id or ""
callback_events[thread_id].append(event)
self._logger.info(
"[%s][%s] final response recorded",
@@ -96,10 +96,11 @@ class ConversationAuditTrail(AgentResponseCallbackProtocol):
@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,
"conversation_id": context.conversation_id,
"thread_id": thread_id,
"correlation_id": context.correlation_id,
"request_message": context.request_message,
}
@@ -122,12 +123,12 @@ app.add_agent(callback_agent)
@app.function_name("get_callback_events")
@app.route(route="agents/{agent_name}/callbacks/{conversationId}", methods=["GET"])
@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 conversation."""
"""Return all callback events collected for a thread."""
conversation_id = req.route_params.get("conversationId", "")
events = callback_events.get(conversation_id, [])
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,
@@ -136,44 +137,44 @@ async def get_callback_events(req: func.HttpRequest) -> func.HttpResponse:
@app.function_name("reset_callback_events")
@app.route(route="agents/{agent_name}/callbacks/{conversationId}", methods=["DELETE"])
@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 conversation."""
"""Clear the stored callback events for a thread."""
conversation_id = req.route_params.get("conversationId", "")
callback_events.pop(conversation_id, None)
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/{conversationId}`:
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",
"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
{
"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
}
}
}
]
"""
@@ -25,6 +25,8 @@ Poll the returned `statusQueryGetUri` until completion:
curl http://localhost:7071/api/singleagent/status/<instanceId>
```
> **Note:** The underlying agent run endpoint now waits for responses by default. If you invoke it directly and prefer an immediate HTTP 202, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the payload.
The orchestration first requests an inspirational sentence from the agent, then refines the initial response while
keeping it under 25 words—mirroring the behaviour of the corresponding .NET sample.
@@ -27,6 +27,8 @@ Poll the returned `statusQueryGetUri` until completion:
curl http://localhost:7071/api/multiagent/status/<instanceId>
```
> **Note:** The agent run endpoints wait for responses by default. If you call them directly and need an immediate HTTP 202, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request payload.
The orchestration launches both agents simultaneously so their domain-specific answers can be combined for the caller.
## Expected Output
@@ -27,6 +27,8 @@ Poll the returned `statusQueryGetUri` or call the status route directly:
curl http://localhost:7071/api/spamdetection/status/<instanceId>
```
> **Note:** The spam detection run endpoint waits for responses by default. To opt into an immediate HTTP 202, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the POST body.
## Expected Responses
- Spam payloads return `Email marked as spam: <reason>` by invoking the `handle_spam_email` activity.
- Legitimate emails return `Email sent: <draft>` after the email assistant agent produces a structured reply.
@@ -39,6 +39,8 @@ curl -X POST http://localhost:7071/api/hitl/approve/<instanceId> \
-d '{"approved": true, "feedback": "Looks good"}'
```
> **Note:** Calls to the underlying agent run endpoint wait for responses by default. If you need an immediate HTTP 202 response, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body.
## Expected Responses
- `POST /api/hitl/run` returns a 202 Accepted payload with the Durable Functions instance ID.
- `POST /api/hitl/approve/{instanceId}` echoes the decision that the orchestration receives.