-
Notifications
You must be signed in to change notification settings - Fork 939
Python: Add Non-durable Azure Functions Samples #2987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature-durabletask-python
Are you sure you want to change the base?
Python: Add Non-durable Azure Functions Samples #2987
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>" |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>" |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 | ||
| """ |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 | ||
| """ |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; | ||
| } | ||
| }; |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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" | ||
| } | ||
| } |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>`; |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This already includes agent-framework-azure
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