Python: Add Non-durable Azure Functions Samples#2987
Python: Add Non-durable Azure Functions Samples#2987ahmedmuhsin wants to merge 2 commits intomicrosoft:feature-durabletask-pythonfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds two new Azure Functions samples demonstrating stateless HTTP streaming for agents and workflows without durable orchestration. The samples provide developers with simpler alternatives to durable functions for real-time agent interactions that complete within HTTP timeout limits.
Key Changes:
- Added
01_agent_http_streamingsample showing single agent with tool calling via SSE streaming - Added
02_workflow_http_streamingsample demonstrating multi-agent sequential workflows with real-time streaming - Added comprehensive parent README with comparison tables and guidance on when to use non-durable vs durable approaches
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
python/samples/getting_started/azure_functions/non-durable/README.md |
Parent directory overview with comparison tables and setup instructions |
python/samples/getting_started/azure_functions/non-durable/01_agent_http_streaming/function_app.py |
Single agent HTTP streaming implementation with SSE format |
python/samples/getting_started/azure_functions/non-durable/01_agent_http_streaming/README.md |
Comprehensive documentation for agent streaming sample |
python/samples/getting_started/azure_functions/non-durable/01_agent_http_streaming/demo.http |
Test cases and client examples for agent streaming |
python/samples/getting_started/azure_functions/non-durable/01_agent_http_streaming/requirements.txt |
Python dependencies for agent sample |
python/samples/getting_started/azure_functions/non-durable/01_agent_http_streaming/local.settings.json.template |
Configuration template for agent sample |
python/samples/getting_started/azure_functions/non-durable/01_agent_http_streaming/host.json |
Azure Functions host configuration |
python/samples/getting_started/azure_functions/non-durable/02_workflow_http_streaming/function_app.py |
Multi-agent workflow streaming implementation |
python/samples/getting_started/azure_functions/non-durable/02_workflow_http_streaming/README.md |
Comprehensive documentation for workflow streaming sample |
python/samples/getting_started/azure_functions/non-durable/02_workflow_http_streaming/demo.http |
Test cases and client examples for workflow streaming |
python/samples/getting_started/azure_functions/non-durable/02_workflow_http_streaming/requirements.txt |
Python dependencies for workflow sample |
python/samples/getting_started/azure_functions/non-durable/02_workflow_http_streaming/local.settings.json.template |
Configuration template for workflow sample |
python/samples/getting_started/azure_functions/non-durable/02_workflow_http_streaming/host.json |
Azure Functions host configuration |
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4", | ||
| "AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>" |
There was a problem hiding this comment.
The local.settings.json.template includes both AzureCliCredential (default in code) and AZURE_OPENAI_API_KEY placeholder. This could be confusing since the code uses AzureCliCredential by default and doesn't read the API key from settings. Consider either:
- Removing the API_KEY line and adding a comment explaining how to switch to API key authentication
- Adding code to check for the API_KEY setting and use it if present
The same pattern appears in sample 02 as well.
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4", | |
| "AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>" | |
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4" |
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4", | ||
| "AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>" |
There was a problem hiding this comment.
The local.settings.json.template includes both AzureCliCredential (default in code) and AZURE_OPENAI_API_KEY placeholder. This could be confusing since the code uses AzureCliCredential by default and doesn't read the API key from settings. Consider either:
- Removing the API_KEY line and adding a comment explaining how to switch to API key authentication
- Adding code to check for the API_KEY setting and use it if present
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4", | |
| "AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>" | |
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4" |
| """Stream agent responses in real-time. | ||
|
|
||
| Request body: {"message": "What's the weather in Seattle?"} | ||
| Response: Server-Sent Events stream with text chunks | ||
| """ |
There was a problem hiding this comment.
The function docstring states "Stream agent responses in real-time" but lacks detail about the request/response format and error handling behavior. According to the custom coding guidelines for samples, code should be well-documented with comments explaining the purpose of each step. Consider expanding the docstring to explain the SSE streaming format and error conditions.
| """Stream workflow execution in real-time. | ||
|
|
||
| Request body: {"message": "Research Seattle weather and write about it"} | ||
| Response: Server-Sent Events stream with workflow events | ||
| """ |
There was a problem hiding this comment.
The function docstring states "Stream workflow execution in real-time" but lacks detail about the request/response format and error handling behavior. According to the custom coding guidelines for samples, code should be well-documented with comments explaining the purpose of each step. Consider expanding the docstring to explain the SSE streaming format and what events are emitted.
| const eventSource = new EventSource('http://localhost:7071/api/agent/stream?message=Hello'); | ||
| eventSource.onmessage = (event) => { | ||
| const data = JSON.parse(event.data); | ||
| if (data.text) { | ||
| document.body.innerHTML += data.text; | ||
| } | ||
| }; |
There was a problem hiding this comment.
The demo.http file includes examples with EventSource in JavaScript that assume GET requests with query parameters, but the actual endpoint only accepts POST requests with JSON body. The JavaScript example on line 162 shows:
const eventSource = new EventSource('http://localhost:7071/api/agent/stream?message=Hello');However, EventSource only supports GET requests and cannot send POST requests with JSON bodies. This example will not work with the implemented endpoint which requires POST with a JSON body containing the message field.
| const eventSource = new EventSource('http://localhost:7071/api/agent/stream?message=Hello'); | |
| eventSource.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.text) { | |
| document.body.innerHTML += data.text; | |
| } | |
| }; | |
| async function startStreaming() { | |
| const response = await fetch('http://localhost:7071/api/agent/stream', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ message: 'Hello' }), | |
| }); | |
| if (!response.body) { | |
| console.error('Streaming not supported in this browser.'); | |
| return; | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) { | |
| break; | |
| } | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() ?? ''; | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| if (data.text) { | |
| document.body.innerHTML += data.text; | |
| } | |
| } catch (e) { | |
| console.error('Failed to parse SSE data:', e); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| startStreaming().catch((error) => { | |
| console.error('Streaming error:', error); | |
| }); |
| ```json | ||
| { | ||
| "IsEncrypted": false, | ||
| "Values": { | ||
| "FUNCTIONS_WORKER_RUNTIME": "python", | ||
| "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", | ||
| "AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com", | ||
| "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "gpt-4" | ||
| } | ||
| } |
There was a problem hiding this comment.
The configuration setting "AzureWebJobsFeatureFlags": "EnableWorkerIndexing" is mentioned in the README documentation but does not match the actual template file which uses "PYTHON_ENABLE_INIT_INDEXING": "1". These should be consistent. The template file appears to use the correct setting for HTTP streaming support in Azure Functions Python.
| } | ||
|
|
||
| ### Stream workflow - Creative task | ||
| POST http localhost:7071/api/workflow/stream |
There was a problem hiding this comment.
Missing colon after "POST" in the HTTP request. The correct format should be "POST http://localhost:7071/api/workflow/stream" (with colon after POST, like on line 44) instead of "POST http" without the colon separator.
| POST http localhost:7071/api/workflow/stream | |
| POST http://localhost:7071/api/workflow/stream |
| eventSource.onmessage = (event) => { | ||
| const data = JSON.parse(event.data); | ||
| if (data.text) { | ||
| document.body.innerHTML += data.text; |
There was a problem hiding this comment.
The JavaScript example appends untrusted streaming text directly into the DOM via document.body.innerHTML += data.text, which can enable cross-site scripting if the agent response ever includes attacker-controlled markup (e.g., via user prompts or external data). An attacker could cause the agent to emit HTML with event handlers (like <img onerror=...>) that would execute in the context of your page when inserted with innerHTML. Use a safe text API such as textContent or a proper HTML sanitizer when rendering agent output.
| document.body.innerHTML += data.text; | |
| document.body.textContent += data.text; |
| # eventSource.onmessage = (event) => { | ||
| # const data = JSON.parse(event.data); | ||
| # if (data.text) { | ||
| # document.body.innerHTML += data.text; |
There was a problem hiding this comment.
This JavaScript browser example appends data.text directly into the DOM using document.body.innerHTML += data.text, which allows any HTML emitted by the agent to be interpreted and can lead to cross-site scripting. If an attacker can influence the agent’s output (for example through crafted prompts), they can inject HTML with event handlers that runs in the page context. Prefer using textContent or building DOM nodes safely instead of writing untrusted strings to innerHTML.
| # document.body.innerHTML += data.text; | |
| # const textNode = document.createTextNode(data.text); | |
| # document.body.appendChild(textNode); |
| # output.innerHTML += '<div class="workflow-start">Workflow Started</div>'; | ||
| # break; | ||
| # case 'agent_started': | ||
| # currentAgent = data.agent; | ||
| # output.innerHTML += `<div class="agent-start">[${currentAgent}]</div>`; | ||
| # break; | ||
| # case 'agent_transition': | ||
| # output.innerHTML += `<div class="transition">${data.from} → ${data.to}</div>`; | ||
| # break; | ||
| # case 'text': | ||
| # output.innerHTML += data.text; | ||
| # break; | ||
| # case 'tool_call': | ||
| # output.innerHTML += `<div class="tool">🔧 ${data.tool}</div>`; | ||
| # break; | ||
| # case 'done': | ||
| # output.innerHTML += '<div class="complete">✓ Completed</div>'; | ||
| # eventSource.close(); | ||
| # break; | ||
| # case 'error': | ||
| # output.innerHTML += `<div class="error">Error: ${data.error}</div>`; |
There was a problem hiding this comment.
The workflow JavaScript example repeatedly appends untrusted fields like data.text, data.tool, and data.error into output.innerHTML, which can result in cross-site scripting if any of those values contain attacker-controlled markup. Because these values originate from streamed server responses (ultimately based on user input and model output), an attacker could cause HTML with event handlers to be injected and executed in the browser. Use text-safe APIs (e.g., textContent) or sanitize/escape these values before inserting them into the DOM instead of concatenating into innerHTML.
| # output.innerHTML += '<div class="workflow-start">Workflow Started</div>'; | |
| # break; | |
| # case 'agent_started': | |
| # currentAgent = data.agent; | |
| # output.innerHTML += `<div class="agent-start">[${currentAgent}]</div>`; | |
| # break; | |
| # case 'agent_transition': | |
| # output.innerHTML += `<div class="transition">${data.from} → ${data.to}</div>`; | |
| # break; | |
| # case 'text': | |
| # output.innerHTML += data.text; | |
| # break; | |
| # case 'tool_call': | |
| # output.innerHTML += `<div class="tool">🔧 ${data.tool}</div>`; | |
| # break; | |
| # case 'done': | |
| # output.innerHTML += '<div class="complete">✓ Completed</div>'; | |
| # eventSource.close(); | |
| # break; | |
| # case 'error': | |
| # output.innerHTML += `<div class="error">Error: ${data.error}</div>`; | |
| # const workflowStartDiv = document.createElement('div'); | |
| # workflowStartDiv.className = 'workflow-start'; | |
| # workflowStartDiv.textContent = 'Workflow Started'; | |
| # output.appendChild(workflowStartDiv); | |
| # break; | |
| # case 'agent_started': | |
| # currentAgent = data.agent; | |
| # const agentStartDiv = document.createElement('div'); | |
| # agentStartDiv.className = 'agent-start'; | |
| # agentStartDiv.textContent = `[${currentAgent}]`; | |
| # output.appendChild(agentStartDiv); | |
| # break; | |
| # case 'agent_transition': | |
| # const transitionDiv = document.createElement('div'); | |
| # transitionDiv.className = 'transition'; | |
| # transitionDiv.textContent = `${data.from} → ${data.to}`; | |
| # output.appendChild(transitionDiv); | |
| # break; | |
| # case 'text': | |
| # const textSpan = document.createElement('span'); | |
| # textSpan.textContent = data.text; | |
| # output.appendChild(textSpan); | |
| # break; | |
| # case 'tool_call': | |
| # const toolDiv = document.createElement('div'); | |
| # toolDiv.className = 'tool'; | |
| # toolDiv.textContent = `🔧 ${data.tool}`; | |
| # output.appendChild(toolDiv); | |
| # break; | |
| # case 'done': | |
| # const completeDiv = document.createElement('div'); | |
| # completeDiv.className = 'complete'; | |
| # completeDiv.textContent = '✓ Completed'; | |
| # output.appendChild(completeDiv); | |
| # eventSource.close(); | |
| # break; | |
| # case 'error': | |
| # const errorDiv = document.createElement('div'); | |
| # errorDiv.className = 'error'; | |
| # errorDiv.textContent = `Error: ${data.error}`; | |
| # output.appendChild(errorDiv); |
|
I wonder if the https://github.com/azure-samples org is a better place for these samples than here, since they showcase a way we can use If you would prefer keeping in this repo, a better place would be to move it under |
| # pass | ||
|
|
||
| ### | ||
| # JavaScript Browser Example |
There was a problem hiding this comment.
I'm not sure if the demo.http is the right file for adding all these client examples. Maybe add it in separate file specific to the clients?
|
|
||
| Before running this sample: | ||
|
|
||
| 1. **Azure OpenAI Resource** |
There was a problem hiding this comment.
Common pre-reqs should be added to the parent readme to avoid duplication.
|
|
||
| ## 🚀 Setup | ||
|
|
||
| ### 1. Create Virtual Environment |
There was a problem hiding this comment.
Same here, this readme should be small and clean with specific things only for this sample
| } | ||
| ``` | ||
|
|
||
| ### Using cURL |
There was a problem hiding this comment.
If we're providing sample files for each client then maybe not needed in the Readme. Or just point to those files in the readme
|
|
||
| ## 🆚 Comparison with Durable Samples | ||
|
|
||
| | Feature | This Sample | Durable Samples (01-10) | | ||
| |---------|-------------|-------------------------| | ||
| | Response Mode | Real-time streaming | Fire-and-forget + polling | | ||
| | State Storage | None | Azure Storage/Azurite | | ||
| | Timeout | ~230s (HTTP timeout) | Hours/days | | ||
| | Status Queries | Not supported | Supported | | ||
| | Complexity | Low | Medium-High | | ||
| | Setup Required | Minimal | Storage + orchestration | | ||
|
|
||
| ## ⚠️ Limitations | ||
|
|
||
| 1. **Timeout Constraints** | ||
| - HTTP connections time out (~230 seconds) | ||
| - Not suitable for very long-running tasks | ||
| - Use durable samples for longer executions | ||
|
|
||
| 2. **No State Persistence** | ||
| - Can't query status after completion | ||
| - Can't resume interrupted executions | ||
| - Use durable samples if you need these features | ||
|
|
||
| 3. **No Orchestration Patterns** | ||
| - No built-in concurrency, conditionals, or HITL | ||
| - Use durable samples for complex workflows | ||
|
|
||
| ## 🎓 Next Steps | ||
|
|
||
| - **[02_workflow_http_streaming](../02_workflow_http_streaming)** - Stream multi-agent workflows | ||
| - **[04_single_agent_orchestration_chaining](../../04_single_agent_orchestration_chaining)** - Learn durable orchestration | ||
| - **[07_single_agent_orchestration_hitl](../../07_single_agent_orchestration_hitl)** - Add human-in-the-loop |
There was a problem hiding this comment.
I dont think any of these lines are necessary since you already do cover it in the parent readme
| @@ -0,0 +1,5 @@ | |||
| agent-framework | |||
There was a problem hiding this comment.
This already includes agent-framework-azure
|
Stale, and both the packages and samples have had major updates, so feel free to reopen a new PR if needed |
Motivation and Context
This PR addresses the need for simpler Azure Functions samples that demonstrate real-time agent and workflow execution without the complexity of durable orchestration. Many developers want to:
Description
This PR adds two new samples under non-durable that demonstrate stateless HTTP streaming for agents and workflows:
1. 01_agent_http_streaming
get_weather)azurefunctions-extensions-http-fastapiforStreamingResponseagent.run_stream()pattern2. 02_workflow_http_streaming
SequentialBuilder().participants([...])patternAgentRunUpdateEventto extract text viaevent.data.textKey Implementation Details:
AzureCliCredentialfor authenticationdata: {"text": "chunk"}\n\n(SSE)PYTHON_ENABLE_INIT_INDEXING=1for HTTP streamingSupporting Files:
demo.httpfiles with multiple test casesrequirements.txtwith all dependencieslocal.settings.json.templatefor configurationDocumentation Highlights:
Contribution Checklist