.NET: Python: Merge main into feature-durabletask-python branch (#3160)

* Python: Add factory pattern to concurrent orchestration builder (#2738)

* Add factory pattern to concurrent orchestration builder

* Update readme

* Address AI comments

* Fix unit tests

* Fix import

* Prevent multiple calls to set participants or factories

* Add comments

* Mitigate warnings

* Fix mypy

* Address comments

* Address Copilot comments

* Fix tests

* Python: fix: GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outpu… (#2750)

* fix: ManagerSelectionResponse JSON Schema for OpenAI Structured Output Strict Mode

* refactor: install pre-commit then commit again

* Capture file IDs from code interpreter in streaming responses (#2741)

* .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)

* prevent nulls in AIAgent property

* address feedback

* code ql sm04598 (#2723)

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* .NET: Add Conversation State Sample (Step05) (#2697)

* Initial plan

* Add Agent_OpenAI_Step05_Conversation sample for conversation state management

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update Program.cs comment to accurately describe the sample

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update the code to use the ConversationClient more in line with the samples in OpenAI

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Changing sample to use ChatClientAgent and conversationId in GetNewThread

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.7 to 4.0.4.11 (#2777)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.Identity from 1.17.0 to 1.17.1 (#2780)

---
updated-dependencies:
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2778)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: added more complete parsing for mcp tool arguments (#2756)

* added more complete parsing for mcp tool arguments

* fixed mypy

* added nonlocal model counter, and some fixes

* fixes in naming logic

* extracted json parsing function, added parametrized test and checked coverage

* Python: Updated package versions (#2784)

* Updated package versions

* Small fix

* Bump actions/checkout from 5 to 6 (#2404)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: adds support for labels in edges,  fixes rendering of labels in dot a… (#1507)

* adds support for labels in edges,  fixes rendering of labels in dot and mermaid, adds rendering of labels in edges

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* escaping edge labels, adding tests for labels containing strange characters that would break the diagram and enabling the previous signature so the API has backwards compatibility.

* Unify label in EdgeData

* Edge API adjustments, removed useless "sanitizer"

* fixed test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Added custom args and thread object to ai_function kwargs (#2769)

* Added an example of using kwargs in ai_function

* Added thread object to ai_function kwargs

* Updated docs

* Small fix

* Added thread parameter filtering

* Fix WorkflowAgent to include thread convo history. Enable checkpointing. (#2774)

* Update OpenAIResponses.yaml to match AgentSchema (#2598)

1. Update `connection` child types --  `kind: ApiKey` to `kind: key` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/apikeyconnection/

2.  Update `outputSchema`'s `PropertySchema` to be `kind` instead of `type` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/propertyschema/

* Python: Remove warnings from workflow builder on not using factories (#2808)

* Revert concurrent

* Fix comments

* Python: Filter framework kwargs from MCP tool invocations (#2870)

* Filter framework kwargs from MCP tool invocations

* Fixes

* Python: Fix WorkflowAgent to emit yield_output as agent response (#2866)

* Fix WorkflowAgent to emit yield_output as agent response

* use raw_representation

* Raw representation handling

* Python: Use agent description in HandoffBuilder auto-generated tools (#2713) (#2714)

## Summary
Enhanced `HandoffBuilder._apply_auto_tools` to use the target agent's
description when creating handoff tools, providing more informative tool
descriptions for LLMs.

## Changes
- Modified `_apply_auto_tools` to extract `description` from
  `AgentExecutor._agent` when available
- Updated iteration to use `.items()` for more efficient dict traversal
- Handoff tools now use agent descriptions instead of generic placeholders

## Example
Before: "Handoff to the refund_agent agent."
After: "You handle refund requests. Ask for order details and process refunds."

## Testing
- All handoff tests pass (20/20)
- No breaking changes to existing API

Fixes #2713

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* Python: [BREAKING] Observability updates (#2782)

* fixes Python: Add env_file_path parameter to setup_observability() similar to AzureOpenAIChatClient
Fixes #2186

* WIP on updates using configure_azure_monitor

* improved setup and clarity

* fixed root .env.example

* revert changes

* updated files

* updated sample

* updated zero code

* test fixes and fixed links

* fix devui

* removed planning docs

* added enable method and updated readme and samples

* clarified docstring

* add return annotation

* updated naming

* update capatilized version

* updated readme and some fixes

* updated decorator name inline with the rest

* feedback from comments addressed

* Python: Fix middleware terminate flag to exit function calling loop immediately (#2868)

* Fix middleware terminate flag to exit function calling loop immediately

* Eliminating duck typing

* Improve function exec result handling

* Fix race condition

* Fix mypy issues

* Python: Fix context duplication in handoff workflows when restoring from checkpoint (#2867)

* Fix context duplication in handoff workflows when restoring from checkpoint

* Address Copilot PR review

* .NET: Update to latest Azure.AI.*, OpenAI, and M.E.AI* (#2850)

* Update to latest Azure.AI.*, OpenAI, and M.E.AI*

Absorb breaking changes in Responses surface area

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Using patch to remove the model is necessary, updated the response client to actually use the the ForAgent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Bump actions/download-artifact from 6 to 7 (#2862)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#2861)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 5 to 6 (#2860)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python : Ollama Connector for Agent Framework (#1104)

* Initial Commit for Olama Connector

* Added Olama Sample

* Add Sample & Fixed Open Telemetry

* Fixed Spelling from Olama to Ollama

* remove"opentelemetry-semantic-conventions-ai ~=0.4.13" since its handled in a different pr

* Added Tool Calling

* Finalizing test cases

* Adjust samples to be more reliable

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/tests/test_ollama_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Improved Docstrings & Sample

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Integrate PR Feedback
- Divided Streaming and Non-Streaming into independent Methods
- Catch Ollama Validation Error
- Add OTEL Provider Name
- Checked Ollama Messages
- Add Usage Statistics

* Revert setting, so it can be none

* Validate Message formatting between AF and Ollama

* Catch Ollama Error and raise a ServiceResponse Error

* Fix mypy error

* remove .vscode comma

* Add Reasoning support & adjust to new structure

* Add Ollama Multimodality and Reasoning

* Add test cases for reasoning

* Add Tests for Error Handling in Ollama Client

* Update python/samples/getting_started/multimodal_input/ollama_chat_multimodal.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Integrated Copilot Feedback

* Implement first PR Feedback

* Adjust Readme files for examples

* Adjust argument passing via additional chat options

* Implemented PR Feedback

* Removing Ollama Package from Core and moving samples

* Fix Link & Adding Samples to Main Sample Readme

* Fixing Links in Readme

* Moved Multimodal and Chat Example

* Fixed Link in ChatClient to Ollama

* Fix AgentFramework Links in Ollama Project

* Fix observability breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Skip failing IT (#2904)

* .NET: Cosmos DB UT Fast Skip (For Non-Configured Local envs) (#2906)

* Cosmos DB UT Fast Skip (Non-Configured Local envs) + Long running UT skip in pipeline when no CosmosDB changes happened

* Force a CosmosDB source code change to trigger the pipeline

* Address possible string boolean mismatch

* Add debug

* Enabling emulator always when running IT

* .NET: Add TTLs to durable agent sessions (#2679)

* .NET: Add TTLs to durable agent sessions

* Remove unnecessary async

* PR feedback: clarify UTC

* PR feedback: limit minimum signal delay to <= 5 minutes

* PR feedback: Fix TTL disablement

* Linter: use auto-property

* Fix build break from OpenAI SDK change

* Updated CHANGELOG.md

* PR feedback

* Reduce default TTL to 14 days to work around DTS bug

* Python:  Update Mem0Provider to use v2 search API `filters` parameter (#2766)

* short fix to move id parameters to filters object

* added tests

* small fix

* mem0 dependency update

* Updated package versions (#2913)

* .NET: Switch to new "Run" method name. (#2843)

* Switch to new "RunAgent" method name.

* Try to disable false positive naming warning.

* Add comment about disabled warnings.

* Rename `RunAgent` to just `Run`.

* Update CHANGELOG.

* Python: Switch to new "run" method name. (#2890)

* Switch to `run` method.

* Add support for deprecated `run_agent`.

* Fix entity method name.

* Fix method name and improve tests.

* Update comment.

* Update Python CHANGELOG.

* [BREAKING] Python: Add factory pattern to handoff orchestration builder (#2844)

* WIP: Factory pattern to handoff

* Add factory pattern to concurrent orchestration builder; Next: tests and sample verification

* Add tests and improve comments

* Fix mypy

* Simplify handoff_simple.py

* Simplify handoff_autonoumous.py and bug fix

* Update readme

* Address Copilot comments

* Python: Flow custom kwargs to agents via Workflow SharedState (#2894)

* Flow custom kwargs to agents via SharedState

* Address Copilot feedback

* Improve sample typing

* Fix test

* Fix Pydantic error when using Literal type for tool params (#2893)

* Updated Ollama package version (#2920)

* Python: Azure AI Agent with Bing Grounding Citations Sample (#2892)

* bing grounding sample with citations

* small fix

* fix

* .NET: Make DelegatingAIAgent abstract (#2797)

* Initial plan

* Make DelegatingAIAgent abstract

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Added additional arguments for Azure AI agent (#2922)

* Python: Correction of MCP image type conversion in  _mcp.py (#2901)

* Correction of MCP image type conversion in  _mcp.py

* Added a new overload to the init function of the DataContent() type of the Agent Framework, edited the test case to correctly test the usage of the data and uri fields while using DataContent()

* Fixed tests related to the changes of the DataContent type, added testing for both string and byte representations

* Pass kwargs into subworkflows (#2923)

* Python: Move ollama samples to samples getting started dir (#2921)

* Move ollama samples to samples getting started dir

* Address feedback

* Python: fix: correct BadRequestError when using Pydantic model in response_fo… (#1843)

* fix: correct BadRequestError when using Pydantic model in response_format

* Fix lint

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>

* .NET: [Breaking] Delete display name property (#2758)

* delete the AIAgent.DisplayName property

* use agent name as a first value for activity display name

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: cleanup and refactoring of chat clients (#2937)

* refactoring and unifying naming schemes of internal methods of chat clients

* set tool_choice to auto

* fix for mypy

* added note on naming and fix #2951

* fix responses

* fixes in azure ai agents client

* Python: Workflow add option to visualize internal executors (#2917)

* Workflow add option to visualize internal executors

* Address Copilot comments

* Python: Fixes Run ID and Thread ID casing to align with AG-UI Typescript SDK (#2948)

* added camelCase input to run id and thread id aligning with @ag-ui/core

* fixed per copilot suggestions

* Python: Add workflow cancellation sample (#2732)

* Add workflow cancellation sample

Add sample demonstrating how to cancel a running workflow using asyncio
tasks. Shows both cancellation mid-execution and normal completion paths.
Useful for implementing timeouts, graceful shutdown, or A2A executors.

* update docstring

* .NET: Update Anthropic package to version 12.0.0 (#2914)

* Initial plan

* Update Anthropic package to version 12.0.0

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Python: Add Azure Managed Redis Support with Credential Provider (#2887)

* azure redis support

* small fixes

* azure managed redis sample

* fixes

* Bump CommunityToolkit.Aspire.OllamaSharp from 13.0.0-beta.440 to 13.0.0 (#2856)

---
updated-dependencies:
- dependency-name: CommunityToolkit.Aspire.OllamaSharp
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.11 to 4.0.5 (#2853)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2854)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Fix WorkflowAgent event handling and kwargs forwarding (#2946)

* Fix kwargs propagation through workflow.as_agent()

* Fix WorkflowAgent to respect AgentExecutor output_response setting

* .NET: Use GrpcEntityRunner instead of TaskEntityDispatcher (#2759)

* Use GrpcEntityRunner instead of TaskEntityDispatcher

* Pin to Durable worker 1.11.0

* Set the invocation result

* Update all Durable packages

* Update changelog, rename dispatcher to encondedEntityRequest

* Python: Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG (#2968)

* Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG

* update lock

* Fix formatting

* Fix ChatKit typing

* Python: Introducing Foundry Local Chat Clients (#2915)

* redo foundry local chat client

* fix mypy and spelling

* better docstring, updated sample

* fixed tests and added tests

* small sample update

* Updated package versions (#2978)

* Python: Added GitHub MCP sample with PAT (#2967)

* added github mcp sample with PAT

* addressed copilot fixes

* env fix

* Python: Preserve reasoning blocks with OpenRouter (#2950)

* Preserve reasoning blocks with OpenRouter

* Put encrypted reasoning in TextReasoningContent

* Remove unneccessary change

* Fix docs

* Support streaming

* Fix handling None in TextReasoningContent.text

* Python: Added response.created and response.in_progress event process to OpenAIBaseResponseClient (#2975)

* added response.created and response.in_progress to include response.id

* better doc string

* added tests for the new streaming event types

* Python: Introducing support for Bedrock-hosted models (Anthropic, Cohere, etc.) (#2610)

* Pushing the bedrock related changes to the new branch after addressing the review comments

* 2524 Addressed the second round review comments

* 2524 Addressed few more minor comments on the PR

* resolving the merge conflict

* 2524 resolved the uv.lock conflicts

* 2524 addressed more comments

* 2524 removed the print statement to fix the checks failure

* 2524 resolved the CI failure issues

* 2524 fixing the CI breaks

* 2524 Addressed the review comment

* 2524 resolved conflict

---------

Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>

* .NET: [Durable Agents] Reliable streaming sample (#2942)

* .NET: [Durable Agents] Reliable streaming sample

* Add automated validation for new sample

* Address Copilot PR feedback

* Fix typo in README.md about agent definitions (#2634)

* Fix typo in README.md about agent definitions

* Update agent-samples/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: latency improvements (#3014)

* latency improvements

* fixed mypy, added coding standards and instructions

* slight logic improvement

* Python: Updated package versions (#3024)

* Updated package versions

* Updated changelog

* Python: add powerfx safe mode (#3028)

* add powerfx safe mode

* improved docstring and aligned env_file loading

* ensured test uses reset

* .NET: [Breaking] Introduce RunCoreAsync/RunCoreStreamingAsync delegation pattern in AIAgent (#2749)

* Initial plan

* Refactor AIAgent: Make RunAsync and RunStreamingAsync non-abstract, add RunCoreAsync and RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix infinite recursion in test implementations

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Make RunAsync and RunStreamingAsync non-virtual as requested

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix DelegatingAIAgent subclasses to use RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix XML documentation references in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Restore <see cref> tags with proper qualified signatures in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Rollback unnecessary XML documentation changes in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Remove pragma and update crefs to RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix EntityAgentWrapper to call base.RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* fix compilation issues

* fix compilatio issue

* fix tests

* fix unit tests

* fix unit test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* add issue template and additional labeling (#3006)

* fix and extra int test (#3037)

* .NET: [BREAKING] Refactor ChatMessageStore methods to be similar to AIContextProvider and add filtering support (#2604)

* Refactor ChatMessageStore methods to be similar to AIContextProvider

* Fix file encoding

* Ensure that AIContextProvider messages area also persisted.

* Update formatting and seal context classes

* Improve formatting

* Remove optional messages from constructor and add unit test

* Add ChatMessageStore filtering via a decorator

* Update sample and cosmos message store to store AIContextProvider messages in right order. Fix unit tests.

* Update Workflowmessage store to use aicontext provider messages.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Improve xml docs messaging

* Address code review comments.

* Also notify message store on failure

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* [BREAKING] Remove unused AgentThreadMetadata (#3067)

* Remove unused AgentThreadMetadata

* Update DurableTask Changelog

* Python: Fix AzureAIClient failure when conversation history contains assistant messages (#3076)

* Fix AzureAIClient failure when conversation history contains assistant messages

* Address PR review feedback: improve docstring and test assertions

* Remove redundant cast

* Fix: Update OTLP exporter protocol conditions (#3070)

* Python: Fix ExecutorInvokedEvent and ExecutorCompletedEvent observability data (#3090)

* Fix ExecutorInvokedEvent.data mutation bug

* Fix bug related to not yielding output type

* .NET: Seal ChatClientAgentThread (#2842)

* Initial plan

* Seal ChatClientAgentThread class

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix broken strands urls. (#3102)

* Fix broken strands urls.

* Fix typos

* .NET: Fix message ordering inconsistency when using AIContextProvider (#2659)

* Initial plan

* Fix message ordering inconsistency when using AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Revert to original message ordering: Input, AIContextProvider, Response

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Reorder messages to ChatClient to match MessageStore order: Existing, Input, AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Remove redundant test methods as existing tests already verify the behavior

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* fix: tool_choice parameter not being honored when passed to agent.run() (#3095)

* sharepoint sample fix (#3108)

* Bump versions to 1.0.0b260106 for a release. Update CHANGELOG.md (#3109)

* Bump Bedrock version to latest (#3110)

* Python: Fix MCP tool result serialization for list[TextContent] (#2523)

* Fix MCP tool result serialization for list[TextContent]

When MCP tools return results containing list[TextContent], they were
incorrectly serialized to object repr strings like:
'[<agent_framework._types.TextContent object at 0x...>]'

This fix properly extracts text content from list items by:
1. Checking if items have a 'text' attribute (TextContent)
2. Using model_dump() for items that support it
3. Falling back to str() for other types
4. Joining single items as plain text, multiple items as JSON array

Fixes #2509

* Address PR review feedback for MCP tool result serialization

- Extract serialize_content_result() to shared _utils.py
- Fix logic: use texts[0] instead of join for single item
- Add type annotation: texts: list[str] = []
- Return empty string for empty list instead of '[]'
- Move import json to file top level
- Add comprehensive unit tests for serialization

* Address PR review feedback: fix type checking and double serialization

- Add isinstance(item.text, str) check to ensure text attribute is a string
- Fix double-serialization issue by keeping model_dump results as dicts
  until final json.dumps (removes escaped JSON strings in arrays)
- Improve docstring with detailed return value documentation
- Add test for non-string text attribute handling
- Add tests for list type tool results in _events.py path

* Simplify PR: minimal changes to fix MCP tool result serialization

Addresses reviewer feedback about excessive refactoring:
- Reset _events.py to original structure
- Only add import and use serialize_content_result in one location
- All review comments addressed in serialize_content_result():
  - Added isinstance(item.text, str) check
  - Use model_dump(mode="json") to avoid double-serialization
  - Improved docstring with explicit return value documentation
  - Empty list returns "" instead of "[]"

* Refactor: Move MCP TextContent serialization to core prepare_function_call_results

Per reviewer feedback, moved the TextContent serialization logic from
ag-ui's serialize_content_result to the core package's
prepare_function_call_results function.

Changes:
- Added handling for objects with 'text' attribute (like MCP TextContent)
  in _prepare_function_call_results_as_dumpable
- Removed serialize_content_result from ag-ui/_utils.py
- Updated _events.py and _message_adapters.py to use
  prepare_function_call_results from core package
- Updated tests to match the core function's behavior

* Fix failing tests for prepare_function_call_results behavior

- test_tool_result_with_none: Update expected value to 'null' (JSON serialization of None)
- test_tool_result_with_model_dump_objects: Use Pydantic BaseModel instead of plain class

* Fix B903 linter error: Convert MockTextContent to dataclass

The ruff linter was reporting B903 (class could be dataclass or namedtuple)
for the MockTextContent test helper classes. This commit converts them to
dataclasses to satisfy the linter check.

* Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)

* Improve DevUI, add Context Inspector view as new tab under traces

* fix mypy errors

* fix: Handle stale MCP connections in DevUI executor

MCP tools can become stale when HTTP streaming responses end - the underlying
stdio streams close but `is_connected` remains True. This causes subsequent
requests to fail with `ClosedResourceError`.

Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before
agent execution. This is a workaround for an upstream Agent Framework issue
where connection state isn't properly tracked.

Fixes MCP tools failing on second HTTP request in DevUI.

fixes  #1476 #1515 #2865

* fix #1572 report import dependency errors more clearly

* Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode?

* remove unused dead code

* improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly

* update ui build

* update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes

* .NET: Seal factory contexts and add non JSO deserialize overloads (#3066)

* Seal factory contexts and add non JSO deserialize overloads

* 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>

* Enable blank issues in issue template configuration

Need to re-enable creating blank issues

* updated templates (#3106)

* updated templates

* enabled blank and fixed triage

* made language optional and moved to the bottom for features

* Python: Streaming sample for azurefunctions (#3057)

* Streaming sample for azurefunctions

* Fixed links and sample name

* Addressed feedback

* Addressed feedback

* Fixed integration tests

* Updated test

* Python: fix(azure-ai): Fix response_format handling for structured outputs (#3114)

* fix(azure-ai): read response_format from chat_options instead of run_options

* refactor: use explicit None checks for response_format

* Fix mypy error

* Mypy fix

* Python: Bump python version to 1.0.0b260107 for a release (#3128)

* Bump python version to 1.0.0b260107 for a release

* Update changelog

* Make A2AAgent public, so that it's concrete implementation methods can be used. (#3119)

* .NET: Map additional props <-> A2A metadata (#3137)

* map additional props from agent run options to a2a request metadata

* small touches

* add unit tests for new extension methods

* Sort using

* add unit test

* add additiona unit tests

* special case json element to avoid unnecessary serialization

* Python: Fix Anthropic streaming response bugs (#3141)

* test commit identity

* fix(anthropic): fix raw_representation and finish_reason in streaming

* lint fix

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.5 to 4.0.5.1 (#2994)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump Anthropic from 12.0.0 to 12.0.1 (#2993)

---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: [Breaking] Prevent loss of input messages & streamed updates when resuming streaming (#2748)

* save input messages and stream updates to the continuation token to be able to use them in the last successful stream resumption call.

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

* init continuation token from chat response

* remove unnecessary types for source generation

* remove check for continuation token passed at initial run

* remove check for continuation token pass at initial run

* centralize continuation token parsing

* update xml comments

* use readonly collection instead of enumerable

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .NET: fix: Expose WorkflowErrorEvent as ErrorContent (#2762)

* fix: Expose WorkflowErrorEvent as ErrorContent

When hosted using .AsAgent(), Workflows were not exposing inner errors coming as Exceptions (through the WorkflowErrorEvent)

The fix is to convert their message to an ErrorContent on the way out, rather than rely on the default "empty update" to collect the raw event.

* feat: Add a way to show/suppress exception information

* Bump Microsoft.Agents.AI.Workflows from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1 (#2997)

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.Workflows
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* .NET: Add Run overloads to expose ChatClientAgentRunOptions in IntelliSense (#3115)

* Initial plan

* Add ChatClientAgentExtensions for improved discoverability of ChatClientAgentRunOptions

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Address code review feedback - use collection expression syntax

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

* Fix issues with Copilot implementation

* Add additional tests for structured output overloads.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Python: Add tool call/result content types and update connectors and samples (#2971)

* Add new AI content types and image tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Add Python content types for tool calls/results and image generation tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Address review feedback for tool content and samples

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Tighten image generation typing and sample tools list

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Align image generation output typing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Handle MCP naming, image options mapping, and connector tool content

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Allow MCP call in function approval request

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Remove raw image_generation tool remapping

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Restore Anthropic tool_use to function calls unless code execution

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix lint issues for hosted file docstring and MCP parsing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Import ChatResponse types in Anthropic client

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix Anthropics citation type imports and MCP typing for handoff/tools

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Skip lightning tests without agentlightning and fix function call import

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* fix lint on lab package

* rebuilt anthropic parsing

* redid anthropic parsing

* typo

* updated parsing and added missing docstrings

* fix tests

* mypy fixes

* second mypy fix

* add new class to other samples

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* Bump Google.GenAI from 0.6.0 to 0.9.0 (#2995)

---
updated-dependencies:
- dependency-name: Google.GenAI
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump js-yaml from 4.1.0 to 4.1.1 in /python/packages/devui/frontend (#3123)

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Updated package versions (#3144)

* .NET: Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI (#2996)

* Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI

Bumps Microsoft.Agents.AI.OpenAI from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1
Bumps Microsoft.Extensions.AI.OpenAI from 10.1.0-preview.1.25608.1 to 10.1.1-preview.1.25612.2

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed samples

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Python: fix(ag-ui): Execute tools with approval_mode, fix shared state, code cleanup  (#3079)

* fix(ag-ui): execute tools after approval in human-in-the-loop flow

* Fix shared state bug

* Bug fix finalized

* Refactoring to clean up code

* Code cleanup

* More fixes

* More code cleanup

* Add version detection in __init__.py to ruff ignore list

* Track agent name with updates for workflow agent (#3146)

* Python: Fix AzureAIClient tool call bug for AG-UI use (#3148)

* Fiz AzureAIClient tool call bug

* Address copilot feedback

* Revert to match main

* revert file to main

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Kurt <65111699+q33566@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Jose Luis Latorre Millas <joslat@gmail.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Richard Ortega <richardjortega@gmail.com>
Co-authored-by: 刘邦学AI <lbbniu@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Nico Möller <nkm-moeller@mail.de>
Co-authored-by: Chris Gillum <cgillum@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Phillip Hoff <phillip.hoff@gmail.com>
Co-authored-by: Ege Ozan Özyedek <36128615+egeozanozyedek@users.noreply.github.com>
Co-authored-by: samueljohnsiby <66901393+samueljohnsiby@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
Co-authored-by: Hao Luo <338265+howlowck@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Jacob Viau <javia@microsoft.com>
Co-authored-by: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com>
Co-authored-by: Sunil Dutta <dutta.2003@gmail.com>
Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>
Co-authored-by: Syrine Chelly <62653967+SyChell@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: takanori-terai <123897708+takanori-terai@users.noreply.github.com>
Co-authored-by: claude89757 <138977524+claude89757@users.noreply.github.com>
Co-authored-by: Gavin Aguiar <80794152+gavin-aguiar@users.noreply.github.com>
Co-authored-by: Sukeesh <vsukeeshbabu@gmail.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>
This commit is contained in:
Laveesh Rohra
2026-01-09 15:58:49 -08:00
committed by GitHub
Unverified
parent e3eff65a6b
commit 1e36ba33c4
184 changed files with 17907 additions and 3316 deletions
+1 -3
View File
@@ -155,7 +155,6 @@ The recommended way to use Ollama is via the native `OllamaChatClient` from the
| [`getting_started/chat_client/openai_responses_client.py`](./getting_started/chat_client/openai_responses_client.py) | OpenAI Responses Client Direct Usage Example |
## Context Providers
### Mem0
@@ -231,14 +230,13 @@ The recommended way to use Ollama is via the native `OllamaChatClient` from the
| [`getting_started/multimodal_input/openai_chat_multimodal.py`](./getting_started/multimodal_input/openai_chat_multimodal.py) | OpenAI Chat with multimodal (image) input example |
## Azure Functions
| Sample | Description |
|--------|-------------|
| [`getting_started/azure_functions/01_single_agent/`](./getting_started/azure_functions/01_single_agent/) | Host a single agent in Azure Functions with Durable Extension HTTP endpoints and per-session state. |
| [`getting_started/azure_functions/02_multi_agent/`](./getting_started/azure_functions/02_multi_agent/) | Register multiple agents in one function app with dedicated run routes and a health check endpoint. |
| [`getting_started/azure_functions/03_callbacks/`](./getting_started/azure_functions/03_callbacks/) | Capture streaming response telemetry via Durable Extension callbacks exposed through HTTP APIs. |
| [`getting_started/azure_functions/03_reliable_streaming/`](./getting_started/azure_functions/03_reliable_streaming/) | Implement reliable streaming for durable agents using Redis Streams with cursor-based resumption. |
| [`getting_started/azure_functions/04_single_agent_orchestration_chaining/`](./getting_started/azure_functions/04_single_agent_orchestration_chaining/) | Chain sequential agent executions inside a durable orchestration while preserving the shared thread context. |
| [`getting_started/azure_functions/05_multi_agent_orchestration_concurrency/`](./getting_started/azure_functions/05_multi_agent_orchestration_concurrency/) | Run two agents concurrently within a durable orchestration and combine their domain-specific outputs. |
| [`getting_started/azure_functions/06_multi_agent_orchestration_conditionals/`](./getting_started/azure_functions/06_multi_agent_orchestration_conditionals/) | Route orchestration logic based on structured agent responses for spam detection and reply drafting. |
@@ -3,7 +3,7 @@ import asyncio
from pathlib import Path
import aiofiles
from agent_framework import DataContent
from agent_framework import DataContent, HostedImageGenerationTool
from agent_framework.azure import AzureAIClient
from azure.identity.aio import AzureCliCredential
@@ -29,12 +29,13 @@ async def main() -> None:
name="ImageGenAgent",
instructions="Generate images based on user requirements.",
tools=[
{
"type": "image_generation",
"model": "gpt-image-1-mini",
"quality": "low",
"size": "1024x1024",
}
HostedImageGenerationTool(
options={
"model": "gpt-image-1-mini",
"quality": "low",
"size": "1024x1024",
}
)
],
) as agent,
):
@@ -28,7 +28,11 @@ async def main() -> None:
tools={
"type": "sharepoint_grounding_preview",
"sharepoint_grounding_preview": {
"project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"]
"project_connections": [
{
"project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"],
}
]
},
},
) as agent,
@@ -3,7 +3,7 @@
import asyncio
import base64
from agent_framework import DataContent, UriContent
from agent_framework import DataContent, HostedImageGenerationTool, ImageGenerationToolResultContent, UriContent
from agent_framework.openai import OpenAIResponsesClient
"""
@@ -51,14 +51,12 @@ async def main() -> None:
agent = OpenAIResponsesClient().create_agent(
instructions="You are a helpful AI that can generate images.",
tools=[
{
"type": "image_generation",
# Core parameters
"size": "1024x1024",
"background": "transparent",
"quality": "low",
"format": "webp",
}
HostedImageGenerationTool(
options={
"size": "1024x1024",
"output_format": "webp",
}
)
],
)
@@ -72,9 +70,11 @@ async def main() -> None:
# Show information about the generated image
for message in result.messages:
for content in message.contents:
if isinstance(content, (DataContent, UriContent)) and content.uri:
show_image_info(content.uri)
break
if isinstance(content, ImageGenerationToolResultContent) and content.outputs:
for output in content.outputs:
if isinstance(output, (DataContent, UriContent)) and output.uri:
show_image_info(output.uri)
break
if __name__ == "__main__":
@@ -4,7 +4,7 @@ import asyncio
import base64
import anyio
from agent_framework import DataContent
from agent_framework import DataContent, HostedImageGenerationTool
from agent_framework.openai import OpenAIResponsesClient
"""OpenAI Responses Client Streaming Image Generation Example
@@ -45,12 +45,13 @@ async def main():
agent = OpenAIResponsesClient().create_agent(
instructions="You are a helpful agent that can generate images.",
tools=[
{
"type": "image_generation",
"size": "1024x1024",
"quality": "high",
"partial_images": 3,
}
HostedImageGenerationTool(
options={
"size": "1024x1024",
"quality": "high",
"partial_images": 3,
}
)
],
)
@@ -2,10 +2,14 @@
import asyncio
from agent_framework import ChatAgent, ChatResponse, HostedCodeInterpreterTool
from agent_framework import (
ChatAgent,
CodeInterpreterToolCallContent,
CodeInterpreterToolResultContent,
HostedCodeInterpreterTool,
TextContent,
)
from agent_framework.openai import OpenAIResponsesClient
from openai.types.responses.response import Response as OpenAIResponse
from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall
"""
OpenAI Responses Client with Code Interpreter Example
@@ -30,15 +34,20 @@ async def main() -> None:
result = await agent.run(query)
print(f"Result: {result}\n")
if (
isinstance(result.raw_representation, ChatResponse)
and isinstance(result.raw_representation.raw_representation, OpenAIResponse)
and len(result.raw_representation.raw_representation.output) > 0
and isinstance(result.raw_representation.raw_representation.output[0], ResponseCodeInterpreterToolCall)
):
generated_code = result.raw_representation.raw_representation.output[0].code
print(f"Generated code:\n{generated_code}")
for message in result.messages:
code_blocks = [c for c in message.contents if isinstance(c, CodeInterpreterToolCallContent)]
outputs = [c for c in message.contents if isinstance(c, CodeInterpreterToolResultContent)]
if code_blocks:
code_inputs = code_blocks[0].inputs or []
for content in code_inputs:
if isinstance(content, TextContent):
print(f"Generated code:\n{content.text}")
break
if outputs:
print("Execution outputs:")
for out in outputs[0].outputs or []:
if isinstance(out, TextContent):
print(out.text)
if __name__ == "__main__":
@@ -1,83 +0,0 @@
# 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:** This is a streaming example that currently uses a local in-memory store for simplicity.
> For distributed environments, consider using Redis, Service Bus, or another pub/sub mechanism for
> callback coordination.
## 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
}
}
]
```
@@ -1,30 +0,0 @@
### 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}}
@@ -1,185 +0,0 @@
"""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 AgentRunResponse, 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.
# NOTE: This is a streaming example using a local console logger. For distributed environments,
# consider using Redis or Service Bus for callback coordination across multiple instances.
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: AgentRunResponse, 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,132 @@
# Agent Response Callbacks with Redis Streaming
This sample demonstrates how to use Redis Streams with agent response callbacks to enable reliable, resumable streaming for durable agents. Clients can disconnect and reconnect without losing messages by using cursor-based pagination.
## Key Concepts Demonstrated
- Using `AgentResponseCallbackProtocol` to capture streaming agent responses
- Persisting streaming chunks to Redis Streams for reliable delivery
- Building a custom HTTP endpoint to read from Redis with Server-Sent Events (SSE) format
- Supporting cursor-based resumption for disconnected clients
- Managing Redis client lifecycle with async context managers
## Prerequisites
In addition to the common setup steps in `../README.md`, this sample requires Redis:
```bash
# Start Redis
docker run -d --name redis -p 6379:6379 redis:latest
```
Update `local.settings.json` with your Redis connection string:
```json
{
"Values": {
"REDIS_CONNECTION_STRING": "redis://localhost:6379"
}
}
```
## Running the Sample
### Start the agent run
The agent executes in the background via durable orchestration. The `RedisStreamCallback` persists streaming chunks to Redis:
```bash
curl -X POST http://localhost:7071/api/agents/TravelPlanner/run \
-H "Content-Type: text/plain" \
-d "Plan a 3-day trip to Tokyo"
```
Response (202 Accepted):
```json
{
"status": "accepted",
"response": "Agent request accepted",
"conversation_id": "abc-123-def-456",
"correlation_id": "xyz-789"
}
```
### Stream the response from Redis
Use the custom `/api/agent/stream/{conversation_id}` endpoint to read persisted chunks:
```bash
curl http://localhost:7071/api/agent/stream/abc-123-def-456 \
-H "Accept: text/event-stream"
```
Response (SSE format):
```
id: 1734649123456-0
event: message
data: Here's a wonderful 3-day Tokyo itinerary...
id: 1734649123789-0
event: message
data: Day 1: Arrival and Shibuya...
id: 1734649124012-0
event: done
data: [DONE]
```
### Resume from a cursor
Use a cursor ID from an SSE event to skip already-processed messages:
```bash
curl "http://localhost:7071/api/agent/stream/abc-123-def-456?cursor=1734649123456-0" \
-H "Accept: text/event-stream"
```
## How It Works
### 1. Redis Callback
The `RedisStreamCallback` class implements `AgentResponseCallbackProtocol` to capture streaming updates:
```python
class RedisStreamCallback(AgentResponseCallbackProtocol):
async def on_streaming_response_update(self, update, context):
# Write chunk to Redis Stream
async with await get_stream_handler() as handler:
await handler.write_chunk(thread_id, update.text, sequence)
async def on_agent_response(self, response, context):
# Write end-of-stream marker
async with await get_stream_handler() as handler:
await handler.write_completion(thread_id, sequence)
```
### 2. Custom Streaming Endpoint
The `/api/agent/stream/{conversation_id}` endpoint reads from Redis:
```python
@app.route(route="agent/stream/{conversation_id}", methods=["GET"])
async def stream(req):
conversation_id = req.route_params.get("conversation_id")
cursor = req.params.get("cursor") # Optional
async with await get_stream_handler() as handler:
async for chunk in handler.read_stream(conversation_id, cursor):
# Format and return chunks
```
### 3. Redis Streams
Messages are stored in Redis Streams with automatic TTL (default: 10 minutes):
```
Stream Key: agent-stream:{conversation_id}
Entry: {
"text": "chunk content",
"sequence": "0",
"timestamp": "1734649123456"
}
```
@@ -0,0 +1,55 @@
### Reliable Streaming with Redis - Demo HTTP Requests
### Use with the VS Code REST Client extension or any HTTP client
###
### Workflow:
### 1. POST /api/agents/{agentName}/run -> Start durable agent (returns conversation_id)
### 2. GET /api/agent/stream/{id} -> Read chunks from Redis (SSE or plain text)
### 3. Add ?cursor={id} to resume from a specific point
###
### Prerequisites:
### - Redis: docker run -d --name redis -p 6379:6379 redis:latest
### - Start function app: func start
### Variables
@baseUrl = http://localhost:7071
@agentName = TravelPlanner
### Health Check
GET {{baseUrl}}/api/health
###
### Start Agent Run
# Starts the agent in the background via durable orchestration.
# The RedisStreamCallback persists streaming chunks to Redis.
# @name trip
POST {{baseUrl}}/api/agents/{{agentName}}/run
Content-Type: text/plain
Plan a 3-day trip to Tokyo
###
### Stream from Redis (SSE format)
# Reads persisted chunks from Redis using cursor-based pagination.
# The conversation_id is automatically captured from the previous request.
@conversationId = {{trip.response.body.$.conversation_id}}
GET {{baseUrl}}/api/agent/stream/{{conversationId}}
Accept: text/event-stream
###
### Stream from Redis (plain text)
# Same as above, but returns plain text instead of SSE format
GET {{baseUrl}}/api/agent/stream/{{conversationId}}
Accept: text/plain
###
### Resume from cursor
# Use a cursor ID from an SSE event to skip already-processed messages
# Replace {cursor_id} with an actual entry ID from the SSE stream
GET {{baseUrl}}/api/agent/stream/{{conversationId}}?cursor={cursor_id}
Accept: text/event-stream
###
@@ -0,0 +1,322 @@
# Copyright (c) Microsoft. All rights reserved.
"""Reliable streaming for durable agents using Redis Streams.
This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams.
Components used in this sample:
- AzureOpenAIChatClient to create the travel planner agent with tools.
- AgentFunctionApp with a Redis-based callback for persistent streaming.
- Custom HTTP endpoint to resume streaming from any point using cursor-based pagination.
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
- Redis running (docker run -d --name redis -p 6379:6379 redis:latest)
- DTS and Azurite running (see parent README)
"""
import logging
import os
from datetime import timedelta
import redis.asyncio as aioredis
from agent_framework import AgentRunResponseUpdate
import azure.functions as func
from agent_framework.azure import (
AgentCallbackContext,
AgentFunctionApp,
AgentResponseCallbackProtocol,
AzureOpenAIChatClient,
)
from azure.identity import AzureCliCredential
from redis_stream_response_handler import RedisStreamResponseHandler, StreamChunk
from tools import get_local_events, get_weather_forecast
logger = logging.getLogger(__name__)
# Configuration
REDIS_CONNECTION_STRING = os.environ.get("REDIS_CONNECTION_STRING", "redis://localhost:6379")
REDIS_STREAM_TTL_MINUTES = int(os.environ.get("REDIS_STREAM_TTL_MINUTES", "10"))
async def get_stream_handler() -> RedisStreamResponseHandler:
"""Create a new Redis stream handler for each request.
This avoids event loop conflicts in Azure Functions by creating
a fresh Redis client in the current event loop context.
"""
# Create a new Redis client in the current event loop
redis_client = aioredis.from_url(
REDIS_CONNECTION_STRING,
encoding="utf-8",
decode_responses=False,
)
return RedisStreamResponseHandler(
redis_client=redis_client,
stream_ttl=timedelta(minutes=REDIS_STREAM_TTL_MINUTES),
)
class RedisStreamCallback(AgentResponseCallbackProtocol):
"""Callback that writes streaming updates to Redis Streams for reliable delivery.
This enables clients to disconnect and reconnect without losing messages.
"""
def __init__(self) -> None:
self._logger = logging.getLogger("durableagent.samples.redis_streaming")
self._sequence_numbers = {} # Track sequence per thread
async def on_streaming_response_update(
self,
update: AgentRunResponseUpdate,
context: AgentCallbackContext,
) -> None:
"""Write streaming update to Redis Stream.
Args:
update: The streaming response update chunk.
context: The callback context with thread_id, agent_name, etc.
"""
thread_id = context.thread_id
if not thread_id:
self._logger.warning("No thread_id available for streaming update")
return
if not update.text:
return
text = update.text
# Get or initialize sequence number for this thread
if thread_id not in self._sequence_numbers:
self._sequence_numbers[thread_id] = 0
sequence = self._sequence_numbers[thread_id]
try:
# Use context manager to ensure Redis client is properly closed
async with await get_stream_handler() as stream_handler:
# Write chunk to Redis Stream using public API
await stream_handler.write_chunk(thread_id, text, sequence)
self._sequence_numbers[thread_id] += 1
self._logger.info(
"[%s][%s] Wrote chunk to Redis: seq=%d, text=%s",
context.agent_name,
thread_id[:8],
sequence,
text,
)
except Exception as ex:
self._logger.error(f"Error writing to Redis stream: {ex}", exc_info=True)
async def on_agent_response(self, response, context: AgentCallbackContext) -> None:
"""Write end-of-stream marker when agent completes.
Args:
response: The final agent response.
context: The callback context.
"""
thread_id = context.thread_id
if not thread_id:
return
sequence = self._sequence_numbers.get(thread_id, 0)
try:
# Use context manager to ensure Redis client is properly closed
async with await get_stream_handler() as stream_handler:
# Write end-of-stream marker using public API
await stream_handler.write_completion(thread_id, sequence)
self._logger.info(
"[%s][%s] Agent completed, wrote end-of-stream marker",
context.agent_name,
thread_id[:8],
)
# Clean up sequence tracker
self._sequence_numbers.pop(thread_id, None)
except Exception as ex:
self._logger.error(f"Error writing end-of-stream marker: {ex}", exc_info=True)
# Create the Redis streaming callback
redis_callback = RedisStreamCallback()
# Create the travel planner agent
def create_travel_agent():
"""Create the TravelPlanner agent with tools."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="TravelPlanner",
instructions="""You are an expert travel planner who creates detailed, personalized travel itineraries.
When asked to plan a trip, you should:
1. Create a comprehensive day-by-day itinerary
2. Include specific recommendations for activities, restaurants, and attractions
3. Provide practical tips for each destination
4. Consider weather and local events when making recommendations
5. Include estimated times and logistics between activities
Always use the available tools to get current weather forecasts and local events
for the destination to make your recommendations more relevant and timely.
Format your response with clear headings for each day and include emoji icons
to make the itinerary easy to scan and visually appealing.""",
tools=[get_weather_forecast, get_local_events],
)
# Create AgentFunctionApp with the Redis callback
app = AgentFunctionApp(
agents=[create_travel_agent()],
enable_health_check=True,
default_callback=redis_callback,
max_poll_retries=100, # Increase for longer-running agents
)
# Custom streaming endpoint for reading from Redis
# Use the standard /api/agents/TravelPlanner/run endpoint to start agent runs
@app.function_name("stream")
@app.route(route="agent/stream/{conversation_id}", methods=["GET"])
async def stream(req: func.HttpRequest) -> func.HttpResponse:
"""Resume streaming from a specific cursor position for an existing session.
This endpoint reads all currently available chunks from Redis for the given
conversation ID, starting from the specified cursor (or beginning if no cursor).
Use this endpoint to resume a stream after disconnection. Pass the conversation ID
and optionally a cursor (Redis entry ID) to continue from where you left off.
Query Parameters:
cursor (optional): Redis stream entry ID to resume from. If not provided, starts from beginning.
Response Headers:
Content-Type: text/event-stream or text/plain based on Accept header
x-conversation-id: The conversation/thread ID
SSE Event Fields (when Accept: text/event-stream):
id: Redis stream entry ID (use as cursor for resumption)
event: "message" for content, "done" for completion, "error" for errors
data: The text content or status message
"""
try:
conversation_id = req.route_params.get("conversation_id")
if not conversation_id:
return func.HttpResponse(
"Conversation ID is required.",
status_code=400,
)
# Get optional cursor from query string
cursor = req.params.get("cursor")
logger.info(
f"Resuming stream for conversation {conversation_id} from cursor: {cursor or '(beginning)'}"
)
# Check Accept header to determine response format
accept_header = req.headers.get("Accept", "")
use_sse_format = "text/plain" not in accept_header.lower()
# Stream chunks from Redis
return await _stream_to_client(conversation_id, cursor, use_sse_format)
except Exception as ex:
logger.error(f"Error in stream endpoint: {ex}", exc_info=True)
return func.HttpResponse(
f"Internal server error: {str(ex)}",
status_code=500,
)
async def _stream_to_client(
conversation_id: str,
cursor: str | None,
use_sse_format: bool,
) -> func.HttpResponse:
"""Stream chunks from Redis to the HTTP response.
Args:
conversation_id: The conversation ID to stream from.
cursor: Optional cursor to resume from. If None, streams from the beginning.
use_sse_format: True to use SSE format, false for plain text.
Returns:
HTTP response with all currently available chunks.
"""
chunks = []
# Use context manager to ensure Redis client is properly closed
async with await get_stream_handler() as stream_handler:
try:
async for chunk in stream_handler.read_stream(conversation_id, cursor):
if chunk.error:
logger.warning(f"Stream error for {conversation_id}: {chunk.error}")
chunks.append(_format_error(chunk.error, use_sse_format))
break
if chunk.is_done:
chunks.append(_format_end_of_stream(chunk.entry_id, use_sse_format))
break
if chunk.text:
chunks.append(_format_chunk(chunk, use_sse_format))
except Exception as ex:
logger.error(f"Error reading from Redis: {ex}", exc_info=True)
chunks.append(_format_error(str(ex), use_sse_format))
# Return all chunks
response_body = "".join(chunks)
return func.HttpResponse(
body=response_body,
mimetype="text/event-stream" if use_sse_format else "text/plain; charset=utf-8",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"x-conversation-id": conversation_id,
},
)
def _format_chunk(chunk: StreamChunk, use_sse_format: bool) -> str:
"""Format a text chunk."""
if use_sse_format:
return _format_sse_event("message", chunk.text, chunk.entry_id)
else:
return chunk.text
def _format_end_of_stream(entry_id: str, use_sse_format: bool) -> str:
"""Format end-of-stream marker."""
if use_sse_format:
return _format_sse_event("done", "[DONE]", entry_id)
else:
return "\n"
def _format_error(error: str, use_sse_format: bool) -> str:
"""Format error message."""
if use_sse_format:
return _format_sse_event("error", error, None)
else:
return f"\n[Error: {error}]\n"
def _format_sse_event(event_type: str, data: str, event_id: str | None = None) -> str:
"""Format a Server-Sent Event."""
lines = []
if event_id:
lines.append(f"id: {event_id}")
lines.append(f"event: {event_type}")
lines.append(f"data: {data}")
lines.append("")
return "\n".join(lines) + "\n"
@@ -7,6 +7,8 @@
"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>"
"AZURE_OPENAI_API_KEY": "<AZURE_OPENAI_API_KEY>",
"REDIS_CONNECTION_STRING": "redis://localhost:6379",
"REDIS_STREAM_TTL_MINUTES": "10"
}
}
@@ -0,0 +1,200 @@
# Copyright (c) Microsoft. All rights reserved.
"""Redis-based streaming response handler for durable agents.
This module provides reliable, resumable streaming of agent responses using Redis Streams
as a message broker. It enables clients to disconnect and reconnect without losing messages.
"""
import asyncio
import time
from dataclasses import dataclass
from datetime import timedelta
from collections.abc import AsyncIterator
import redis.asyncio as aioredis
@dataclass
class StreamChunk:
"""Represents a chunk of streamed data from Redis.
Attributes:
entry_id: The Redis stream entry ID (used as cursor for resumption).
text: The text content of the chunk, if any.
is_done: Whether this is the final chunk in the stream.
error: Error message if an error occurred, otherwise None.
"""
entry_id: str
text: str | None = None
is_done: bool = False
error: str | None = None
class RedisStreamResponseHandler:
"""Handles agent responses by persisting them to Redis Streams.
This handler writes agent response updates to Redis Streams, enabling reliable,
resumable streaming delivery to clients. Clients can disconnect and reconnect
at any point using cursor-based pagination.
Attributes:
MAX_EMPTY_READS: Maximum number of empty reads before timing out.
POLL_INTERVAL_MS: Interval in milliseconds between polling attempts.
"""
MAX_EMPTY_READS = 300
POLL_INTERVAL_MS = 1000
def __init__(self, redis_client: aioredis.Redis, stream_ttl: timedelta):
"""Initialize the Redis stream response handler.
Args:
redis_client: The async Redis client instance.
stream_ttl: Time-to-live for stream entries in Redis.
"""
self._redis = redis_client
self._stream_ttl = stream_ttl
async def __aenter__(self):
"""Enter async context manager."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Exit async context manager and close Redis connection."""
await self._redis.aclose()
async def write_chunk(
self,
conversation_id: str,
text: str,
sequence: int,
) -> None:
"""Write a single text chunk to the Redis Stream.
Args:
conversation_id: The conversation ID for this agent run.
text: The text content to write.
sequence: The sequence number for ordering.
"""
stream_key = self._get_stream_key(conversation_id)
await self._redis.xadd(
stream_key,
{
"text": text,
"sequence": str(sequence),
"timestamp": str(int(time.time() * 1000)),
}
)
await self._redis.expire(stream_key, self._stream_ttl)
async def write_completion(
self,
conversation_id: str,
sequence: int,
) -> None:
"""Write an end-of-stream marker to the Redis Stream.
Args:
conversation_id: The conversation ID for this agent run.
sequence: The final sequence number.
"""
stream_key = self._get_stream_key(conversation_id)
await self._redis.xadd(
stream_key,
{
"text": "",
"sequence": str(sequence),
"timestamp": str(int(time.time() * 1000)),
"done": "true",
}
)
await self._redis.expire(stream_key, self._stream_ttl)
async def read_stream(
self,
conversation_id: str,
cursor: str | None = None,
) -> AsyncIterator[StreamChunk]:
"""Read entries from a Redis Stream with cursor-based pagination.
This method polls the Redis Stream for new entries, yielding chunks as they
become available. Clients can resume from any point using the entry_id from
a previous chunk.
Args:
conversation_id: The conversation ID to read from.
cursor: Optional cursor to resume from. If None, starts from beginning.
Yields:
StreamChunk instances containing text content or status markers.
"""
stream_key = self._get_stream_key(conversation_id)
start_id = cursor if cursor else "0-0"
empty_read_count = 0
has_seen_data = False
while True:
try:
# Read up to 100 entries from the stream
entries = await self._redis.xread(
{stream_key: start_id},
count=100,
block=None,
)
if not entries:
# No entries found
if not has_seen_data:
empty_read_count += 1
if empty_read_count >= self.MAX_EMPTY_READS:
timeout_seconds = self.MAX_EMPTY_READS * self.POLL_INTERVAL_MS / 1000
yield StreamChunk(
entry_id=start_id,
error=f"Stream not found or timed out after {timeout_seconds} seconds"
)
return
# Wait before polling again
await asyncio.sleep(self.POLL_INTERVAL_MS / 1000)
continue
has_seen_data = True
# Process entries from the stream
for stream_name, stream_entries in entries:
for entry_id, entry_data in stream_entries:
start_id = entry_id.decode() if isinstance(entry_id, bytes) else entry_id
# Decode entry data
text = entry_data.get(b"text", b"").decode() if b"text" in entry_data else None
done = entry_data.get(b"done", b"").decode() if b"done" in entry_data else None
error = entry_data.get(b"error", b"").decode() if b"error" in entry_data else None
if error:
yield StreamChunk(entry_id=start_id, error=error)
return
if done == "true":
yield StreamChunk(entry_id=start_id, is_done=True)
return
if text:
yield StreamChunk(entry_id=start_id, text=text)
except Exception as ex:
yield StreamChunk(entry_id=start_id, error=str(ex))
return
@staticmethod
def _get_stream_key(conversation_id: str) -> str:
"""Generate the Redis key for a conversation's stream.
Args:
conversation_id: The conversation ID.
Returns:
The Redis stream key.
"""
return f"agent-stream:{conversation_id}"
@@ -1,2 +1,3 @@
agent-framework-azurefunctions
azure-identity
azure-identity
redis
@@ -0,0 +1,165 @@
# Copyright (c) Microsoft. All rights reserved.
"""Mock travel tools for demonstration purposes.
In a real application, these would call actual weather and events APIs.
"""
from typing import Annotated
def get_weather_forecast(
destination: Annotated[str, "The destination city or location"],
date: Annotated[str, 'The date for the forecast (e.g., "2025-01-15" or "next Monday")'],
) -> str:
"""Get the weather forecast for a destination on a specific date.
Use this to provide weather-aware recommendations in the itinerary.
Args:
destination: The destination city or location.
date: The date for the forecast.
Returns:
A weather forecast summary.
"""
# Mock weather data based on destination for realistic responses
weather_by_region = {
"Tokyo": ("Partly cloudy with a chance of light rain", 58, 45),
"Paris": ("Overcast with occasional drizzle", 52, 41),
"New York": ("Clear and cold", 42, 28),
"London": ("Foggy morning, clearing in afternoon", 48, 38),
"Sydney": ("Sunny and warm", 82, 68),
"Rome": ("Sunny with light breeze", 62, 48),
"Barcelona": ("Partly sunny", 59, 47),
"Amsterdam": ("Cloudy with light rain", 46, 38),
"Dubai": ("Sunny and hot", 85, 72),
"Singapore": ("Tropical thunderstorms in afternoon", 88, 77),
"Bangkok": ("Hot and humid, afternoon showers", 91, 78),
"Los Angeles": ("Sunny and pleasant", 72, 55),
"San Francisco": ("Morning fog, afternoon sun", 62, 52),
"Seattle": ("Rainy with breaks", 48, 40),
"Miami": ("Warm and sunny", 78, 65),
"Honolulu": ("Tropical paradise weather", 82, 72),
}
# Find a matching destination or use a default
forecast = ("Partly cloudy", 65, 50)
for city, weather in weather_by_region.items():
if city.lower() in destination.lower():
forecast = weather
break
condition, high_f, low_f = forecast
high_c = (high_f - 32) * 5 // 9
low_c = (low_f - 32) * 5 // 9
recommendation = _get_weather_recommendation(condition)
return f"""Weather forecast for {destination} on {date}:
Conditions: {condition}
High: {high_f}°F ({high_c}°C)
Low: {low_f}°F ({low_c}°C)
Recommendation: {recommendation}"""
def get_local_events(
destination: Annotated[str, "The destination city or location"],
date: Annotated[str, 'The date to search for events (e.g., "2025-01-15" or "next week")'],
) -> str:
"""Get local events and activities happening at a destination around a specific date.
Use this to suggest timely activities and experiences.
Args:
destination: The destination city or location.
date: The date to search for events.
Returns:
A list of local events and activities.
"""
# Mock events data based on destination
events_by_city = {
"Tokyo": [
"🎭 Kabuki Theater Performance at Kabukiza Theatre - Traditional Japanese drama",
"🌸 Winter Illuminations at Yoyogi Park - Spectacular light displays",
"🍜 Ramen Festival at Tokyo Station - Sample ramen from across Japan",
"🎮 Gaming Expo at Tokyo Big Sight - Latest video games and technology",
],
"Paris": [
"🎨 Impressionist Exhibition at Musée d'Orsay - Extended evening hours",
"🍷 Wine Tasting Tour in Le Marais - Local sommelier guided",
"🎵 Jazz Night at Le Caveau de la Huchette - Historic jazz club",
"🥐 French Pastry Workshop - Learn from master pâtissiers",
],
"New York": [
"🎭 Broadway Show: Hamilton - Limited engagement performances",
"🏀 Knicks vs Lakers at Madison Square Garden",
"🎨 Modern Art Exhibit at MoMA - New installations",
"🍕 Pizza Walking Tour of Brooklyn - Artisan pizzerias",
],
"London": [
"👑 Royal Collection Exhibition at Buckingham Palace",
"🎭 West End Musical: The Phantom of the Opera",
"🍺 Craft Beer Festival at Brick Lane",
"🎪 Winter Wonderland at Hyde Park - Rides and markets",
],
"Sydney": [
"🏄 Pro Surfing Competition at Bondi Beach",
"🎵 Opera at Sydney Opera House - La Bohème",
"🦘 Wildlife Night Safari at Taronga Zoo",
"🍽️ Harbor Dinner Cruise with fireworks",
],
"Rome": [
"🏛️ After-Hours Vatican Tour - Skip the crowds",
"🍝 Pasta Making Class in Trastevere",
"🎵 Classical Concert at Borghese Gallery",
"🍷 Wine Tasting in Roman Cellars",
],
}
# Find events for the destination or use generic events
events = [
"🎭 Local theater performance",
"🍽️ Food and wine festival",
"🎨 Art gallery opening",
"🎵 Live music at local venues",
]
for city, city_events in events_by_city.items():
if city.lower() in destination.lower():
events = city_events
break
event_list = "\n".join(events)
return f"""Local events in {destination} around {date}:
{event_list}
💡 Tip: Book popular events in advance as they may sell out quickly!"""
def _get_weather_recommendation(condition: str) -> str:
"""Get a recommendation based on weather conditions.
Args:
condition: The weather condition description.
Returns:
A recommendation string.
"""
condition_lower = condition.lower()
if "rain" in condition_lower or "drizzle" in condition_lower:
return "Bring an umbrella and waterproof jacket. Consider indoor activities for backup."
elif "fog" in condition_lower:
return "Morning visibility may be limited. Plan outdoor sightseeing for afternoon."
elif "cold" in condition_lower:
return "Layer up with warm clothing. Hot drinks and cozy cafés recommended."
elif "hot" in condition_lower or "warm" in condition_lower:
return "Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours."
elif "thunder" in condition_lower or "storm" in condition_lower:
return "Keep an eye on weather updates. Have indoor alternatives ready."
else:
return "Pleasant conditions expected. Great day for outdoor exploration!"
@@ -119,6 +119,7 @@ async def main() -> None:
Input: str: 'HELLO WORLD'
[WORKFLOW OUTPUT] str: 'DLROW OLLEH'
[COMPLETED] reverse_text
Output: list: [str: 'DLROW OLLEH']
"""