Compare commits

..
Author SHA1 Message Date
cb2862d4c3 update package versions (#3223)
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2026-01-14 23:26:50 +00:00
Giles OdigweandGitHub 9b9a0f178c Python: Added AzureAI sample for downloading code interpreter generated files (#3189)
* added azure ai code interpreter file download sample

* copilot fix suggestions

* function name fixes + readme update

* small fix
2026-01-14 22:50:07 +00:00
Tao ChenandGitHub 6c956ec596 Python: Add more specific exceptions to Workflow (#3188)
* Add more specifc workflow exceptions

* Fix tests

* AI comments

* Misc
2026-01-14 20:10:52 +00:00
99c5718696 Python: Create/Get Agent API for Azure V2 (#3059)
* Added get_agent method to Azure AI V2

* Small fixes

* Small fix

* Removed AzureAIAgentProvider

* Added create_agent method

* Small fixes

* Fixed code interpreter tool mapping

* Added agent provider for V2 client

* Updated response format handling

* Added provider example

* Fixed errors

* Update python/samples/getting_started/agents/azure_ai/README.md

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

* Small fix

* Updates from merge

* Resolved comments

* Resolved comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 19:35:01 +00:00
SergeyMenshykhandGitHub f56808b279 .NET: [Breaking] Rename AgentRunResponseEvent and AgentRunUpdateEvent classes (#3214)
* rename AgentRunResponseEvent and AgentRunUpdateEvent classes

* rollback unnecessary changes
2026-01-14 17:44:35 +00:00
SergeyMenshykhandGitHub c70e594e6c .NET: [Breaking] RenameAgentRunResponse and AgentRunResponseUpdate classes (#3197)
* rename AgentRunResponse and AgentRunResponseUpdate classes - part1

* rename varialbles, parameters, methods and tests

* rollback unnecessary changes
2026-01-14 10:27:41 +00:00
Eduard van ValkenburgandGitHub 8b1449024e Python: ADR for simplified get response (#3098)
* ADR for simplified get response

* updated some language, added agent option and code comparison

* small update in sample

* added workflows and expanded some points

* changed decision and number

* updated with stream=False default
2026-01-14 08:50:34 +00:00
Eduard van ValkenburgandGitHub d8cf8361bd renamed all (#3207) 2026-01-14 05:54:07 +00:00
Evan MattsonandGitHub 1ae0b09e42 Python: Add dependencies param to ag-ui FastAPI endpoint (#3191)
* Add dependencies param to ag-ui FastAPI endpoint

* Address Copilot feedback
2026-01-13 22:31:33 +00:00
westeyandGitHub c063fc77e6 .NET: Make ChatMessageStore and AIContextProvider context props settable (#3196)
* Make ChatMessageStore and AIContextProvider context props setable

* Add validation to preserve non-null requirement of certain properties.

* Fix broken tests.
2026-01-13 19:47:57 +00:00
westeyandGitHub 04657c207a Implement IReadOnlyList on InMemoryChatMessageStore (#3205) 2026-01-13 19:47:23 +00:00
Mark WallaceandGitHub 655a59a75f Add ignored parameter for CodeQL in workflow (#3204) 2026-01-13 19:10:55 +00:00
Dmytro StrukandGitHub 7d2d34511c Python: ADR for create/get agent API (#2618)
* ADR for create/get agent API

* Updated ADR with implementation options

* Small updates

* Updated decision outcome section

* Updated broken links

* Small updates

* Fixed merge conflicts

* Small fix

* Updated decision outcome section

* Small fixes

* Updated provider naming based on client SDK
2026-01-13 18:55:05 +00:00
Tao ChenandGitHub 0b152418b6 [BREAKING] Python: Refactor orchestrations (#3023)
* Group chat refactoring Part 1; Next: HIL and handoff

* Add agent approval flow; next samples

* WIP: samples

* WIP: HIL samples

* Group chat HIL working; next: handoff

* Fix group chat tool approval sample

* WIP: refactor handoff; next handoff handling

* Handoff done; next handoff samples and concurrent and sequential

* Handoff samples, concurrent, and sequential done; next Magentic

* WIP: magentic; next test with samples + HIL

* Magentic Working; next fix all samples and tests

* Fix handoff samples; next tests

* WIP: fixing tests; some orchestration as agent samples are failing

* Group chat unit tests done

* Handoff  unit tests done

* Remove old orchestration_request_info and fix related tests

* Magentic unit tests done

* Fix samples

* Fix test

* Fix test 2

* mypy

* Address comments

* Update readme

* Address comments

* Address comments 2

* Replace display name
2026-01-13 18:40:26 +00:00
Eduard van ValkenburgandGitHub 3e97425245 Python: [BREAKING]: Introducing Options as TypedDict and Generic (#3140)
* WIP typeddict for options

* updated all clients and ChatAgents

* updated everything

* added ADR

* fix mypy

* proper typevar imports

* fixed import

* fixed other imports

* slight update in the sample

* updated from feedback

* fixes

* fixed missing covariants and test fixes

* fixed typing

* updated anthropic thinking config

* ruff fixes

* fixed int tests

* fix tests and mypy

* updated integration tests

* updated docstring and test fix

* improved options handling in obser

* mypy fix

* updated a host of integration tests

* fix tests

* bedrock fix
2026-01-13 16:41:05 +00:00
Korolev DmitryandGitHub 5faa2851bb point URL to agent, not to agentcard (#3176) 2026-01-13 16:32:05 +00:00
Evan MattsonandGitHub 9c094573e8 Python: Add declarative workflow runtime (#2815)
* Further support for declarative python workflows

* Add tests. Clean up for typing and formatting

* Improvements and cleanup

* Typing cleanup. Improve docstrings

* Proper code in docstrings

* Fix malformed code-block directive in docstring

* Remove dead links

* PR feedback

* Address PR feedback

* Address PR feedback

* Remove sl

* Update devui frontend

* More cleanup

* Fix uv lock

* Skip Py 3.14 tests as powerfx doesn't support it

* Fix mypy error

* Fix for tool calls

* Removed stale docstring

* Fix lint

* Standardize on .NET namespaces. Revert DevUI changes (bring in later)

* Implement remaining items for Python declarative support to match dotnet
2026-01-13 07:11:21 +00:00
Eduard van ValkenburgandGitHub b2893fbc00 Python: MCP Improvements: improved connection loss behavior, pagination for loading and a param to control representation (#3154)
* pagination support (#2848) added a parse_tool_result param and connection loss (#2884)

* fix #3153

* improved connection handling

* improved logic
2026-01-13 04:09:33 +00:00
Eduard van ValkenburgandGitHub 203fb7b1c4 Python: [BREAKING]: removed display_name, renamed context_providers, middleware and AggregateContextProvider (#3139)
* removed display_name, renamed context_providers, middleware and AggregateContextProvider

* fixes

* fixed test

* testfix

* removed mistakenly put back test

* updated new test

* rename middlewares to middleware

* middleware fixes
2026-01-13 02:24:07 +00:00
Giles OdigweandGitHub ef44fb4960 azureai direct a2a endpoint support (#3127) 2026-01-12 22:03:04 +00:00
CopilotGitHubeavanvalkenburgcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>eavanvalkenburg
e63c148fc7 Python: Fix MCPStreamableHTTPTool to use new streamable_http_client API (#3088)
* Fix MCPStreamableHTTPTool to use new streamable_http_client API with proper httpx client cleanup

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

* Update docstring to reflect new streamable_http_client API usage

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

* Refactor MCPStreamableHTTPTool to accept optional http_client parameter and delegate client creation to streamable_http_client

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

* Update mcp package minimum version to 1.24.0 for streamable_http_client API support

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

* Fix critical bugs: apply headers/timeout/sse_read_timeout when creating httpx client, add version constraint <2, and properly manage client lifecycle

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

* Simplify implementation: remove headers/timeout/sse_read_timeout params, remove kwargs, remove close() override per feedback

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

* Add back **kwargs parameter for backward compatibility (accepted but not used)

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

* Remove unused httpx import from test file

Note: The uv.lock file needs to be updated with 'uv sync' to reflect the mcp version constraint change (>=1.24.0,<2)

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

* cicd fixes

* udpated samples with headers examples

---------

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>
2026-01-12 17:26:53 +00:00
Korolev DmitryandGitHub c7cb5be231 .NET: Improve resolving AITool from DI (#3175)
* remove localagenttoolregistry

* also give the factory method API
2026-01-12 16:13:44 +00:00
claude89757andGitHub 3e13909e59 Python: Add Pydantic request model and OpenAPI tags support to AG-UI FastAPI endpoint (#2522)
* feat(ag-ui): Add Pydantic request model and OpenAPI tags support

- Add AGUIRequest Pydantic model in _types.py with field descriptions
- Update add_agent_framework_fastapi_endpoint() to accept tags parameter
- Use AGUIRequest model for automatic validation and OpenAPI schema generation
- Export AGUIRequest and DEFAULT_TAGS in __init__.py
- Update test_endpoint.py to expect 422 for invalid requests
- Add tests for OpenAPI schema, default tags, custom tags, and validation

Benefits:
- Better API documentation with complete request schema in Swagger UI
- Automatic request validation with Pydantic
- Organized endpoints under 'AG-UI' tag instead of 'default'
- Improved developer experience and type safety

Fixes #<issue-number>

* test(ag-ui): Add test for internal error handling to achieve 100% coverage

- Add test_endpoint_internal_error_handling() to cover exception handling code
- Mock copy.deepcopy to simulate internal error during default_state processing
- Add type: ignore for FastAPI tags parameter (known pyright compatibility issue)
- Achieves 100% test coverage for _endpoint.py (previously missing lines 103-105)
2026-01-12 15:07:34 +00:00
Dina Suehiro JonesandGitHub 3a5fe31263 Fix Ollama model env var in documentation (#3156)
Signed-off-by: Dina Suehiro Jones <dina.s.jones@intel.com>
2026-01-12 14:15:49 +00:00
westeyandGitHub bb6ecd9c71 .NET: [BREAKING] Change GetNewThread and DeserializeThread to async (#3152)
* Change GetNewThread and DeserializeThread plus ChatMessageStore and AIContextProvider Factories to async

* Merge fixes
2026-01-12 11:25:51 +00:00
6e3bc219e0 fix(anthropic): fix duplicate ToolCallStartEvent in streaming tool calls (#3051)
When processing `input_json_delta` events, the Anthropic client was
passing the tool name from the previous `tool_use` event. This caused
ag-ui's `_handle_function_call_content` to emit a `ToolCallStartEvent`
for every streaming chunk (since it triggers on `if content.name:`).

This fix changes the behavior to pass an empty string for `name` in
`input_json_delta` events, matching OpenAI's behavior where streaming
argument chunks have `name=""`. The initial `tool_use` event still
provides the tool name, so only one `ToolCallStartEvent` is emitted.

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-01-12 08:14:49 +00:00
Eduard van ValkenburgandGitHub 551c2c3abe Python: multiple bug fixes (#3150)
* fix Python: kwargs are not passed to _prepare_thread_and_messages in ChatAgent.run
Fixes #3118

* fix Python: [Bug]: model_id versus model_deployment_name is confusing in Azure AI Agents
Fixes #3147

* add types

* fixed type and docstring
2026-01-12 01:01:41 +00:00
Evan MattsonandGitHub 6445b6b3a6 Python: Fix AzureAIClient tool call bug for AG-UI use (#3148)
* Fiz AzureAIClient tool call bug

* Address copilot feedback
2026-01-09 18:53:15 +00:00
Evan MattsonandGitHub d28ad2d7df Track agent name with updates for workflow agent (#3146) 2026-01-09 08:23:32 +00:00
Evan MattsonandGitHub 88968da0bd 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
2026-01-09 03:08:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>ChrisMark WallaceDmytro Struk
50d34aec91 .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>
2026-01-08 23:45:32 +00:00
Dmytro StrukandGitHub 13a5b70703 Updated package versions (#3144) 2026-01-08 22:19:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9c04196491 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>
2026-01-08 20:49:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
01c5aabda5 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>
2026-01-08 20:12:20 +00:00
CopilotGitHubeavanvalkenburgcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>eavanvalkenburg
3f7ea350dc 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>
2026-01-08 19:46:32 +00:00
CopilotGitHubwestey-mcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
92435c6ab5 .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>
2026-01-08 19:25:47 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>ChrisSergeyMenshykh
f6086e4ccd 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>
2026-01-08 17:55:43 +00:00
Jacob AlberandGitHub 99fac4ca56 .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
2026-01-08 17:34:05 +00:00
7aa72f6fdb .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>
2026-01-08 17:31:13 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
49cecf324c 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>
2026-01-08 17:00:29 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
b88b2c3190 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>
2026-01-08 17:00:12 +00:00
SukeeshandGitHub ab493af110 Python: Fix Anthropic streaming response bugs (#3141)
* test commit identity

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

* lint fix
2026-01-08 16:22:52 +00:00
SergeyMenshykhandGitHub 33888641ec .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
2026-01-08 14:05:16 +00:00
westeyandGitHub 299a5110ed Make A2AAgent public, so that it's concrete implementation methods can be used. (#3119) 2026-01-08 11:27:41 +00:00
Evan MattsonandGitHub f508f1d6da Python: Bump python version to 1.0.0b260107 for a release (#3128)
* Bump python version to 1.0.0b260107 for a release

* Update changelog
2026-01-08 08:49:28 +09:00
Evan MattsonandGitHub e9d97ce6b7 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
2026-01-07 23:11:28 +00:00
Gavin AguiarandGitHub f4ab586f11 Python: Streaming sample for azurefunctions (#3057)
* Streaming sample for azurefunctions

* Fixed links and sample name

* Addressed feedback

* Addressed feedback

* Fixed integration tests

* Updated test
2026-01-07 22:20:42 +00:00
Eduard van ValkenburgandGitHub a118fd5c07 updated templates (#3106)
* updated templates

* enabled blank and fixed triage

* made language optional and moved to the bottom for features
2026-01-07 15:39:31 +00:00
Mark WallaceandGitHub 521f04632d Enable blank issues in issue template configuration
Need to re-enable creating blank issues
2026-01-07 14:55:43 +00:00
dd69cabc67 .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>
2026-01-07 11:40:39 +00:00
Victor DibiaandGitHub 2e1189ca65 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
2026-01-07 08:26:08 +00:00
claude89757andGitHub db283cd396 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.
2026-01-07 00:47:26 +00:00
Evan MattsonandGitHub f49e537721 Bump Bedrock version to latest (#3110) 2026-01-07 09:34:02 +09:00
Evan MattsonandGitHub 202f557c71 Bump versions to 1.0.0b260106 for a release. Update CHANGELOG.md (#3109) 2026-01-07 00:09:49 +00:00
Giles OdigweandGitHub ea370f8ff6 sharepoint sample fix (#3108) 2026-01-06 22:57:54 +00:00
Evan MattsonandGitHub 24c822590f fix: tool_choice parameter not being honored when passed to agent.run() (#3095) 2026-01-06 22:51:20 +00:00
CopilotGitHubwestey-mcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Mark WallaceChris
953fde69ac .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>
2026-01-06 15:39:42 +00:00
westeyandGitHub 7a05849609 Fix broken strands urls. (#3102)
* Fix broken strands urls.

* Fix typos
2026-01-06 14:55:29 +00:00
CopilotGitHubSergeyMenshykhcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
0aa0579b1b .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>
2026-01-06 10:44:13 +00:00
Evan MattsonandGitHub 844d345106 Python: Fix ExecutorInvokedEvent and ExecutorCompletedEvent observability data (#3090)
* Fix ExecutorInvokedEvent.data mutation bug

* Fix bug related to not yielding output type
2026-01-06 09:12:26 +00:00
takanori-teraiandGitHub ed5278c41d Fix: Update OTLP exporter protocol conditions (#3070) 2026-01-06 04:45:29 +00:00
Evan MattsonandGitHub 928c9d54ad 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
2026-01-05 22:05:46 +00:00
westeyandGitHub 0aba02c402 [BREAKING] Remove unused AgentThreadMetadata (#3067)
* Remove unused AgentThreadMetadata

* Update DurableTask Changelog
2026-01-05 14:03:18 +00:00
3ef67eff10 .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>
2026-01-05 11:51:15 +00:00
Eduard van ValkenburgandGitHub deea844bc7 fix and extra int test (#3037) 2026-01-05 04:35:10 +00:00
Eduard van ValkenburgandGitHub 577ad4b838 add issue template and additional labeling (#3006) 2026-01-05 01:32:33 +00:00
CopilotGitHubSergeyMenshykhcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>SergeyMenshykhChris
8b4f7d5e29 .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>
2025-12-30 12:24:09 +00:00
Eduard van ValkenburgandGitHub 4b8a545589 Python: add powerfx safe mode (#3028)
* add powerfx safe mode

* improved docstring and aligned env_file loading

* ensured test uses reset
2025-12-23 20:12:50 +00:00
Dmytro StrukandGitHub 5ab47596ff Python: Updated package versions (#3024)
* Updated package versions

* Updated changelog
2025-12-23 16:04:53 +00:00
Eduard van ValkenburgandGitHub a32702cf38 Python: latency improvements (#3014)
* latency improvements

* fixed mypy, added coding standards and instructions

* slight logic improvement
2025-12-23 16:04:34 +00:00
8b743af217 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>
2025-12-22 14:39:52 +00:00
Chris GillumandGitHub 0e152a0e33 .NET: [Durable Agents] Reliable streaming sample (#2942)
* .NET: [Durable Agents] Reliable streaming sample

* Add automated validation for new sample

* Address Copilot PR feedback
2025-12-19 23:43:36 +00:00
3b77192ad0 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>
2025-12-19 18:35:53 +00:00
Hao LuoandGitHub defe0f1a89 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
2025-12-19 17:50:15 +00:00
SuperKenVeryandGitHub 85d70f01f6 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
2025-12-19 17:03:19 +00:00
Giles OdigweandGitHub 6930c0f0b6 Python: Added GitHub MCP sample with PAT (#2967)
* added github mcp sample with PAT

* addressed copilot fixes

* env fix
2025-12-19 16:46:12 +00:00
Dmytro StrukandGitHub d83cf93f07 Updated package versions (#2978) 2025-12-19 16:16:49 +00:00
Eduard van ValkenburgandGitHub 8783ac58f1 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
2025-12-19 16:05:55 +00:00
Evan MattsonandGitHub e15eab7da6 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
2025-12-19 01:31:57 +00:00
Jacob ViauandGitHub 19a9e13788 .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
2025-12-19 00:55:33 +00:00
Evan MattsonandGitHub b0a7a1fcb8 Python: Fix WorkflowAgent event handling and kwargs forwarding (#2946)
* Fix kwargs propagation through workflow.as_agent()

* Fix WorkflowAgent to respect AgentExecutor output_response setting
2025-12-18 19:35:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
a841bdd1cc 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>
2025-12-18 18:36:13 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Mark Wallace
d46adffe6c 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>
2025-12-18 17:25:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b0b5777363 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>
2025-12-18 17:25:36 +00:00
Giles OdigweandGitHub 37b4cfd024 Python: Add Azure Managed Redis Support with Credential Provider (#2887)
* azure redis support

* small fixes

* azure managed redis sample

* fixes
2025-12-18 17:10:55 +00:00
CopilotGitHubstephentoubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
ff9343d7cc .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>
2025-12-18 16:02:20 +00:00
Victor DibiaandGitHub 8ff34f9a43 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
2025-12-18 14:12:42 +00:00
Hao LuoandGitHub e3f8bfc645 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
2025-12-18 14:10:16 +00:00
Tao ChenandGitHub b4f2709b6d Python: Workflow add option to visualize internal executors (#2917)
* Workflow add option to visualize internal executors

* Address Copilot comments
2025-12-18 14:04:03 +00:00
Eduard van ValkenburgandGitHub e5c11d38d6 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
2025-12-18 12:02:23 +00:00
a71f768331 .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>
2025-12-18 09:22:45 +00:00
0298e0a401 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>
2025-12-18 08:42:00 +00:00
Evan MattsonandGitHub ca1532cf22 Python: Move ollama samples to samples getting started dir (#2921)
* Move ollama samples to samples getting started dir

* Address feedback
2025-12-18 08:37:05 +00:00
Evan MattsonandGitHub 360839782c Pass kwargs into subworkflows (#2923) 2025-12-18 04:34:33 +00:00
Ege Ozan ÖzyedekandGitHub ee53fe4666 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
2025-12-17 16:11:39 +00:00
Dmytro StrukandGitHub 3cd805f0bf Added additional arguments for Azure AI agent (#2922) 2025-12-17 08:08:01 +00:00
CopilotGitHubSergeyMenshykhcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
c7ddb8aa14 .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>
2025-12-17 07:31:10 +00:00
Giles OdigweandGitHub d5527982b6 Python: Azure AI Agent with Bing Grounding Citations Sample (#2892)
* bing grounding sample with citations

* small fix

* fix
2025-12-17 00:43:38 +00:00
Dmytro StrukandGitHub ec1c5e9c11 Updated Ollama package version (#2920) 2025-12-17 00:42:27 +00:00
Evan MattsonandGitHub 06cdcb93f0 Fix Pydantic error when using Literal type for tool params (#2893) 2025-12-17 00:27:01 +00:00
Evan MattsonandGitHub 6adcac2e97 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
2025-12-17 00:04:00 +00:00
Tao ChenandGitHub 8fca71e5ad [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
2025-12-16 23:38:33 +00:00
Phillip HoffandGitHub 2bde58f915 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.
2025-12-16 22:08:12 +00:00
Phillip HoffandGitHub 03a403d2fa .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.
2025-12-16 22:07:59 +00:00
Dmytro StrukandGitHub e319707058 Updated package versions (#2913) 2025-12-16 18:51:44 +00:00
Giles OdigweandGitHub 54f482df73 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
2025-12-16 18:23:37 +00:00
Chris GillumandGitHub 754dfb2c9d .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
2025-12-16 18:11:44 +00:00
Roger BarretoandGitHub b15466f058 .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
2025-12-16 17:37:41 +00:00
Roger BarretoandGitHub 3a7047f6e4 Skip failing IT (#2904) 2025-12-16 17:21:22 +00:00
2f06fe557a 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>
2025-12-16 15:02:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1dbf3fd5cf 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>
2025-12-16 13:39:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0132cf65e4 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>
2025-12-16 13:37:15 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a53a3c7af8 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>
2025-12-16 13:36:56 +00:00
3c322c91e7 .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>
2025-12-16 12:41:20 +00:00
Evan MattsonandGitHub 958a488f96 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
2025-12-16 09:52:59 +00:00
Evan MattsonandGitHub 11d6dcfe80 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
2025-12-16 09:52:52 +00:00
Eduard van ValkenburgandGitHub 3139347526 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
2025-12-16 06:56:30 +00:00
3c379718e9 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>
2025-12-16 01:31:26 +00:00
Evan MattsonandGitHub a7298757f5 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
2025-12-16 01:14:26 +00:00
Evan MattsonandGitHub 0dcebc6eae Python: Filter framework kwargs from MCP tool invocations (#2870)
* Filter framework kwargs from MCP tool invocations

* Fixes
2025-12-16 01:10:09 +00:00
Tao ChenandGitHub e0ff153ee9 Python: Remove warnings from workflow builder on not using factories (#2808)
* Revert concurrent

* Fix comments
2025-12-12 07:56:16 +00:00
Richard OrtegaandGitHub e008144187 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/
2025-12-12 07:55:01 +00:00
Evan MattsonandGitHub 0fc7933a92 Fix WorkflowAgent to include thread convo history. Enable checkpointing. (#2774) 2025-12-12 04:04:31 +00:00
Dmytro StrukandGitHub d7434d59ce 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
2025-12-12 01:53:04 +00:00
eb1117fff4 .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>
2025-12-12 00:31:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
16230d3b20 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>
2025-12-11 18:43:52 +00:00
Dmytro StrukandGitHub 8d53b20026 Python: Updated package versions (#2784)
* Updated package versions

* Small fix
2025-12-11 18:39:08 +00:00
Eduard van ValkenburgandGitHub c376868ec9 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
2025-12-11 17:24:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8bb9927f3c 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>
2025-12-11 14:08:20 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
194486c4cc 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>
2025-12-11 11:04:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0413f4220a 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>
2025-12-11 11:01:19 +00:00
CopilotGitHubrogerbarretoCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
67e83042cf .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>
2025-12-11 10:58:42 +00:00
5da1c2fd4c code ql sm04598 (#2723)
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
2025-12-11 10:34:58 +00:00
SergeyMenshykhandGitHub 989b6ebe71 .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)
* prevent nulls in AIAgent property

* address feedback
2025-12-11 09:50:25 +00:00
Evan MattsonandGitHub 3481914981 Capture file IDs from code interpreter in streaming responses (#2741) 2025-12-11 07:30:19 +00:00
KurtandGitHub 4c6a5d4aa1 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
2025-12-11 02:34:12 +00:00
Tao ChenandGitHub 191779ce80 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
2025-12-11 01:23:28 +00:00
CopilotGitHubstephentoubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Roger Barreto
638fbb5f03 Update Microsoft.Extensions.AI.* packages to 10.1.0 (#2735)
* Initial plan

* Update Microsoft.Extensions.AI dependencies to latest versions (10.1.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>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2025-12-10 19:04:05 +00:00
Tao ChenandGitHub 523305ac62 Python: Add factory pattern to sequential orchestration builder (#2710)
* Add factory pattern to sequential orchestration builder

* Use temp list to avoid override

* Add sample and some other fixes

* Fix comments

* Small fix

* Update readme
2025-12-10 18:16:10 +00:00
CopilotGitHublarohracopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Laveesh Rohra
3f4eeb00be Python: Change DurableAIAgent log level from warning to debug when invoked without thread (#2736)
* Initial plan

* Change logger.warning to logger.debug for missing thread

Co-authored-by: larohra <41490930+larohra@users.noreply.github.com>

* Fix test to use getMessage() method for log records

Co-authored-by: larohra <41490930+larohra@users.noreply.github.com>

* Remove test for logging change per review feedback

Co-authored-by: larohra <41490930+larohra@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: larohra <41490930+larohra@users.noreply.github.com>
Co-authored-by: Laveesh Rohra <larohra@microsoft.com>
2025-12-10 17:57:08 +00:00
Evan MattsonandGitHub b378ca75d1 Python: extend HITL support for all orchestration patterns (#2620)
* Support HITL for orchestration patterns

* Cleanup around naming

* Fix typing issues

* Clean up

* Naming clean up

* Updates to HITL to make it cleaner

* Rename human input hook to orchestration request info

* Clean up per PR feedback
2025-12-10 15:59:29 +00:00
1034 changed files with 76098 additions and 35868 deletions
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Documentation
url: https://aka.ms/agent-framework
about: Check out the official documentation for guides and API reference.
- name: Discussions
url: https://github.com/microsoft/agent-framework/discussions
about: Ask questions about Agent Framework.
+70
View File
@@ -0,0 +1,70 @@
name: .NET Bug Report
description: Report a bug in the Agent Framework .NET SDK
title: ".NET: [Bug]: "
labels: ["bug", ".NET"]
type: bug
body:
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and detailed description of the bug.
placeholder: |
- What happened?
- What did you expect to happen?
- Steps to reproduce the issue
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a minimal code sample that demonstrates the issue.
placeholder: |
```csharp
// Your code here
```
render: markdown
validations:
required: false
- type: textarea
id: error-messages
attributes:
label: Error Messages / Stack Traces
description: Include any error messages or stack traces you received.
placeholder: |
```
Paste error messages or stack traces here
```
render: markdown
validations:
required: false
- type: input
id: dotnet-packages
attributes:
label: Package Versions
description: List the Microsoft.Agents.* packages and versions you are using
placeholder: "e.g., Microsoft.Agents.AI.Abstractions: 1.0.0, Microsoft.Agents.AI.OpenAI: 1.0.0"
validations:
required: true
- type: input
id: dotnet-version
attributes:
label: .NET Version
description: What version of .NET are you using?
placeholder: "e.g., .NET 8.0"
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots that might be helpful.
placeholder: "Any additional information..."
validations:
required: false
@@ -0,0 +1,51 @@
name: Feature Request
description: Request a new feature for Microsoft Agent Framework
title: "[Feature]: "
type: feature
body:
- type: textarea
id: description
attributes:
label: Description
description: Please describe the feature you'd like and why it would be useful.
placeholder: |
Describe the feature you're requesting:
- What problem does it solve?
- What would the expected behavior be?
- Are there any alternatives you've considered?
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a code sample showing how you'd like to use this feature.
placeholder: |
```python
# Your code here
```
or
```csharp
// Your code here
```
render: markdown
validations:
required: false
- type: dropdown
id: language
attributes:
label: Language/SDK
description: Which language/SDK does this feature apply to?
options:
- Both
- .NET
- Python
- Other / Not Applicable
default: 0
validations:
required: false
+70
View File
@@ -0,0 +1,70 @@
name: Python Bug Report
description: Report a bug in the Agent Framework Python SDK
title: "Python: [Bug]: "
labels: ["bug", "Python"]
type: bug
body:
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and detailed description of the bug.
placeholder: |
- What happened?
- What did you expect to happen?
- Steps to reproduce the issue
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a minimal code sample that demonstrates the issue.
placeholder: |
```python
# Your code here
```
render: markdown
validations:
required: false
- type: textarea
id: error-messages
attributes:
label: Error Messages / Stack Traces
description: Include any error messages or stack traces you received.
placeholder: |
```
Paste error messages or stack traces here
```
render: markdown
validations:
required: false
- type: input
id: python-packages
attributes:
label: Package Versions
description: List the agent-framework-* packages and versions you are using
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
validations:
required: true
- type: input
id: python-version
attributes:
label: Python Version
description: What version of Python are you using?
placeholder: "e.g., Python 3.11"
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots that might be helpful.
placeholder: "Any additional information..."
validations:
required: false
@@ -28,6 +28,18 @@ runs:
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
echo "Azurite (Azure Storage emulator) is ready"
- name: Start Redis
shell: bash
run: |
if [ "$(docker ps -aq -f name=redis)" ]; then
echo "Stopping and removing existing Redis"
docker rm -f redis
fi
echo "Starting Redis"
docker run -d --name redis -p 6379:6379 redis:latest
echo "Waiting for Redis to be ready"
timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
echo "Redis is ready"
- name: Install Azure Functions Core Tools
shell: bash
run: |
@@ -105,7 +105,7 @@ After completing migration, verify these specific items:
1. **Compilation**: Execute `dotnet build` on all modified projects - zero errors required
2. **Namespace Updates**: Confirm all `using Microsoft.SemanticKernel.Agents` statements are replaced
3. **Method Calls**: Verify all `InvokeAsync` calls are changed to `RunAsync`
4. **Return Types**: Confirm handling of `AgentRunResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
4. **Return Types**: Confirm handling of `AgentResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
5. **Thread Creation**: Validate all thread creation uses `agent.GetNewThread()` pattern
6. **Tool Registration**: Ensure `[KernelFunction]` attributes are removed and `AIFunctionFactory.Create()` is used
7. **Options Configuration**: Verify `AgentRunOptions` or `ChatClientAgentRunOptions` replaces `AgentInvokeOptions`
@@ -119,7 +119,7 @@ Agent Framework provides functionality for creating and managing AI agents throu
Key API differences:
- Agent creation: Remove Kernel dependency, use direct client-based creation
- Method names: `InvokeAsync``RunAsync`, `InvokeStreamingAsync``RunStreamingAsync`
- Return types: `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>``AgentRunResponse`
- Return types: `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>``AgentResponse`
- Thread creation: Provider-specific constructors → `agent.GetNewThread()`
- Tool registration: `KernelPlugin` system → Direct `AIFunction` registration
- Options: `AgentInvokeOptions` → Provider-specific run options (e.g., `ChatClientAgentRunOptions`)
@@ -166,8 +166,8 @@ Replace these method calls:
| `thread.DeleteAsync()` | Provider-specific cleanup | Use provider client directly |
Return type changes:
- `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>``AgentRunResponse`
- `IAsyncEnumerable<StreamingChatMessageContent>``IAsyncEnumerable<AgentRunResponseUpdate>`
- `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>``AgentResponse`
- `IAsyncEnumerable<StreamingChatMessageContent>``IAsyncEnumerable<AgentResponseUpdate>`
</api_changes>
<configuration_changes>
@@ -191,8 +191,8 @@ Agent Framework changes these behaviors compared to Semantic Kernel Agents:
1. **Thread Management**: Agent Framework automatically manages thread state. Semantic Kernel required manual thread updates in some scenarios (e.g., OpenAI Responses).
2. **Return Types**:
- Non-streaming: Returns single `AgentRunResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
- Streaming: Returns `IAsyncEnumerable<AgentRunResponseUpdate>` instead of `IAsyncEnumerable<StreamingChatMessageContent>`
- Non-streaming: Returns single `AgentResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
- Streaming: Returns `IAsyncEnumerable<AgentResponseUpdate>` instead of `IAsyncEnumerable<StreamingChatMessageContent>`
3. **Tool Registration**: Agent Framework uses direct function registration without requiring `[KernelFunction]` attributes.
@@ -397,7 +397,7 @@ await foreach (AgentResponseItem<ChatMessageContent> item in agent.InvokeAsync(u
**With this Agent Framework non-streaming pattern:**
```csharp
AgentRunResponse result = await agent.RunAsync(userInput, thread, options);
AgentResponse result = await agent.RunAsync(userInput, thread, options);
Console.WriteLine(result);
```
@@ -411,7 +411,7 @@ await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync(
**With this Agent Framework streaming pattern:**
```csharp
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
{
Console.Write(update);
}
@@ -420,8 +420,8 @@ await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInpu
**Required changes:**
1. Replace `agent.InvokeAsync()` with `agent.RunAsync()`
2. Replace `agent.InvokeStreamingAsync()` with `agent.RunStreamingAsync()`
3. Change return type handling from `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` to `AgentRunResponse`
4. Change streaming type from `StreamingChatMessageContent` to `AgentRunResponseUpdate`
3. Change return type handling from `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` to `AgentResponse`
4. Change streaming type from `StreamingChatMessageContent` to `AgentResponseUpdate`
5. Remove `await foreach` for non-streaming calls
6. Access message content directly from result object instead of iterating
</api_changes>
@@ -661,7 +661,7 @@ await foreach (var result in agent.InvokeAsync(input, thread, options))
```csharp
ChatClientAgentRunOptions options = new(new ChatOptions { MaxOutputTokens = 1000 });
AgentRunResponse result = await agent.RunAsync(input, thread, options);
AgentResponse result = await agent.RunAsync(input, thread, options);
Console.WriteLine(result);
// Access underlying content when needed:
@@ -689,7 +689,7 @@ await foreach (var result in agent.InvokeAsync(input, thread, options))
**With this Agent Framework non-streaming usage pattern:**
```csharp
AgentRunResponse result = await agent.RunAsync(input, thread, options);
AgentResponse result = await agent.RunAsync(input, thread, options);
Console.WriteLine($"Tokens: {result.Usage.TotalTokenCount}");
```
@@ -709,7 +709,7 @@ await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsyn
**With this Agent Framework streaming usage pattern:**
```csharp
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
{
if (update.Contents.OfType<UsageContent>().FirstOrDefault() is { } usageContent)
{
@@ -839,7 +839,7 @@ var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
{
MaxOutputTokens = 8000,
// Breaking glass to access provider-specific options
RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions()
RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
{
ReasoningOptions = new()
{
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
+21 -15
View File
@@ -35,19 +35,25 @@ jobs:
contents: read
pull-requests: read
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
dotnet:
- 'dotnet/**'
cosmosdb:
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
# run only if 'dotnet' files were changed
- name: dotnet tests
if: steps.filter.outputs.dotnet == 'true'
run: echo "Dotnet file"
- name: dotnet CosmosDB tests
if: steps.filter.outputs.cosmosdb == 'true'
run: echo "Dotnet CosmosDB changes"
# run only if not 'dotnet' files were changed
- name: not dotnet tests
if: steps.filter.outputs.dotnet != 'true'
@@ -68,7 +74,7 @@ jobs:
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: |
@@ -77,6 +83,16 @@ jobs:
dotnet
python
workflow-samples
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Setup dotnet
uses: actions/setup-dotnet@v5.0.1
@@ -123,17 +139,7 @@ jobs:
popd
popd
rm -rf "$TEMP_DIR"
# Start Cosmos DB Emulator for Cosmos-based unit tests (only on Windows)
- name: Start Azure Cosmos DB Emulator
if: runner.os == 'Windows'
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Run Unit Tests
shell: bash
run: |
@@ -225,7 +231,7 @@ jobs:
- name: Upload coverage report artifact
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
path: ./TestResults/Reports # Directory containing files to upload
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
+50 -11
View File
@@ -45,19 +45,58 @@ jobs:
labels.push("triage")
}
// Check if the body or the title contains the word 'python' (case-insensitive)
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
// Add the 'python' label to the array
labels.push("python")
// Helper function to extract field value from issue form body
// Issue forms format fields as: ### Field Name\n\nValue
function getFormFieldValue(body, fieldName) {
if (!body) return null
const regex = new RegExp(`###\\s*${fieldName}\\s*\\n\\n([^\\n#]+)`, 'i')
const match = body.match(regex)
return match ? match[1].trim() : null
}
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) ||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
// Add the '.NET' label to the array
labels.push(".NET")
// Check for language from issue form dropdown first
const languageField = getFormFieldValue(body, 'Language')
let languageLabelAdded = false
if (languageField) {
if (languageField === 'Python') {
labels.push("python")
languageLabelAdded = true
} else if (languageField === '.NET') {
labels.push(".NET")
languageLabelAdded = true
}
// 'None / Not Applicable' - don't add any language label
}
// Fallback: Check if the body or the title contains the word 'python' (case-insensitive)
// Only if language wasn't already determined from the form field
if (!languageLabelAdded) {
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
// Add the 'python' label to the array
labels.push("python")
}
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
if ((body != null && body.match(/\.net/i)) || (title != null && title.match(/\.net/i)) ||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
// Add the '.NET' label to the array
labels.push(".NET")
}
}
// Check for issue type from issue form dropdown
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
if (issueTypeField) {
if (issueTypeField === 'Bug') {
labels.push("bug")
} else if (issueTypeField === 'Feature Request') {
labels.push("enhancement")
} else if (issueTypeField === 'Question') {
labels.push("question")
}
}
// Add the labels to the issue (only if there are labels to add)
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04
# check out the latest version of the code
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
+1
View File
@@ -29,3 +29,4 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
timeout: 3600
interval: 30
ignored: CodeQL
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -39,7 +39,7 @@ jobs:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -59,7 +59,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
+5 -5
View File
@@ -28,7 +28,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -75,7 +75,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -97,7 +97,7 @@ jobs:
id: azure-functions-setup
- name: Test with pytest
timeout-minutes: 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 600 --retries 3 --retry-delay 10
working-directory: ./python
- name: Test core samples
timeout-minutes: 10
@@ -135,7 +135,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -154,7 +154,7 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 10
run: uv run poe azure-ai-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -19,9 +19,9 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Download coverage report
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
run-id: ${{ github.event.workflow_run.id }}
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
env:
UV_PYTHON: "3.10"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Save the PR number to a file since the workflow_run event
# in the coverage report workflow does not have access to it
- name: Save PR number
@@ -38,7 +38,7 @@ jobs:
- name: Run all tests with coverage report
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
- name: Upload coverage report
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
path: |
python/python-coverage.xml
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+1
View File
@@ -226,3 +226,4 @@ local.settings.json
# Database files
*.db
python/dotnet-ref
+1 -1
View File
@@ -1,3 +1,3 @@
# Declarative Agents
This folder contains sample agent definitions than be ran using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
+5 -5
View File
@@ -10,19 +10,19 @@ model:
temperature: 0.9
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_API_KEY
kind: key
apiKey: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
type: string
kind: string
required: true
description: The language of the answer.
answer:
type: string
kind: string
required: true
description: The answer text.
type:
type: string
kind: string
required: true
description: The type of the response.
+22 -22
View File
@@ -64,7 +64,7 @@ Approaches observed from the compared SDKs:
| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
@@ -163,8 +163,8 @@ foreach (var update in response.Messages)
### Option 2 Run: Container with Primary and Secondary Properties, RunStreaming: Stream of Primary + Secondary
Run returns a new response type that has separate properties for the Primary Content and the Secondary Updates leading up to it.
The Primary content is available in the `AgentRunResponse.Messages` property while Secondary updates are in a new `AgentRunResponse.Updates` property.
`AgentRunResponse.Text` returns the Primary content text.
The Primary content is available in the `AgentResponse.Messages` property while Secondary updates are in a new `AgentResponse.Updates` property.
`AgentResponse.Text` returns the Primary content text.
Since streaming would still need to return an `IAsyncEnumerable` of updates, the design would differ from non-streaming.
With non-streaming Primary and Secondary content is split into separate lists, while with streaming it's combined in one stream.
@@ -232,24 +232,24 @@ await foreach (var update in responses)
```csharp
class Agent
{
public abstract Task<AgentRunResponse> RunAsync(
public abstract Task<AgentResponse> RunAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
}
class AgentRunResponse : ChatResponse
class AgentResponse : ChatResponse
{
}
public class AgentRunResponseUpdate : ChatResponseUpdate
public class AgentResponseUpdate : ChatResponseUpdate
{
}
```
@@ -265,20 +265,20 @@ The new types could also exclude properties that make less sense for agents, lik
```csharp
class Agent
{
public abstract Task<AgentRunResponse> RunAsync(
public abstract Task<AgentResponse> RunAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
}
class AgentRunResponse // Compare with ChatResponse
class AgentResponse // Compare with ChatResponse
{
public string Text { get; } // Aggregation of TextContent from messages.
@@ -294,12 +294,12 @@ class AgentRunResponse // Compare with ChatResponse
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
// Not Included in AgentRunResponse compared to ChatResponse
// Not Included in AgentResponse compared to ChatResponse
public ChatFinishReason? FinishReason { get; set; }
public string? ConversationId { get; set; }
public string? ModelId { get; set; }
public class AgentRunResponseUpdate // Compare with ChatResponseUpdate
public class AgentResponseUpdate // Compare with ChatResponseUpdate
{
public string Text { get; } // Aggregation of TextContent from Contents.
@@ -317,7 +317,7 @@ public class AgentRunResponseUpdate // Compare with ChatResponseUpdate
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
// Not Included in AgentRunResponseUpdate compared to ChatResponseUpdate
// Not Included in AgentResponseUpdate compared to ChatResponseUpdate
public ChatFinishReason? FinishReason { get; set; }
public string? ConversationId { get; set; }
public string? ModelId { get; set; }
@@ -360,7 +360,7 @@ public class ChatFinishReason
### Option 2: Add another property on responses for AgentRun
```csharp
class AgentRunResponse
class AgentResponse
{
...
public AgentRun RunReference { get; set; } // Reference to long running process
@@ -368,7 +368,7 @@ class AgentRunResponse
}
public class AgentRunResponseUpdate
public class AgentResponseUpdate
{
...
public AgentRun RunReference { get; set; } // Reference to long running process
@@ -424,7 +424,7 @@ Note that where an agent doesn't support structured output, it may also be possi
See [Structured Outputs Support](#structured-outputs-support) for a comparison on what other agent frameworks and protocols support.
To support a good user experience for structured outputs, I'm proposing that we follow the pattern used by MEAI.
We would add a generic version of `AgentRunResponse<T>`, that allows us to get the agent result already deserialized into our preferred type.
We would add a generic version of `AgentResponse<T>`, that allows us to get the agent result already deserialized into our preferred type.
This would be coupled with generic overload extension methods for Run that automatically builds a schema from the supplied type and updates
the run options.
@@ -438,14 +438,14 @@ class Movie
public int ReleaseYear { get; set; }
}
AgentRunResponse<Movie[]> response = agent.RunAsync<Movie[]>("What are the top 3 children's movies of the 80s.");
AgentResponse<Movie[]> response = agent.RunAsync<Movie[]>("What are the top 3 children's movies of the 80s.");
Movie[] movies = response.Result
```
If we only support requesting a schema at agent creation time or where an agent has a built in schema, the following would be the preferred approach:
```csharp
AgentRunResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
AgentResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
Movie[] movies = response.TryParseStructuredOutput<Movie[]>();
```
@@ -463,7 +463,7 @@ Option 2 chosen so that we can vary Agent responses independently of Chat Client
### StructuredOutputs Decision
We will not support structured output per run request, but individual agents are free to allow this on the concrete implementation or at construction time.
We will however add support for easily extracting a structured output type from the `AgentRunResponse`.
We will however add support for easily extracting a structured output type from the `AgentResponse`.
## Addendum 1: AIContext Derived Types for different response types / Gap Analysis (Work in progress)
@@ -495,8 +495,8 @@ We need to decide what AIContent types, each agent response type will be mapped
| SDK | Structured Outputs support |
|-|-|
| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
| Google ADK | **Approach 1** Both [input and output shemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.structured_output) |
| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
@@ -508,7 +508,7 @@ We need to decide what AIContent types, each agent response type will be mapped
|-|-|
| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/api-reference/types/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) class with options that are tied closely to LLM operations. |
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
@@ -54,7 +54,7 @@ The table below represents the majority of the naming changes discussed in issue
| *Mcp* & *Http* | *MCP* & *HTTP* | accepted | Acronyms should be uppercased in class names, according to PEP 8. | None |
| `agent.run_streaming` | `agent.run_stream` | accepted | Shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
| `workflow.run_streaming` | `workflow.run_stream` | accepted | In sync with `agent.run_stream` and shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
| AgentRunResponse & AgentRunResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
| AgentResponse & AgentResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
| *Content | * | rejected | Rejected other content type renames (removing `Content` suffix) because it would reduce clarity and discoverability. | Item was also considered, but rejected as it is very similar to Content, but would be inconsistent with dotnet. |
| ChatResponse & ChatResponseUpdate | Response & ResponseUpdate | rejected | Rejected, because Response is too generic. | None |
+6 -6
View File
@@ -161,11 +161,11 @@ while (response.ApprovalRequests.Count > 0)
response = await agent.RunAsync(messages, thread);
}
class AgentRunResponse
class AgentResponse
{
...
// A new property on AgentRunResponse to aggregate the ApprovalRequestContent items from
// A new property on AgentResponse to aggregate the ApprovalRequestContent items from
// the response messages (Similar to the Text property).
public IEnumerable<ApprovalRequestContent> ApprovalRequests { get; set; }
@@ -251,11 +251,11 @@ while (response.UserInputRequests.Any())
response = await agent.RunAsync(messages, thread);
}
class AgentRunResponse
class AgentResponse
{
...
// A new property on AgentRunResponse to aggregate the UserInputRequestContent items from
// A new property on AgentResponse to aggregate the UserInputRequestContent items from
// the response messages (Similar to the Text property).
public IReadOnlyList<UserInputRequestContent> UserInputRequests { get; set; }
@@ -366,11 +366,11 @@ while (response.UserInputRequests.Any())
response = await agent.RunAsync(messages, thread);
}
class AgentRunResponse
class AgentResponse
{
...
// A new property on AgentRunResponse to aggregate the UserInputRequestContent items from
// A new property on AgentResponse to aggregate the UserInputRequestContent items from
// the response messages (Similar to the Text property).
public IEnumerable<UserInputRequestContent> UserInputRequests { get; set; }
@@ -115,7 +115,7 @@ public class AIAgent
}
}
public async Task<AgentRunResponse> RunAsync(
public async Task<AgentResponse> RunAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -135,7 +135,7 @@ public class AIAgent
return context.Response ?? throw new InvalidOperationException("Agent execution did not produce a response");
}
protected abstract Task<AgentRunResponse> ExecuteCoreLogicAsync(
protected abstract Task<AgentResponse> ExecuteCoreLogicAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread,
AgentRunOptions? options,
@@ -190,7 +190,7 @@ internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
public GuardrailCallbackAgent(AIAgent innerAgent) : base(innerAgent) { }
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
var filteredMessages = this.FilterMessages(messages);
Console.WriteLine($"Guardrail Middleware - Filtered messages: {new ChatResponse(filteredMessages).Text}");
@@ -202,14 +202,14 @@ internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
return response;
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var filteredMessages = this.FilterMessages(messages);
await foreach (var update in this.InnerAgent.RunStreamingAsync(filteredMessages, thread, options, cancellationToken))
{
if (update.Text != null)
{
yield return new AgentRunResponseUpdate(update.Role, this.FilterContent(update.Text));
yield return new AgentResponseUpdate(update.Role, this.FilterContent(update.Text));
}
else
{
@@ -252,7 +252,7 @@ internal sealed class RunningCallbackHandlerAgent : DelegatingAIAgent
this._func = func;
}
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
var context = new AgentInvokeCallbackContext(this, messages, thread, options, isStreaming: false, cancellationToken);
@@ -469,7 +469,7 @@ public sealed class CallbackEnabledAgent : DelegatingAIAgent
this._callbacksProcessor = callbackMiddlewareProcessor ?? new();
}
public override async Task<AgentRunResponse> RunAsync(
public override async Task<AgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -541,7 +541,7 @@ public abstract class AgentContext
public class AgentRunContext : AgentContext
{
public IList<ChatMessage> Messages { get; set; }
public AgentRunResponse? Response { get; set; }
public AgentResponse? Response { get; set; }
public AgentThread? Thread { get; }
public AgentRunContext(AIAgent agent, IList<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options)
@@ -687,7 +687,7 @@ This section considers different options for exposing the `RunId`, `Status`, and
#### 4.1. As AIContent
The `AsyncRunContent` class will represent a long-running operation initiated and managed by an agent/LLM.
Items of this content type will be returned in a chat message as part of the `AgentRunResponse` or `ChatResponse`
Items of this content type will be returned in a chat message as part of the `AgentResponse` or `ChatResponse`
response to represent the long-running operation.
The `AsyncRunContent` class has two properties: `RunId` and `Status`. The `RunId` identifies the
@@ -1162,29 +1162,29 @@ For cancellation and deletion of long-running operations, new methods will be ad
public abstract class AIAgent
{
// Existing methods...
public Task<AgentRunResponse> RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
public Task<AgentResponse> RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
// New methods for uncommon operations
public virtual Task<AgentRunResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
public virtual Task<AgentResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
{
return Task.FromResult<AgentRunResponse?>(null);
return Task.FromResult<AgentResponse?>(null);
}
public virtual Task<AgentRunResponse?> DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
public virtual Task<AgentResponse?> DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
{
return Task.FromResult<AgentRunResponse?>(null);
return Task.FromResult<AgentResponse?>(null);
}
}
// Agent that supports update and cancellation
public class CustomAgent : AIAgent
{
public override async Task<AgentRunResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
public override async Task<AgentResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
{
var response = await this._client.CancelRunAsync(id, options?.Thread?.ConversationId);
return ConvertToAgentRunResponse(response);
return ConvertToAgentResponse(response);
}
// No overload for DeleteRunAsync as it's not supported by the underlying API
@@ -1195,7 +1195,7 @@ AIAgent agent = new CustomAgent();
AgentThread thread = agent.GetNewThread();
AgentRunResponse response = await agent.RunAsync("What is the capital of France?");
AgentResponse response = await agent.RunAsync("What is the capital of France?");
response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOptions { Thread = thread });
```
@@ -1251,10 +1251,10 @@ public class AgentRunOptions
AIAgent agent = ...; // Get an instance of an AIAgent
// Start a long-running execution for the prompt if supported by the underlying API
AgentRunResponse response = await agent.RunAsync("<prompt>", new AgentRunOptions { AllowLongRunningResponses = true });
AgentResponse response = await agent.RunAsync("<prompt>", new AgentRunOptions { AllowLongRunningResponses = true });
// Start a quick prompt
AgentRunResponse response = await agent.RunAsync("<prompt>");
AgentResponse response = await agent.RunAsync("<prompt>");
```
**Pros:**
@@ -1279,7 +1279,7 @@ Below are the details of the option selected for chat clients that is also selec
#### 3.1 Continuation Token of a Custom Type
This option suggests using `ContinuationToken` to encapsulate all properties representing a long-running operation. The continuation token will be returned by agents in the
`ContinuationToken` property of the `AgentRunResponse` and `AgentRunResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
`ContinuationToken` property of the `AgentResponse` and `AgentResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
of the property will indicate that the response is not part of a long-running operation or the long-running operation has been completed. Callers will set the token in the
`ContinuationToken` property of the `AgentRunOptions` class in follow-up calls to the `Run{Streaming}Async` methods to indicate that they want to "continue" the long-running
operation identified by the token.
@@ -1313,18 +1313,18 @@ public class AgentRunOptions
public ResponseContinuationToken? ContinuationToken { get; set; }
}
public class AgentRunResponse
public class AgentResponse
{
public ResponseContinuationToken? ContinuationToken { get; }
}
public class AgentRunResponseUpdate
public class AgentResponseUpdate
{
public ResponseContinuationToken? ContinuationToken { get; }
}
// Usage example
AgentRunResponse response = await agent.RunAsync("What is the capital of France?");
AgentResponse response = await agent.RunAsync("What is the capital of France?");
AgentRunOptions options = new() { ContinuationToken = response.ContinuationToken };
+2 -2
View File
@@ -36,7 +36,7 @@ Chosen option: "Current approach with internal event types and framework-native
- Protects consumers from protocol changes by keeping AG-UI events internal
- Maintains framework abstractions through conversion at boundaries
- Uses existing framework types (AgentRunResponseUpdate, ChatMessage) for public API
- Uses existing framework types (AgentResponseUpdate, ChatMessage) for public API
- Focuses on core text streaming functionality
- Leverages existing properties (ConversationId, ResponseId, ErrorContent) instead of custom types
- Provides bidirectional client and server support
@@ -69,7 +69,7 @@ Chosen option: "Current approach with internal event types and framework-native
3. **Agent Factory Pattern** - `MapAGUIAgent` uses factory function `(messages) => AIAgent` to allow request-specific agent configuration supporting multi-tenancy
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentRunResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentRunResponseUpdate`)
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentResponseUpdate`)
5. **Thread Management** - `AGUIAgentThread` stores only `ThreadId` with thread ID communicated via `ConversationId`; applications manage persistence for parity with other implementations and to be compliant with the protocol. Future extensions will support having the server manage the conversation.
+368
View File
@@ -0,0 +1,368 @@
---
status: proposed
contact: dmytrostruk
date: 2025-12-12
deciders: dmytrostruk, markwallace-microsoft, eavanvalkenburg, giles17
---
# Create/Get Agent API
## Context and Problem Statement
There is a misalignment between the create/get agent API in the .NET and Python implementations.
In .NET, the `CreateAIAgent` method can create either a local instance of an agent or a remote instance if the backend provider supports it. For remote agents, once the agent is created, you can retrieve an existing remote agent by using the `GetAIAgent` method. If a backend provider doesn't support remote agents, `CreateAIAgent` just initializes a new local agent instance and `GetAIAgent` is not available. There is also a `BuildAIAgent` method, which is an extension for the `ChatClientBuilder` class from `Microsoft.Extensions.AI`. It builds pipelines of `IChatClient` instances with an `IServiceProvider`. This functionality does not exist in Python, so `BuildAIAgent` is out of scope.
In Python, there is only one `create_agent` method, which always creates a local instance of the agent. If the backend provider supports remote agents, the remote agent is created only on the first `agent.run()` invocation.
Below is a short summary of different providers and their APIs in .NET:
| Package | Method | Behavior | Python support |
|---|---|---|---|
| Microsoft.Agents.AI | `CreateAIAgent` (based on `IChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
| Microsoft.Agents.AI.Anthropic | `CreateAIAgent` (based on `IBetaService` and `IAnthropicClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`AnthropicClient` inherits `BaseChatClient`, which exposes `create_agent`). |
| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent` (based on `AIProjectClient` with `AgentReference`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent`/`GetAIAgentAsync` (with `Name`/`ChatClientAgentOptions`) | Fetches `AgentRecord` via HTTP, then creates a local `ChatClientAgent` instance. | No |
| Microsoft.Agents.AI.AzureAI (V2) | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AIProjectClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent` (based on `PersistentAgentsClient` with `PersistentAgent`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `PersistentAgent` via HTTP, then creates a local `ChatClientAgent` instance. | No |
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `CreateAIAgent`/`CreateAIAgentAsync` | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
| Microsoft.Agents.AI.OpenAI | `GetAIAgent` (based on `AssistantClient` with `Assistant`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
| Microsoft.Agents.AI.OpenAI | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `Assistant` via HTTP, then creates a local `ChatClientAgent` instance. | No |
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AssistantClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `ChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `OpenAIResponseClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
Another difference between Python and .NET implementation is that in .NET `CreateAIAgent`/`GetAIAgent` methods are implemented as extension methods based on underlying SDK client, like `AIProjectClient` from Azure AI or `AssistantClient` from OpenAI:
```csharp
// Definition
public static ChatClientAgent CreateAIAgent(
this AIProjectClient aiProjectClient,
string name,
string model,
string instructions,
string? description = null,
IList<AITool>? tools = null,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{ }
// Usage
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); // Initialization of underlying SDK client
var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AgentName, model: deploymentName, instructions: AgentInstructions, tools: [tool]); // ChatClientAgent creation from underlying SDK client
// Alternative usage (same as extension method, just explicit syntax)
var newAgent = await AzureAIProjectChatClientExtensions.CreateAIAgentAsync(
aiProjectClient,
name: AgentName,
model: deploymentName,
instructions: AgentInstructions,
tools: [tool]);
```
Python doesn't support extension methods. Currently `create_agent` method is defined on `BaseChatClient`, but this method only creates a local instance of `ChatAgent` and it can't create remote agents for providers that support it for a couple of reasons:
- It's defined as non-async.
- `BaseChatClient` implementation is stateful for providers like Azure AI or OpenAI Assistants. The implementation stores agent/assistant metadata like `AgentId` and `AgentName`, so currently it's not possible to create different instances of `ChatAgent` from a single `BaseChatClient` in case if the implementation is stateful.
## Decision Drivers
- API should be aligned between .NET and Python.
- API should be intuitive and consistent between backend providers in .NET and Python.
## Considered Options
Add missing implementations on the Python side. This should include the following:
### agent-framework-azure-ai (both V1 and V2)
- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
- Add a `get_agent` method that accepts an agent identifier, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
.NET:
```csharp
var agent1 = new AIProjectClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from Azure.AI.Projects.OpenAI.AgentReference
var agent2 = new AIProjectClient(...).GetAIAgent(agentName); // Fetches agent data, creates a local ChatClientAgent instance
var agent3 = new AIProjectClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
```
### agent-framework-core (OpenAI Assistants)
- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
- Add a `get_agent` method that accepts an agent name, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
.NET:
```csharp
var agent1 = new AssistantClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from OpenAI.Assistants.Assistant
var agent2 = new AssistantClient(...).GetAIAgent(agentId); // Fetches agent data, creates a local ChatClientAgent instance
var agent3 = new AssistantClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
```
### Possible Python implementations
Methods like `create_agent` and `get_agent` should be implemented separately or defined on some stateless component that will allow to create multiple agents from the same instance/place.
Possible options:
#### Option 1: Module-level functions
Implement free functions in the provider package that accept the underlying SDK client as the first argument (similar to .NET extension methods, but expressed in Python).
Example:
```python
from agent_framework.azure import create_agent, get_agent
ai_project_client = AIProjectClient(...)
# Creates a remote agent first, then returns a local ChatAgent wrapper
created_agent = await create_agent(
ai_project_client,
name="",
instructions="",
tools=[tool],
)
# Gets an existing remote agent and returns a local ChatAgent wrapper
first_agent = await get_agent(ai_project_client, agent_id=agent_id)
# Wraps an SDK agent instance (no extra HTTP call)
second_agent = get_agent(ai_project_client, agent_reference)
```
Pros:
- Naturally supports async `create_agent` / `get_agent`.
- Supports multiple agents per SDK client.
- Closest conceptual match to .NET extension methods while staying Pythonic.
Cons:
- Discoverability is lower (users need to know where the functions live).
- Verbose when creating multiple agents (client must be passed every time):
```python
agent1 = await azure_agents.create_agent(client, name="Agent1", ...)
agent2 = await azure_agents.create_agent(client, name="Agent2", ...)
```
#### Option 2: Provider object
Introduce a dedicated provider type that is constructed from the underlying SDK client, and exposes async `create_agent` / `get_agent` methods.
Example:
```python
from agent_framework.azure import AzureAIAgentProvider
ai_project_client = AIProjectClient(...)
provider = AzureAIAgentProvider(ai_project_client)
agent = await provider.create_agent(
name="",
instructions="",
tools=[tool],
)
agent = await provider.get_agent(agent_id=agent_id)
agent = provider.get_agent(agent_reference=agent_reference)
```
Pros:
- High discoverability and clear grouping of related behavior.
- Keeps SDK clients unchanged and supports multiple agents per SDK client.
- Concise when creating multiple agents (client passed once):
```python
provider = AzureAIAgentProvider(ai_project_client)
agent1 = await provider.create_agent(name="Agent1", ...)
agent2 = await provider.create_agent(name="Agent2", ...)
```
Cons:
- Adds a new public concept/type for users to learn.
#### Option 3: Inheritance (SDK client subclass)
Create a subclass of the underlying SDK client and add `create_agent` / `get_agent` methods.
Example:
```python
class ExtendedAIProjectClient(AIProjectClient):
async def create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
...
async def get_agent(self, *, agent_id: str | None = None, sdk_agent=None, **kwargs) -> ChatAgent:
...
client = ExtendedAIProjectClient(...)
agent = await client.create_agent(name="", instructions="")
```
Pros:
- Discoverable and ergonomic call sites.
- Mirrors the .NET “methods on the client” feeling.
Cons:
- Many SDK clients are not designed for inheritance; SDK upgrades can break subclasses.
- Users must opt into subclass everywhere.
- Typing/initialization can be tricky if the SDK client has non-trivial constructors.
#### Option 4: Monkey patching
Attach `create_agent` / `get_agent` methods to an SDK client class (or instance) at runtime.
Example:
```python
def _create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
...
AIProjectClient.create_agent = _create_agent # monkey patch
```
Pros:
- Produces “extension method-like” call sites without wrappers or subclasses.
Cons:
- Fragile across SDK updates and difficult to type-check.
- Surprising behavior (global side effects), potential conflicts across packages.
- Harder to support/debug, especially in larger apps and test suites.
## Decision Outcome
Implement `create_agent`/`get_agent`/`as_agent` API via **Option 2: Provider object**.
### Rationale
| Aspect | Option 1 (Functions) | Option 2 (Provider) |
|--------|----------------------|---------------------|
| Multiple implementations | One package may contain V1, V2, and other agent types. Function names like `create_agent` become ambiguous - which agent type does it create? | Each provider class is explicit: `AzureAIAgentsProvider` vs `AzureAIProjectAgentProvider` |
| Discoverability | Users must know to import specific functions from the package | IDE autocomplete on provider instance shows all available methods |
| Client reuse | SDK client must be passed to every function call: `create_agent(client, ...)`, `get_agent(client, ...)` | SDK client passed once at construction: `provider = Provider(client)` |
**Option 1 example:**
```python
from agent_framework.azure import create_agent, get_agent
agent1 = await create_agent(client, name="Agent1", ...) # Which agent type, V1 or V2?
agent2 = await create_agent(client, name="Agent2", ...) # Repetitive client passing
```
**Option 2 example:**
```python
from agent_framework.azure import AzureAIProjectAgentProvider
provider = AzureAIProjectAgentProvider(client) # Clear which service, client passed once
agent1 = await provider.create_agent(name="Agent1", ...)
agent2 = await provider.create_agent(name="Agent2", ...)
```
### Method Naming
| Operation | Python | .NET | Async |
|-----------|--------|------|-------|
| Create on service | `create_agent()` | `CreateAIAgent()` | Yes |
| Get from service | `get_agent(id=...)` | `GetAIAgent(agentId)` | Yes |
| Wrap SDK object | `as_agent(reference)` | `AsAIAgent(agentInstance)` | No |
The method names (`create_agent`, `get_agent`) do not explicitly mention "service" or "remote" because:
- In Python, the provider class name explicitly identifies the service (`AzureAIAgentsProvider`, `OpenAIAssistantProvider`), making additional qualifiers in method names redundant.
- In .NET, these are extension methods on `AIProjectClient` or `AssistantClient`, which already imply service operations.
### Provider Class Naming
| Package | Provider Class | SDK Client | Service |
|---------|---------------|------------|---------|
| `agent_framework.azure` | `AzureAIProjectAgentProvider` | `AIProjectClient` | Azure AI Agent Service, based on Responses API (V2) |
| `agent_framework.azure` | `AzureAIAgentsProvider` | `AgentsClient` | Azure AI Agent Service (V1) |
| `agent_framework.openai` | `OpenAIAssistantProvider` | `AsyncOpenAI` | OpenAI Assistants API |
> **Note:** Azure AI naming is temporary. Final naming will be updated according to Azure AI / Microsoft Foundry renaming decisions.
### Usage Examples
#### Azure AI Agent Service V2 (based on Responses API)
```python
from agent_framework.azure import AzureAIProjectAgentProvider
from azure.ai.projects import AIProjectClient
client = AIProjectClient(endpoint, credential)
provider = AzureAIProjectAgentProvider(client)
# Create new agent on service
agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
# Get existing agent by name
agent = await provider.get_agent(agent_name="MyAgent")
# Wrap already-fetched SDK object (no HTTP calls)
agent_ref = await client.agents.get("MyAgent")
agent = provider.as_agent(agent_ref)
```
#### Azure AI Persistent Agents V1
```python
from agent_framework.azure import AzureAIAgentsProvider
from azure.ai.agents import AgentsClient
client = AgentsClient(endpoint, credential)
provider = AzureAIAgentsProvider(client)
agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
agent = await provider.get_agent(agent_id="persistent-agent-456")
agent = provider.as_agent(persistent_agent)
```
#### OpenAI Assistants
```python
from agent_framework.openai import OpenAIAssistantProvider
from openai import OpenAI
client = OpenAI()
provider = OpenAIAssistantProvider(client)
agent = await provider.create_agent(name="MyAssistant", model="gpt-4", instructions="...")
agent = await provider.get_agent(assistant_id="asst_123")
agent = provider.as_agent(assistant)
```
#### Local-Only Agents (No Provider)
Current method `create_agent` (python) / `CreateAIAgent` (.NET) can be renamed to `as_agent` (python) / `AsAIAgent` (.NET) to emphasize the conversion logic rather than creation/initialization logic and to avoid collision with `create_agent` method for remote calls.
```python
from agent_framework import ChatAgent
from agent_framework.openai import OpenAIChatClient
# Convert chat client to ChatAgent (no remote service involved)
client = OpenAIChatClient(model="gpt-4")
agent = client.as_agent(name="LocalAgent", instructions="...") # instead of create_agent
```
### Adding New Agent Types
Python:
1. Create provider class in appropriate package.
2. Implement `create_agent`, `get_agent`, `as_agent` as applicable.
.NET:
1. Create static class for extension methods.
2. Implement `CreateAIAgentAsync`, `GetAIAgentAsync`, `AsAIAgent` as applicable.
@@ -0,0 +1,129 @@
---
# These are optional elements. Feel free to remove any of them.
status: proposed
contact: eavanvalkenburg
date: 2026-01-08
deciders: eavanvalkenburg, markwallace-microsoft, sphenry, alliscode, johanst, brettcannon
consulted: taochenosu, moonbox3, dmytrostruk, giles17
---
# Leveraging TypedDict and Generic Options in Python Chat Clients
## Context and Problem Statement
The Agent Framework Python SDK provides multiple chat client implementations for different providers (OpenAI, Anthropic, Azure AI, Bedrock, Ollama, etc.). Each provider has unique configuration options beyond the common parameters defined in `ChatOptions`. Currently, developers using these clients lack type safety and IDE autocompletion for provider-specific options, leading to runtime errors and a poor developer experience.
How can we provide type-safe, discoverable options for each chat client while maintaining a consistent API across all implementations?
## Decision Drivers
- **Type Safety**: Developers should get compile-time/static analysis errors when using invalid options
- **IDE Support**: Full autocompletion and inline documentation for all available options
- **Extensibility**: Users should be able to define custom options that extend provider-specific options
- **Consistency**: All chat clients should follow the same pattern for options handling
- **Provider Flexibility**: Each provider can expose its unique options without affecting the common interface
## Considered Options
- **Option 1: Status Quo - Class `ChatOptions` with `**kwargs`**
- **Option 2: TypedDict with Generic Type Parameters**
### Option 1: Status Quo - Class `ChatOptions` with `**kwargs`
The current approach uses a base `ChatOptions` Class with common parameters, and provider-specific options are passed via `**kwargs` or loosely typed dictionaries.
```python
# Current usage - no type safety for provider-specific options
response = await client.get_response(
messages=messages,
temperature=0.7,
top_k=40,
random=42, # No validation
)
```
**Pros:**
- Simple implementation
- Maximum flexibility
**Cons:**
- No type checking for provider-specific options
- No IDE autocompletion for available options
- Runtime errors for typos or invalid options
- Documentation must be consulted for each provider
### Option 2: TypedDict with Generic Type Parameters (Chosen)
Each chat client is parameterized with a TypeVar bound to a provider-specific `TypedDict` that extends `ChatOptions`. This enables full type safety and IDE support.
```python
# Provider-specific TypedDict
class AnthropicChatOptions(ChatOptions, total=False):
"""Anthropic-specific chat options."""
top_k: int
thinking: ThinkingConfig
# ... other Anthropic-specific options
# Generic chat client
class AnthropicChatClient(ChatClientBase[TAnthropicChatOptions]):
...
client = AnthropicChatClient(...)
# Usage with full type safety
response = await client.get_response(
messages=messages,
options={
"temperature": 0.7,
"top_k": 40,
"random": 42, # fails type checking and IDE would flag this
}
)
# Users can extend for custom options
class MyAnthropicOptions(AnthropicChatOptions, total=False):
custom_field: str
client = AnthropicChatClient[MyAnthropicOptions](...)
# Usage of custom options with full type safety
response = await client.get_response(
messages=messages,
options={
"temperature": 0.7,
"top_k": 40,
"custom_field": "value",
}
)
```
**Pros:**
- Full type safety with static analysis
- IDE autocompletion for all options
- Compile-time error detection
- Self-documenting through type hints
- Users can extend options for their specific needs or advances in models
**Cons:**
- More complex implementation
- Some type: ignore comments needed for TypedDict field overrides
- Minor: Requires TypeVar with default (Python 3.13+ or typing_extensions)
> [NOTE!]
> In .NET this is already achieved through overloads on the `GetResponseAsync` method for each provider-specific options class, e.g., `AnthropicChatOptions`, `OpenAIChatOptions`, etc. So this does not apply to .NET.
### Implementation Details
1. **Base Protocol**: `ChatClientProtocol[TOptions]` is generic over options type, with default set to `ChatOptions` (the new TypedDict)
2. **Provider TypedDicts**: Each provider defines its options extending `ChatOptions`
They can even override fields with type=None to indicate they are not supported.
3. **TypeVar Pattern**: `TProviderOptions = TypeVar("TProviderOptions", bound=TypedDict, default=ProviderChatOptions, contravariant=True)`
4. **Option Translation**: Common options are kept in place,and explicitly documented in the Options class how they are used. (e.g., `user``metadata.user_id`) in `_prepare_options` (for Anthropic) to preserve easy use of common options.
## Decision Outcome
Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.
See [typed_options.py](../../python/samples/getting_started/chat_client/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
@@ -0,0 +1,258 @@
---
status: Accepted
contact: eavanvalkenburg
date: 2026-01-06
deciders: markwallace-microsoft, dmytrostruk, taochenosu, alliscode, moonbox3, sphenry
consulted: sergeymenshykh, rbarreto, dmytrostruk, westey-m
informed:
---
# Simplify Python Get Response API into a single method
## Context and Problem Statement
Currently chat clients must implement two separate methods to get responses, one for streaming and one for non-streaming. This adds complexity to the client implementations and increases the maintenance burden. This was likely done because the .NET version cannot do proper typing with a single method, in Python this is possible and this for instance is also how the OpenAI python client works, this would then also make it simpler to work with the Python version because there is only one method to learn about instead of two.
## Implications of this change
### Current Architecture Overview
The current design has **two separate methods** at each layer:
| Layer | Non-streaming | Streaming |
|-------|---------------|-----------|
| **Protocol** | `get_response()``ChatResponse` | `get_streaming_response()``AsyncIterable[ChatResponseUpdate]` |
| **BaseChatClient** | `get_response()` (public) | `get_streaming_response()` (public) |
| **Implementation** | `_inner_get_response()` (private) | `_inner_get_streaming_response()` (private) |
### Key Usage Areas Identified
#### 1. **ChatAgent** (_agents.py)
- `run()` → calls `self.chat_client.get_response()`
- `run_stream()` → calls `self.chat_client.get_streaming_response()`
These are parallel methods on the agent, so consolidating the client methods would **not break** the agent API. You could keep `agent.run()` and `agent.run_stream()` unchanged while internally calling `get_response(stream=True/False)`.
#### 2. **Function Invocation Decorator** (_tools.py)
This is **the most impacted area**. Currently:
- `_handle_function_calls_response()` decorates `get_response`
- `_handle_function_calls_streaming_response()` decorates `get_streaming_response`
- The `use_function_invocation` class decorator wraps **both methods separately**
**Impact**: The decorator logic is almost identical (~200 lines each) with small differences:
- Non-streaming collects response, returns it
- Streaming yields updates, returns async iterable
With a unified method, you'd need **one decorator** that:
- Checks the `stream` parameter
- Uses `@overload` to determine return type
- Handles both paths with conditional logic
- The new decorator could be applied just on the method, instead of the whole class.
This would **reduce code duplication** but add complexity to a single function.
#### 3. **Observability/Instrumentation** (observability.py)
Same pattern as function invocation:
- `_trace_get_response()` wraps `get_response`
- `_trace_get_streaming_response()` wraps `get_streaming_response`
- `use_instrumentation` decorator applies both
**Impact**: Would need consolidation into a single tracing wrapper.
#### 4. **Chat Middleware** (_middleware.py)
The `use_chat_middleware` decorator also wraps both methods separately with similar logic.
#### 5. **AG-UI Client** (_client.py)
Wraps both methods to unwrap server function calls:
```python
original_get_streaming_response = chat_client.get_streaming_response
original_get_response = chat_client.get_response
```
#### 6. **Provider Implementations** (all subpackages)
All subclasses implement both `_inner_*` methods, except:
- OpenAI Assistants Client (and similar clients, such as Foundry Agents V1) - it implements `_inner_get_response` by calling `_inner_get_streaming_response`
### Implications of Consolidation
| Aspect | Impact |
|--------|--------|
| **Type Safety** | Overloads work well: `@overload` with `Literal[True]``AsyncIterable`, `Literal[False]``ChatResponse`. Runtime return type based on `stream` param. |
| **Breaking Change** | **Major breaking change** for anyone implementing custom chat clients. They'd need to update from 2 methods to 1 (or 2 inner methods to 1). |
| **Decorator Complexity** | All 3 decorator systems (function invocation, middleware, observability) would need refactoring to handle both paths in one wrapper. |
| **Code Reduction** | Significant reduction in _tools.py (~200 lines of near-duplicate code) and other decorators. |
| **Samples/Tests** | Many samples call `get_streaming_response()` directly - would need updates. |
| **Protocol Simplification** | `ChatClientProtocol` goes from 2 methods + 1 property to 1 method + 1 property. |
### Recommendation
The consolidation makes sense architecturally, but consider:
1. **The overload pattern with `stream: bool`** works well in Python typing:
```python
@overload
async def get_response(self, messages, *, stream: Literal[True] = True, ...) -> AsyncIterable[ChatResponseUpdate]: ...
@overload
async def get_response(self, messages, *, stream: Literal[False] = False, ...) -> ChatResponse: ...
```
2. **The decorator complexity** is the biggest concern. The current approach of separate decorators for separate methods is cleaner than conditional logic inside one wrapper.
## Decision Drivers
- Reduce code needed to implement a Chat Client, simplify the public API for chat clients
- Reduce code duplication in decorators and middleware
- Maintain type safety and clarity in method signatures
## Considered Options
1. Status quo: Keep separate methods for streaming and non-streaming
2. Consolidate into a single `get_response` method with a `stream` parameter
3. Option 2 plus merging `agent.run` and `agent.run_stream` into a single method with a `stream` parameter as well
## Option 1: Status Quo
- Good: Clear separation of streaming vs non-streaming logic
- Good: Aligned with .NET design, although it is already `run` for Python and `RunAsync` for .NET
- Bad: Code duplication in decorators and middleware
- Bad: More complex client implementations
## Option 2: Consolidate into Single Method
- Good: Simplified public API for chat clients
- Good: Reduced code duplication in decorators
- Good: Smaller API footprint for users to get familiar with
- Good: People using OpenAI directly already expect this pattern
- Bad: Increased complexity in decorators and middleware
- Bad: Less alignment with .NET design (`get_response(stream=True)` vs `GetStreamingResponseAsync`)
## Option 3: Consolidate + Merge Agent and Workflow Methods
- Good: Further simplifies agent and workflow implementation
- Good: Single method for all chat interactions
- Good: Smaller API footprint for users to get familiar with
- Good: People using OpenAI directly already expect this pattern
- Good: Workflows internally already use a single method (_run_workflow_with_tracing), so would eliminate public API duplication as well, with hardly any code changes
- Bad: More breaking changes for agent users
- Bad: Increased complexity in agent implementation
- Bad: More extensive misalignment with .NET design (`run(stream=True)` vs `RunStreamingAsync` in addition to `get_response` change)
## Misc
Smaller questions to consider:
- Should default be `stream=False` or `stream=True`? (Current is False)
- Default to `False` makes it simpler for new users, as non-streaming is easier to handle.
- Default to `False` aligns with existing behavior.
- Streaming tends to be faster, so defaulting to `True` could improve performance for common use cases.
- Should this differ between ChatClient, Agent and Workflows? (e.g., Agent and Workflow defaults to streaming, ChatClient to non-streaming)
## Decision Outcome
Chosen Option: **Option 3: Consolidate + Merge Agent and Workflow Methods**
Since this is the most pythonic option and it reduces the API surface and code duplication the most, we will go with this option.
We will keep the default of `stream=False` for all methods to maintain backward compatibility and simplicity for new users.
# Appendix
## Code Samples for Consolidated Method
### Python - Option 3: Direct ChatClient + Agent with Single Method
```python
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import ChatAgent
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def main() -> None:
# Example 1: Direct ChatClient usage with single method
client = OpenAIChatClient()
message = "What's the weather in Amsterdam and in Paris?"
# Non-streaming usage
print(f"User: {message}")
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response.text}")
# Streaming usage - same method, different parameter
print(f"\nUser: {message}")
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if chunk.text:
print(chunk.text, end="")
print("")
# Example 2: Agent usage with single method
agent = ChatAgent(
chat_client=client,
tools=get_weather,
name="WeatherAgent",
instructions="You are a weather assistant.",
)
thread = agent.get_new_thread()
# Non-streaming agent
print(f"\nUser: {message}")
result = await agent.run(message, thread=thread) # default would be stream=False
print(f"{agent.name}: {result.text}")
# Streaming agent - same method, different parameter
print(f"\nUser: {message}")
print(f"{agent.name}: ", end="")
async for update in agent.run(message, thread=thread, stream=True):
if update.text:
print(update.text, end="")
print("")
if __name__ == "__main__":
asyncio.run(main())
```
### .NET - Current pattern for comparison
```csharp
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(
instructions: "You are good at telling jokes about pirates.",
name: "PirateJoker");
// Non-streaming: Returns a string directly
Console.WriteLine("=== Non-streaming ===");
string result = await agent.RunAsync("Tell me a joke about a pirate.");
Console.WriteLine(result);
// Streaming: Returns IAsyncEnumerable<AgentUpdate>
Console.WriteLine("\n=== Streaming ===");
await foreach (AgentUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
{
Console.Write(update);
}
Console.WriteLine();
```
@@ -0,0 +1,147 @@
# Time-To-Live (TTL) for durable agent sessions
## Overview
The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity.
## What is TTL?
Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime.
## Benefits
- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions
- **Cost optimization**: Reduces storage costs by automatically removing unused session state
- **Resource management**: Prevents unbounded growth of agent session state in storage
- **Configurable**: Set TTL globally or per-agent type to match your application's needs
## Configuration
TTL can be configured at two levels:
1. **Global default TTL**: Applies to all agent sessions unless overridden
2. **Per-agent type TTL**: Overrides the global default for specific agent types
Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes.
> [!NOTE]
> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution.
### Default values
- **Default TTL**: 14 days
- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases)
### Configuration examples
#### .NET
```csharp
// Configure global default TTL and minimum signal delay
services.ConfigureDurableAgents(
options =>
{
// Set global default TTL to 7 days
options.DefaultTimeToLive = TimeSpan.FromDays(7);
// Add agents (will use global default TTL)
options.AddAIAgent(myAgent);
});
// Configure per-agent TTL
services.ConfigureDurableAgents(
options =>
{
options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default
// Agent with custom TTL of 1 day
options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));
// Agent with custom TTL of 90 days
options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));
// Agent using global default (14 days)
options.AddAIAgent(defaultAgent);
});
// Disable TTL for specific agents by setting TTL to null
services.ConfigureDurableAgents(
options =>
{
options.DefaultTimeToLive = TimeSpan.FromDays(14);
// Agent with no TTL (never expires)
options.AddAIAgent(permanentAgent, timeToLive: null);
});
```
## How TTL works
The following sections describe how TTL works in detail.
### Expiration tracking
Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message:
1. When a message is sent to an agent session, the expiration time is set to `current time + TTL`
2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints)
3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time.
### State deletion
When an agent session expires, its entire state is deleted, including:
- Conversation history
- Any custom state data
- Expiration timestamps
After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history.
## Behavior examples
The following examples illustrate how TTL works in different scenarios.
### Example 1: Agent session expires after TTL
1. Agent configured with 30-day TTL
2. User sends message at Day 0 → agent session created, expiration set to Day 30
3. No further messages sent
4. At Day 30 → Agent session is deleted
5. User sends message at Day 31 → New agent session created with fresh conversation history
### Example 2: TTL reset on interaction
1. Agent configured with 30-day TTL
2. User sends message at Day 0 → agent session created, expiration set to Day 30
3. User sends message at Day 15 → Expiration reset to Day 45
4. User sends message at Day 40 → Expiration reset to Day 70
5. Agent session remains active as long as there are regular interactions
## Logging
The TTL feature includes comprehensive logging to track state changes:
- **Expiration time updated**: Logged when TTL expiration time is set or updated
- **Deletion scheduled**: Logged when a deletion check signal is scheduled
- **Deletion check**: Logged when a deletion check operation runs
- **Session expired**: Logged when an agent session is deleted due to expiration
- **TTL rescheduled**: Logged when a deletion signal is rescheduled
These logs help monitor TTL behavior and troubleshoot any issues.
## Best practices
1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state.
2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes.
3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed.
4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods.
## Limitations
- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp.
- Deletion checks are durably scheduled operations and may have slight delays depending on system load.
- Once an agent session is deleted, its conversation history cannot be recovered.
- TTL deletion requires at least one worker to be available to process the deletion operation message.
+5 -5
View File
@@ -125,7 +125,7 @@ The proposed solution is to add helper methods which allow developers to either
- [Foundry SDK] Create a `PersistentAgentsClient`
- [Foundry SDK] Create a `PersistentAgent` using the `PersistentAgentsClient`
- [Foundry SDK] Retrieve an `AIAgent` using the `PersistentAgentsClient`
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentRunResponse`
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
- [Foundry SDK] Clean up the agent
@@ -156,7 +156,7 @@ await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
- [Foundry SDK] Create a `PersistentAgentsClient`
- [Foundry SDK] Create a `AIAgent` using the `PersistentAgentsClient`
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentRunResponse`
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
- [Foundry SDK] Clean up the agent
```csharp
@@ -184,7 +184,7 @@ await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
- [Foundry SDK] Create a `PersistentAgentsClient`
- [Foundry SDK] Create a `AIAgent` using the `PersistentAgentsClient`
- [Agent Framework SDK] Optionally create an `AgentThread` for the agent run
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentRunResponse`
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
- [Foundry SDK] Clean up the agent and the agent thread
```csharp
@@ -227,7 +227,7 @@ await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
- [Foundry SDK] Create a `PersistentAgentsClient`
- [Foundry SDK] Create multiple `AIAgent` instances using the `PersistentAgentsClient`
- [Agent Framework SDK] Create a `SequentialOrchestration` and add all of the agents to it
- [Agent Framework SDK] Invoke the `SequentialOrchestration` instance and access response from the `AgentRunResponse`
- [Agent Framework SDK] Invoke the `SequentialOrchestration` instance and access response from the `AgentResponse`
- [Foundry SDK] Clean up the agents
```csharp
@@ -281,7 +281,7 @@ SequentialOrchestration orchestration =
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
AgentResponse result = await orchestration.RunAsync(input);
Console.WriteLine($"\n# RESULT: {result}");
// Cleanup
+1
View File
@@ -209,6 +209,7 @@ dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on ob
dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates
dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter
dotnet_diagnostic.CA2249.severity = suggestion # Consider using 'Contains' method instead of 'IndexOf' method
dotnet_diagnostic.CA2252.severity = none # Requires preview
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload
+26 -26
View File
@@ -11,40 +11,40 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="11.0.0" />
<PackageVersion Include="Anthropic" Version="12.0.1" />
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.6.0" />
<PackageVersion Include="Google.GenAI" Version="0.9.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.1" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.1" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
@@ -61,10 +61,9 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
@@ -72,11 +71,11 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
@@ -101,11 +100,10 @@
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
<!-- Inference SDKs -->
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.7" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.7.0" />
<PackageVersion Include="OpenAI" Version="2.8.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<!-- Workflows -->
@@ -114,19 +112,21 @@
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
<!-- Durable Task -->
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.16.2" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.16.2-preview.1" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.16.2" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.16.2-preview.1" />
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
<!-- Azure Functions -->
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.9.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Mcp" Version="1.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
<!-- Redis -->
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.22" />
+3 -6
View File
@@ -20,14 +20,9 @@
</Folder>
<Folder Name="/Samples/AGUIClientServer/">
<Project Path="samples/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
</Folder>
<Folder Name="/Samples/AGUIDojo/">
<File Path="samples/AGUIDojo/Design.md" />
<File Path="samples/AGUIDojo/Plan.md" />
<Project Path="samples/AGUIDojo/AGUIDojoClient/AGUIDojoClient.csproj" />
<Project Path="samples/AGUIDojo/AGUIDojoServer/AGUIDojoServer.csproj" />
</Folder>
<Folder Name="/Samples/AzureFunctions/">
<File Path="samples/AzureFunctions/.editorconfig" />
<File Path="samples/AzureFunctions/README.md" />
@@ -38,6 +33,7 @@
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -134,6 +130,7 @@
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
</Folder>
<Folder Name="/Samples/Purview/" />
<Folder Name="/Samples/Purview/AgentWithPurview/">
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251204.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251204.1</PackageVersion>
<GitTag>1.0.0-preview.251204.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260108.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260108.1</PackageVersion>
<GitTag>1.0.0-preview.260108.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -42,7 +42,7 @@ public static class Program
// Create the Host agent
var hostAgent = new HostClientAgent(loggerFactory);
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
AgentThread thread = hostAgent.Agent!.GetNewThread();
AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
try
{
while (true)
@@ -88,7 +88,7 @@ public static class Program
description: "AG-UI Client Agent",
tools: [changeBackground, readClientClimateSensors]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
try
{
@@ -114,7 +114,7 @@ public static class Program
bool isFirstUpdate = true;
string? threadId = null;
var updates = new List<ChatResponseUpdate>();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
{
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
@@ -2,8 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks></TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
@@ -19,12 +19,12 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -69,7 +69,7 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
yield return update;
yield return new AgentRunResponseUpdate(
yield return new AgentResponseUpdate(
new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
{
MessageId = "delta_" + Guid.NewGuid().ToString("N"),
@@ -135,7 +135,7 @@ internal static class ChatClientAgentFactory
{
Instructions = """
You are a document editor assistant. When asked to write or edit content:
IMPORTANT:
- Use the `write_document` tool with the full document text in Markdown format
- Format the document extensively so it's easy to read
@@ -145,7 +145,7 @@ internal static class ChatClientAgentFactory
- When making edits to the document, try to make them minimal - do not change every word
- Keep stories SHORT!
- After you are done writing the document you MUST call a confirm_changes tool after you call write_document
After the user confirms the changes, provide a brief summary of what you wrote.
""",
Tools = [
@@ -20,12 +20,12 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -79,7 +79,7 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
stateUpdate,
this._jsonSerializerOptions.GetTypeInfo(typeof(DocumentState)));
yield return new AgentRunResponseUpdate(
yield return new AgentResponseUpdate(
new ChatResponseUpdate(role: ChatRole.Assistant, [new DataContent(stateBytes, "application/json")])
{
MessageId = "snapshot" + Guid.NewGuid().ToString("N"),
@@ -19,12 +19,12 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -63,7 +63,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
var firstRunMessages = messages.Append(stateUpdateMessage);
var allUpdates = new List<AgentRunResponseUpdate>();
var allUpdates = new List<AgentResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
@@ -76,14 +76,14 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
}
}
var response = allUpdates.ToAgentRunResponse();
var response = allUpdates.ToAgentResponse();
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
{
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentRunResponseUpdate
yield return new AgentResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
+6 -6
View File
@@ -151,9 +151,9 @@ AIAgent agent = chatClient.CreateAIAgent(
tools: []);
bool isFirstUpdate = true;
AgentRunResponseUpdate? currentUpdate = null;
AgentResponseUpdate? currentUpdate = null;
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
// First update indicates run started
if (isFirstUpdate)
@@ -190,19 +190,19 @@ if (currentUpdate != null)
The `RunStreamingAsync` method:
1. Sends messages to the server via HTTP POST
2. Receives server-sent events (SSE) stream
3. Parses events into `AgentRunResponseUpdate` objects
3. Parses events into `AgentResponseUpdate` objects
4. Yields updates as they arrive for real-time display
## Key Concepts
- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
- **AgentRunResponseUpdate**: Contains the response data with:
- **AgentResponseUpdate**: Contains the response data with:
- `ResponseId`: The unique run identifier
- `ConversationId`: The thread/conversation identifier
- `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
- **Run Lifecycle**:
- The **first** `AgentRunResponseUpdate` in a run indicates the run has started
- The **first** `AgentResponseUpdate` in a run indicates the run has started
- Subsequent updates contain streaming content as the agent processes
- The **last** `AgentRunResponseUpdate` in a run indicates the run has finished
- The **last** `AgentResponseUpdate` in a run indicates the run has finished
- If an error occurs, the update will contain `ErrorContent`
-1
View File
@@ -1 +0,0 @@
.playwright-mcp/
-11
View File
@@ -1,11 +0,0 @@
<Solution>
<Project Path="../../src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="../../src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="../../src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="../../src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="../../src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="../../src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
<Project Path="../../src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
<Project Path="AGUIDojoClient/AGUIDojoClient.csproj" />
<Project Path="AGUIDojoServer/AGUIDojoServer.csproj" />
</Solution>
@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks></TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
</ItemGroup>
</Project>
@@ -1,141 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.AI;
[CascadingTypeParameter(nameof(TState))]
public partial class AgentStateBoundary<TState> : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private RenderFragment? _renderWithState;
private protected AgentBoundaryContext<TState>? _context;
private AgentThread? _currentThread;
private AIAgent? _currentAgent;
private bool _disposed;
[EditorRequired][Parameter] public AIAgent? Agent { get; set; }
[Parameter] public AgentThread? Thread { get; set; }
[Parameter] public TState? State { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Callback invoked when the boundary context is created or recreated.
/// Use this to register tools or perform other initialization.
/// </summary>
[Parameter] public EventCallback<IAgentBoundaryContext> OnContextCreated { get; set; }
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
this._renderWithState = this.RenderWithState;
}
// Agent boundary renders once when it receives the initial set of parameters
// and only ever renders again if the Agent, Thread, or State changes.
// Re-rendering the agent boundary will dispose the existing context and create
// a new one.
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (this.Agent == null)
{
throw new InvalidOperationException("AgentBoundary requires an Agent parameter.");
}
// Use Thread parameter if provided, otherwise keep existing thread or create new one
var thread = this.Thread ?? this._currentThread ?? this.Agent.GetNewThread();
TState? currentState = this._context != null ? this._context.CurrentState : default;
bool agentChanged = this.Agent != this._currentAgent;
bool threadChanged = thread != this._currentThread;
bool stateChanged = !EqualityComparer<TState?>.Default.Equals(this.State, currentState);
bool refresh = agentChanged || threadChanged || stateChanged;
if (refresh)
{
this._context?.Dispose();
this._context = null;
this._currentThread = null;
}
this._currentAgent = this.Agent;
this._currentThread = thread;
// Agent is validated non-null above, thread is either from Thread parameter or GetNewThread()
bool isNewContext = this._context == null;
this._context ??= new AgentBoundaryContext<TState>(this.Agent!, thread!);
this._context.CurrentState = this.State;
// Invoke the context created callback if we created a new context
if (isNewContext && this.OnContextCreated.HasDelegate)
{
_ = this.OnContextCreated.InvokeAsync(this._context);
}
if (refresh)
{
this.Render();
}
return Task.CompletedTask;
}
private void Render()
{
this._renderHandle.Render(builder =>
{
builder.OpenComponent<CascadingValue<AgentBoundaryContext<TState>>>(0);
builder.AddComponentParameter(1, "Value", this._context);
builder.AddComponentParameter(2, "IsFixed", true);
builder.AddComponentParameter(3, "ChildContent", this._renderWithState);
builder.CloseComponent();
});
}
protected virtual void RenderWithState(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<TState?>>(0);
builder.AddComponentParameter(1, "Value", this.State);
builder.AddComponentParameter(2, "IsFixed", false);
builder.AddComponentParameter(3, "ChildContent", this.ChildContent);
builder.CloseComponent();
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
this._context?.Dispose();
}
this._disposed = true;
}
}
}
public class AgentBoundary : AgentStateBoundary<object?>
{
protected override void RenderWithState(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<IAgentBoundaryContext>>(0);
builder.AddComponentParameter(1, "Value", this._context);
builder.AddComponentParameter(2, "IsFixed", true);
builder.AddComponentParameter(3, "ChildContent", this.ChildContent);
builder.CloseComponent();
}
}
@@ -1,411 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public sealed partial class AgentBoundaryContext<TState> : IAgentBoundaryContext, IDisposable
{
private readonly AIAgent _agent;
private readonly AgentThread _thread;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly List<ChatMessage> _messages = [];
private readonly List<ChatMessage> _pendingMessages = [];
private readonly List<Action> _messageChangeSubscribers = [];
private readonly List<Action> _responseUpdateSubscribers = [];
private readonly List<Action> _runStatusSubscribers = [];
private readonly List<AITool> _tools = [];
private readonly Dictionary<string, TaskCompletionSource<object>> _pendingResponses = [];
public CancellationToken CancellationToken => this._cancellationTokenSource.Token;
public IReadOnlyList<ChatMessage> CompletedMessages => this._messages.AsReadOnly();
public TState? CurrentState { get; set; }
public IReadOnlyList<ChatMessage> PendingMessages => this._pendingMessages.AsReadOnly();
public ChatMessage? CurrentMessage { get; private set; }
public ChatResponseUpdate? CurrentUpdate { get; private set; }
/// <summary>
/// Gets whether the agent is currently processing a turn.
/// </summary>
public bool IsProcessing { get; private set; }
public AgentBoundaryContext(AIAgent agent, AgentThread thread)
{
this._agent = agent;
this._thread = thread;
this._cancellationTokenSource = new CancellationTokenSource();
}
/// <summary>
/// Registers a client-side tool that will be passed to the agent during invocation.
/// These tools are sent to the server via ChatClientAgentRunOptions.ChatOptions.Tools.
/// </summary>
public void RegisterTool(AITool tool)
{
ArgumentNullException.ThrowIfNull(tool);
this._tools.Add(tool);
}
/// <summary>
/// Registers multiple client-side tools that will be passed to the agent during invocation.
/// </summary>
public void RegisterTools(params AITool[] tools)
{
foreach (var tool in tools)
{
this.RegisterTool(tool);
}
}
/// <summary>
/// Creates a pending response for the given key and returns a task that completes when ProvideResponse is called.
/// Used by frontend tools to wait for user input from UI components.
/// </summary>
/// <param name="key">A unique key to identify this pending response (e.g., function call ID).</param>
/// <returns>A task that completes with the response object when ProvideResponse is called.</returns>
public Task<object> WaitForResponse(string key)
{
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
this._pendingResponses[key] = tcs;
return tcs.Task;
}
/// <summary>
/// Provides a response for a pending request, completing the task returned by WaitForResponse.
/// Called by UI components when user interaction is complete.
/// </summary>
/// <param name="key">The key used when calling WaitForResponse.</param>
/// <param name="response">The response object to return.</param>
/// <returns>True if the response was provided successfully, false if no pending request was found.</returns>
public bool ProvideResponse(string key, object response)
{
if (this._pendingResponses.TryGetValue(key, out var tcs))
{
this._pendingResponses.Remove(key);
tcs.TrySetResult(response);
return true;
}
return false;
}
public void Dispose()
{
this._cancellationTokenSource.Cancel();
this._cancellationTokenSource.Dispose();
GC.SuppressFinalize(this);
}
public async Task SendAsync(params ChatMessage[] userMessages)
{
// This starts a new turn. Once completed, we will add the new messages to the conversation.
this._pendingMessages.Clear();
this._pendingMessages.AddRange(userMessages);
// Mark as processing
this.IsProcessing = true;
// Notify subscribers that run status changed (processing started)
this.TriggerRunStatusChanged();
// User messages added, notify subscribers.
this.TriggerMessageChanges();
// Build run options with registered tools if any
AgentRunOptions? options = null;
if (this._tools.Count > 0)
{
options = new ChatClientAgentRunOptions(new ChatOptions
{
Tools = [.. this._tools]
});
}
// Start a turn. Collect all updates as we stream them.
await foreach (var update in this._agent.RunStreamingAsync(
userMessages,
this._thread,
options,
this._cancellationTokenSource.Token))
{
var chatUpdate = update.AsChatResponseUpdate();
this.CurrentUpdate = chatUpdate;
// Notify subscribers of the new update, this always happens before a new message is created/updated.
this.TriggerChatResponseUpdate();
// This creates or adds a new message to the pending messages as needed.
var isNewMessage = MessageHelpers.ProcessUpdate(chatUpdate, this._pendingMessages);
if (isNewMessage)
{
this.CurrentMessage = this._pendingMessages[this._pendingMessages.Count - 1];
// Finalize the previous message's content if we have 2 or more messages now.
if (this._pendingMessages.Count > userMessages.Length + 1)
{
MessageHelpers.CoalesceContent(this._pendingMessages[this._pendingMessages.Count - 2].Contents);
}
// Notify subscribers of new message
this.TriggerMessageChanges();
}
}
// Process any remaining updates to finalize the last message.
if (this._pendingMessages.Count > userMessages.Length)
{
MessageHelpers.CoalesceContent(this._pendingMessages[this._pendingMessages.Count - 1].Contents);
}
// Add the new messages to the conversation
this._messages.AddRange(this._pendingMessages);
// Finish the turn
this._pendingMessages.Clear();
this.CurrentMessage = null;
this.CurrentUpdate = null;
this.IsProcessing = false;
// Notify subscribers that run status changed (processing ended)
this.TriggerRunStatusChanged();
// Notify subscribers
this.TriggerChatResponseUpdate();
this.TriggerMessageChanges();
}
private void TriggerChatResponseUpdate()
{
// Iterate backwards to avoid issues if subscribers are removed during iteration
for (var i = this._responseUpdateSubscribers.Count - 1; i >= 0; i--)
{
this._responseUpdateSubscribers[i]();
}
}
private void TriggerMessageChanges()
{
// Iterate backwards to avoid issues if subscribers are removed during iteration
for (var i = this._messageChangeSubscribers.Count - 1; i >= 0; i--)
{
this._messageChangeSubscribers[i]();
}
}
public MessageSubscription SubscribeToMessageChanges(Action onNewMessage)
{
return new MessageSubscription(this._messageChangeSubscribers, onNewMessage);
}
public ResponseUpdateSubscription SubscribeToResponseUpdates(Action onChatResponse)
{
return new ResponseUpdateSubscription(this._responseUpdateSubscribers, onChatResponse);
}
public RunStatusSubscription SubscribeToRunStatusChanges(Action onRunStatusChanged)
{
return new RunStatusSubscription(this._runStatusSubscribers, onRunStatusChanged);
}
private void TriggerRunStatusChanged()
{
// Iterate backwards to avoid issues if subscribers are removed during iteration
for (var i = this._runStatusSubscribers.Count - 1; i >= 0; i--)
{
this._runStatusSubscribers[i]();
}
}
}
public interface IAgentBoundaryContext
{
// Push new messages to the agent
Task SendAsync(params ChatMessage[] userMessages);
// All message interactions from previous turns
IReadOnlyList<ChatMessage> CompletedMessages { get; }
// All message interactions from the current turn. These represent completed messages only.
IReadOnlyList<ChatMessage> PendingMessages { get; }
// The current message being processed by the agent.
ChatMessage? CurrentMessage { get; }
ChatResponseUpdate? CurrentUpdate { get; }
/// <summary>
/// Gets whether the agent is currently processing a turn.
/// </summary>
bool IsProcessing { get; }
// Triggered any time there is a change on a message.
MessageSubscription SubscribeToMessageChanges(Action onNewMessage);
ResponseUpdateSubscription SubscribeToResponseUpdates(Action onChatResponse);
/// <summary>
/// Subscribes to run status changes (when processing starts or ends).
/// </summary>
/// <param name="onRunStatusChanged">The callback to invoke when run status changes.</param>
/// <returns>A subscription that can be disposed to unsubscribe.</returns>
RunStatusSubscription SubscribeToRunStatusChanges(Action onRunStatusChanged);
CancellationToken CancellationToken { get; }
/// <summary>
/// Registers a client-side tool that will be passed to the agent during invocation.
/// </summary>
void RegisterTool(AITool tool);
/// <summary>
/// Registers multiple client-side tools that will be passed to the agent during invocation.
/// </summary>
void RegisterTools(params AITool[] tools);
/// <summary>
/// Creates a pending response for the given key and returns a task that completes when ProvideResponse is called.
/// Used by frontend tools to wait for user input from UI components.
/// </summary>
/// <param name="key">A unique key to identify this pending response (e.g., function call ID).</param>
/// <returns>A task that completes with the response object when ProvideResponse is called.</returns>
Task<object> WaitForResponse(string key);
/// <summary>
/// Provides a response for a pending request, completing the task returned by WaitForResponse.
/// Called by UI components when user interaction is complete.
/// </summary>
/// <param name="key">The key used when calling WaitForResponse.</param>
/// <param name="response">The response object to return.</param>
/// <returns>True if the response was provided successfully, false if no pending request was found.</returns>
bool ProvideResponse(string key, object response);
}
public readonly struct MessageSubscription : IDisposable, IEquatable<MessageSubscription>
{
internal readonly List<Action> _subscribers;
internal readonly Action _subscription;
internal MessageSubscription(List<Action> subscribers, Action subscription)
{
this._subscribers = subscribers;
this._subscription = subscription;
this._subscribers.Add(this._subscription);
}
public void Dispose() => this._subscribers.Remove(this._subscription);
public override bool Equals(object? obj)
{
return obj is MessageSubscription subscription && this.Equals(subscription);
}
public bool Equals(MessageSubscription other)
{
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
}
public override int GetHashCode()
{
return HashCode.Combine(this._subscribers, this._subscription);
}
public static bool operator ==(MessageSubscription left, MessageSubscription right)
{
return left.Equals(right);
}
public static bool operator !=(MessageSubscription left, MessageSubscription right)
{
return !(left == right);
}
}
public readonly struct ResponseUpdateSubscription : IDisposable, IEquatable<ResponseUpdateSubscription>
{
private readonly List<Action> _subscribers;
private readonly Action _subscription;
public ResponseUpdateSubscription(List<Action> subscribers, Action subscription)
{
this._subscribers = subscribers;
this._subscription = subscription;
this._subscribers.Add(this._subscription);
}
public void Dispose() => this._subscribers.Remove(this._subscription);
public override bool Equals(object? obj)
{
return obj is ResponseUpdateSubscription subscription && this.Equals(subscription);
}
public bool Equals(ResponseUpdateSubscription other)
{
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
}
public override int GetHashCode()
{
return HashCode.Combine(this._subscribers, this._subscription);
}
public static bool operator ==(ResponseUpdateSubscription left, ResponseUpdateSubscription right)
{
return left.Equals(right);
}
public static bool operator !=(ResponseUpdateSubscription left, ResponseUpdateSubscription right)
{
return !(left == right);
}
}
public readonly struct RunStatusSubscription : IDisposable, IEquatable<RunStatusSubscription>
{
private readonly List<Action> _subscribers;
private readonly Action _subscription;
public RunStatusSubscription(List<Action> subscribers, Action subscription)
{
this._subscribers = subscribers;
this._subscription = subscription;
this._subscribers.Add(this._subscription);
}
public void Dispose() => this._subscribers.Remove(this._subscription);
public override bool Equals(object? obj)
{
return obj is RunStatusSubscription subscription && this.Equals(subscription);
}
public bool Equals(RunStatusSubscription other)
{
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
}
public override int GetHashCode()
{
return HashCode.Combine(this._subscribers, this._subscription);
}
public static bool operator ==(RunStatusSubscription left, RunStatusSubscription right)
{
return left.Equals(right);
}
public static bool operator !=(RunStatusSubscription left, RunStatusSubscription right)
{
return !(left == right);
}
}
@@ -1,94 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public sealed partial class AgentInput : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private AgentBoundaryContext<object?>? _context;
private string? _inputText;
private RunStatusSubscription? _subscription;
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
[Parameter] public string Placeholder { get; set; } = "Type a message...";
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Unsubscribe from previous context if it changed
if (this._context != this.AgentContext)
{
this._subscription?.Dispose();
this._context = this.AgentContext;
// Subscribe to run status changes
if (this._context != null)
{
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
}
}
this.Render();
return Task.CompletedTask;
}
private void OnRunStatusChanged()
{
this.Render();
}
private void Render()
{
var isProcessing = this._context?.IsProcessing ?? false;
var isDisabled = string.IsNullOrWhiteSpace(this._inputText) || isProcessing;
this._renderHandle.Render(builder =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "agent-input");
builder.OpenElement(2, "textarea");
builder.AddAttribute(3, "value", this._inputText);
builder.AddAttribute(4, "oninput", EventCallback.Factory.Create<ChangeEventArgs>(this, e => { this._inputText = e.Value?.ToString(); this.Render(); }));
builder.AddAttribute(5, "placeholder", this.Placeholder);
builder.AddAttribute(6, "rows", "1");
builder.AddAttribute(7, "disabled", isProcessing);
builder.CloseElement();
builder.OpenElement(8, "button");
builder.AddAttribute(9, "class", "send-button");
builder.AddAttribute(10, "onclick", EventCallback.Factory.Create(this, this.SendAsync));
builder.AddAttribute(11, "disabled", isDisabled);
builder.AddMarkupContent(12, """<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>""");
builder.CloseElement(); // close button
builder.CloseElement(); // close div
});
}
private async Task SendAsync()
{
if (!string.IsNullOrWhiteSpace(this._inputText) && this._context is { IsProcessing: false })
{
var text = this._inputText;
this._inputText = ""; // Clear input immediately
this.Render(); // Re-render to clear input
await this._context.SendAsync(new ChatMessage(ChatRole.User, text));
}
}
public void Dispose()
{
this._subscription?.Dispose();
}
}
@@ -1,72 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// A component that displays a loading indicator (three animated dots) when the agent is processing.
/// </summary>
public sealed partial class AgentLoadingIndicator : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private AgentBoundaryContext<object?>? _context;
private RunStatusSubscription? _subscription;
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Unsubscribe from previous context if it changed
if (this._context != this.AgentContext)
{
this._subscription?.Dispose();
this._context = this.AgentContext;
// Subscribe to run status changes
if (this._context != null)
{
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
}
}
this.Render();
return Task.CompletedTask;
}
private void OnRunStatusChanged()
{
this.Render();
}
private void Render()
{
var isProcessing = this._context?.IsProcessing ?? false;
this._renderHandle.Render(builder =>
{
if (!isProcessing)
{
return;
}
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "agent-loading-indicator");
builder.OpenElement(2, "div");
builder.AddAttribute(3, "class", "agent-loading-dots");
builder.AddMarkupContent(4, "<span></span><span></span><span></span>");
builder.CloseElement();
builder.CloseElement();
});
}
public void Dispose()
{
this._subscription?.Dispose();
}
}
@@ -1,93 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@namespace Microsoft.AspNetCore.Components.AI
@using Microsoft.Extensions.AI
@typeparam TState
@implements IDisposable
<CascadingValue Value="@CurrentState" IsFixed="false">
@ChildContent
</CascadingValue>
@code {
private ResponseUpdateSubscription? _subscription;
/// <summary>
/// The agent boundary context to subscribe to for state updates.
/// </summary>
[CascadingParameter]
public IAgentBoundaryContext? BoundaryContext { get; set; }
/// <summary>
/// The current state value. Can be set initially and will be updated by state events.
/// </summary>
[Parameter]
public TState? CurrentState { get; set; }
/// <summary>
/// Callback to deserialize a STATE_SNAPSHOT (application/json) into TState.
/// </summary>
[Parameter]
public Func<ReadOnlyMemory<byte>, TState?>? OnSnapshot { get; set; }
/// <summary>
/// Callback to apply a STATE_DELTA (application/json-patch+json) to the current state.
/// Returns the updated state.
/// </summary>
[Parameter]
public Func<TState?, ReadOnlyMemory<byte>, TState?>? OnDelta { get; set; }
/// <summary>
/// Optional callback invoked whenever state changes.
/// </summary>
[Parameter]
public EventCallback<TState?> CurrentStateChanged { get; set; }
/// <summary>
/// Child content that will receive the cascaded state.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
if (BoundaryContext is not null)
{
_subscription = BoundaryContext.SubscribeToResponseUpdates(OnResponseUpdate);
}
}
private void OnResponseUpdate()
{
var update = BoundaryContext?.CurrentUpdate;
if (update?.Contents is null)
{
return;
}
foreach (var content in update.Contents)
{
if (content is DataContent dataContent)
{
if (string.Equals(dataContent.MediaType, "application/json", StringComparison.OrdinalIgnoreCase) && OnSnapshot is not null)
{
// STATE_SNAPSHOT - let app deserialize
CurrentState = OnSnapshot(dataContent.Data);
_ = CurrentStateChanged.InvokeAsync(CurrentState);
InvokeAsync(StateHasChanged);
}
else if (string.Equals(dataContent.MediaType, "application/json-patch+json", StringComparison.OrdinalIgnoreCase) && OnDelta is not null)
{
// STATE_DELTA - let app apply the patch
CurrentState = OnDelta(CurrentState, dataContent.Data);
_ = CurrentStateChanged.InvokeAsync(CurrentState);
InvokeAsync(StateHasChanged);
}
}
}
}
public void Dispose()
{
_subscription?.Dispose();
}
}
@@ -1,148 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Represents a suggestion that can be displayed and sent to the agent.
/// </summary>
public readonly struct Suggestion : IEquatable<Suggestion>
{
/// <summary>
/// Initializes a new instance of the <see cref="Suggestion"/> struct.
/// </summary>
/// <param name="text">The display text for the suggestion.</param>
/// <param name="message">The message to send when the suggestion is selected.</param>
public Suggestion(string text, ChatMessage message)
{
this.Text = text;
this.Message = message;
}
/// <summary>
/// Initializes a new instance of the <see cref="Suggestion"/> struct with a simple text message.
/// </summary>
/// <param name="text">The display text for the suggestion, also used as the message content.</param>
public Suggestion(string text)
{
this.Text = text;
this.Message = new ChatMessage(ChatRole.User, text);
}
/// <summary>
/// Gets the display text for the suggestion.
/// </summary>
public string Text { get; }
/// <summary>
/// Gets the message to send when the suggestion is selected.
/// </summary>
public ChatMessage Message { get; }
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Suggestion other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(Suggestion other) => this.Text == other.Text;
/// <inheritdoc/>
public override int GetHashCode() => this.Text?.GetHashCode() ?? 0;
/// <summary>
/// Determines whether two <see cref="Suggestion"/> instances are equal.
/// </summary>
public static bool operator ==(Suggestion left, Suggestion right) => left.Equals(right);
/// <summary>
/// Determines whether two <see cref="Suggestion"/> instances are not equal.
/// </summary>
public static bool operator !=(Suggestion left, Suggestion right) => !left.Equals(right);
}
public sealed partial class AgentSuggestions : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private AgentBoundaryContext<object?>? _context;
private IReadOnlyList<Suggestion>? _suggestions;
private RunStatusSubscription? _subscription;
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
[Parameter] public IReadOnlyList<Suggestion>? Suggestions { get; set; }
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Unsubscribe from previous context if it changed
if (this._context != this.AgentContext)
{
this._subscription?.Dispose();
this._context = this.AgentContext;
// Subscribe to run status changes
if (this._context != null)
{
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
}
}
this._suggestions = this.Suggestions;
this.Render();
return Task.CompletedTask;
}
private void OnRunStatusChanged()
{
this.Render();
}
private void Render()
{
var isProcessing = this._context?.IsProcessing ?? false;
this._renderHandle.Render(builder =>
{
if (this._suggestions is null || this._suggestions.Count == 0)
{
return;
}
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "agent-suggestions");
for (var i = 0; i < this._suggestions.Count; i++)
{
var suggestion = this._suggestions[i];
builder.OpenElement(2, "button");
builder.SetKey(suggestion.Text);
builder.AddAttribute(3, "class", "suggestion-button");
builder.AddAttribute(4, "onclick", EventCallback.Factory.Create(this, () => this.SelectSuggestionAsync(suggestion)));
builder.AddAttribute(5, "disabled", isProcessing);
builder.AddContent(6, suggestion.Text);
builder.CloseElement();
}
builder.CloseElement();
});
}
private async Task SelectSuggestionAsync(Suggestion suggestion)
{
if (this._context is { IsProcessing: false })
{
await this._context.SendAsync(suggestion.Message);
}
}
public void Dispose()
{
this._subscription?.Dispose();
}
}
@@ -1,33 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.AI;
#pragma warning disable CA1812 // Internal class is apparently never instantiated
internal sealed class ContentBlock : IComponent
#pragma warning restore CA1812 // Internal class is apparently never instantiated
{
private RenderHandle _renderHandle;
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Always render when parameters are set - the parent component (MessageList)
// only triggers renders when there are actual updates to show.
this._renderHandle.Render(this.Render);
return Task.CompletedTask;
}
private void Render(RenderTreeBuilder builder)
{
builder.AddContent(0, this.ChildContent);
}
}
@@ -1,10 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public class ContentContext(AIContent content)
{
public AIContent Content { get; init; } = content;
}
@@ -1,14 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.AspNetCore.Components.AI;
public abstract class ContentTemplateBase : IComponent
{
public abstract void Attach(RenderHandle renderHandle);
public abstract Task SetParametersAsync(ParameterView parameters);
public virtual bool When(ContentContext context) => true;
[Parameter] public RenderFragment<ContentContext> ChildContent { get; set; } = (content) => builder => { };
}
@@ -1,36 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Default template for rendering DataContent. Renders nothing by default
/// as DataContent typically contains binary/JSON data not meant for display.
/// </summary>
public class DataContentTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
this.ChildContent = this.RenderData;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
public override bool When(ContentContext context)
{
return context.Content is DataContent;
}
private RenderFragment RenderData(ContentContext content) => builder =>
{
// By default, render nothing.
};
}
@@ -1,43 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Default template for rendering ErrorContent. Renders nothing by default
/// to prevent the error from crashing the UI. More specific templates can
/// override this to display error messages.
/// </summary>
public class ErrorTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
this.ChildContent = this.RenderError;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Determines if this template should handle the given content.
/// Matches ErrorContent.
/// </summary>
public override bool When(ContentContext context)
{
return context.Content is ErrorContent;
}
private RenderFragment RenderError(ContentContext content) => builder =>
{
// By default, render nothing.
// Specific templates can override to display error messages.
};
}
@@ -1,60 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Template for rendering function/tool call content in messages.
/// Provides access to both the call and its result (when available) via InvocationContext.
/// </summary>
public class FunctionCallTemplate : ContentTemplateBase
{
/// <summary>
/// Gets or sets the tool name to filter on. If null, matches all function calls.
/// </summary>
[Parameter] public string? ToolName { get; set; }
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
// This component never renders anything by itself.
this.ChildContent = this.RenderFunctionCall;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Determines if this template should handle the given content.
/// </summary>
/// <param name="context">The content context.</param>
/// <returns>True if this template should render the content.</returns>
public override bool When(ContentContext context)
{
if (context.Content is not FunctionCallContent call)
{
return false;
}
// Filter by tool name if specified
if (this.ToolName != null && !string.Equals(call.Name, this.ToolName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
private RenderFragment RenderFunctionCall(ContentContext content) => builder =>
{
// By default, function calls are not rendered visually.
// Custom templates (like WeatherCallTemplate) can override this
// behavior for specific functions by registering before this template.
};
}
@@ -1,39 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Default template for FunctionResultContent that renders nothing.
/// Function results are internal tool responses and typically don't need
/// visual representation in the chat UI.
/// </summary>
public class FunctionResultTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
// This component never renders anything by itself.
this.ChildContent = this.RenderFunctionResult;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Only match FunctionResultContent.
/// </summary>
public override bool When(ContentContext context) => context.Content is FunctionResultContent;
private RenderFragment RenderFunctionResult(ContentContext content) => builder =>
{
// By default, function results are not rendered visually.
// The result data is typically processed by the agent to generate text responses.
};
}
@@ -1,135 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Context for a function invocation, tracking the call and its result.
/// </summary>
public class InvocationContext
{
private Action? _resultArrived;
/// <summary>
/// Initializes a new instance of the <see cref="InvocationContext"/> class.
/// </summary>
/// <param name="call">The function call content.</param>
public InvocationContext(FunctionCallContent call)
{
this.Call = call;
}
/// <summary>
/// Gets the function call content.
/// </summary>
public FunctionCallContent Call { get; }
/// <summary>
/// Gets the function result content, if available.
/// </summary>
public FunctionResultContent? ResultContent { get; private set; }
/// <summary>
/// Gets a value indicating whether the result has arrived.
/// </summary>
public bool HasResult => this.ResultContent != null;
/// <summary>
/// Gets the function name from the call.
/// </summary>
public string FunctionName => this.Call.Name;
/// <summary>
/// Gets the call ID.
/// </summary>
public string CallId => this.Call.CallId;
/// <summary>
/// Gets the arguments from the call.
/// </summary>
public IDictionary<string, object?>? Arguments => this.Call.Arguments;
/// <summary>
/// Event raised when the result arrives.
/// </summary>
#pragma warning disable CA1003 // Use generic event handler instances
public event Action? ResultArrived
#pragma warning restore CA1003 // Use generic event handler instances
{
add => this._resultArrived += value;
remove => this._resultArrived -= value;
}
/// <summary>
/// Sets the result and raises the ResultArrived event.
/// </summary>
/// <param name="result">The function result content.</param>
internal void SetResult(FunctionResultContent result)
{
this.ResultContent = result;
this._resultArrived?.Invoke();
this._resultArrived = null; // Clear invocation list after firing
}
/// <summary>
/// Gets an argument value by name, deserializing from JSON if necessary.
/// </summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <param name="name">The argument name.</param>
/// <returns>The argument value, or default if not found.</returns>
public T? GetArgument<T>(string name)
{
if (this.Arguments is null || !this.Arguments.TryGetValue(name, out var value))
{
return default;
}
if (value is null)
{
return default;
}
if (value is T typed)
{
return typed;
}
if (value is JsonElement jsonElement)
{
return jsonElement.Deserialize<T>();
}
// Try to convert via JSON serialization
var json = JsonSerializer.Serialize(value);
return JsonSerializer.Deserialize<T>(json);
}
/// <summary>
/// Gets the result as a specific type, deserializing from JSON if necessary.
/// </summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The result value, or default if not available.</returns>
public T? GetResult<T>()
{
if (this.ResultContent?.Result is null)
{
return default;
}
if (this.ResultContent.Result is T typed)
{
return typed;
}
if (this.ResultContent.Result is JsonElement jsonElement)
{
return jsonElement.Deserialize<T>();
}
// Try to convert via JSON serialization
var json = JsonSerializer.Serialize(this.ResultContent.Result);
return JsonSerializer.Deserialize<T>(json);
}
}
@@ -1,37 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public class TextTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
// This component never renders anything by itself.
this.ChildContent = this.RenderText;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Only match TextContent, allowing other content templates to handle
/// FunctionCallContent, FunctionResultContent, etc.
/// </summary>
public override bool When(ContentContext context) => context.Content is TextContent;
private RenderFragment RenderText(ContentContext content) => builder =>
{
if (content.Content is TextContent textContent)
{
builder.AddContent(0, textContent.Text);
}
};
}
@@ -1,116 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
#pragma warning disable CA1812 // Internal class is apparently never instantiated
internal sealed class DefaultMessageTemplate : MessageTemplateBase
#pragma warning restore CA1812 // Internal class is apparently never instantiated
{
public override bool When(MessageContext context) => true;
// Buffer to convert response updates to chat messages.
private readonly List<ChatMessage> _buffer = [];
public DefaultMessageTemplate()
{
this.ChildContent = this.SelectTemplate;
}
private RenderFragment SelectTemplate(MessageContext messageContext)
{
if (messageContext.ChatMessage is not null)
{
var getRenderContents = messageContext.RenderContents();
// Return a render fragment that checks visibility at render time
// This is important because message contents may change during streaming
return CreateRenderMessage(
messageContext.ChatMessage,
getRenderContents);
}
else
{
var getRenderContents = messageContext.RenderContents();
var updates = messageContext.ResponseUpdates ?? [];
if (updates.Count == 0)
{
throw new InvalidOperationException("MessageContext must have either a ChatMessage or at least one ResponseUpdate.");
}
this._buffer.Clear();
this._buffer.AddMessages(updates);
if (this._buffer.Count != 1)
{
throw new InvalidOperationException("DefaultMessageTemplate only supports a single ResponseUpdate.");
}
return CreateRenderMessage(
this._buffer[0],
getRenderContents);
}
}
/// <summary>
/// Checks if a message has any visible content that should be displayed.
/// Messages that only contain FunctionResultContent are internal tool messages
/// and should not be rendered as chat bubbles.
/// FunctionCallContent is considered visible because content templates can render them
/// (e.g., WeatherCallTemplate renders weather tool calls as cards).
/// </summary>
private static bool HasVisibleContent(ChatMessage message)
{
if (message.Contents.Count == 0)
{
return false;
}
foreach (var content in message.Contents)
{
// TextContent is visible
if (content is TextContent textContent && !string.IsNullOrWhiteSpace(textContent.Text))
{
return true;
}
// FunctionCallContent is visible - content templates can render them
// (e.g., WeatherCallTemplate renders weather tool calls as cards)
if (content is FunctionCallContent)
{
return true;
}
// Other content types that are not function results are visible
if (content is not FunctionResultContent)
{
return true;
}
}
return false;
}
private static RenderFragment CreateRenderMessage(
ChatMessage message,
RenderFragment getRenderContents)
{
var roleClass = $"{message.Role}-message";
return builder =>
{
// Check visibility at render time, not at template creation time
// This is important because message contents may change during streaming
if (!HasVisibleContent(message))
{
return; // Don't render messages without visible content
}
builder.OpenElement(0, "div");
if (!string.IsNullOrEmpty(message.MessageId))
{
builder.AddAttribute(1, "id", message.MessageId);
}
builder.AddAttribute(2, "class", $"chat-message {roleClass}");
builder.AddContent(3, getRenderContents);
builder.CloseElement();
};
}
}
@@ -1,96 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public class MessageContext
{
private readonly MessageListContext _messageListContext;
private RenderFragment? _contentsRenderer;
private MessageTemplateBase? _template;
private readonly Dictionary<AIContent, RenderFragment> _contentRenderers = [];
internal MessageContext(ChatMessage message, MessageListContext messageListContext)
{
this.ChatMessage = message;
this._messageListContext = messageListContext;
}
public ChatMessage? ChatMessage { get; init; }
public IList<ChatResponseUpdate>? ResponseUpdates { get; init; }
public RenderFragment RenderContents() => this.GetOrCreateContentsRenderer();
internal void SetTemplate(MessageTemplateBase template)
{
this._template = template;
}
private RenderFragment GetOrCreateContentsRenderer()
{
if (this._contentsRenderer != null)
{
return this._contentsRenderer;
}
if (this._template == null)
{
throw new InvalidOperationException("Message template has not been set for this message context.");
}
this._contentsRenderer = builder =>
{
if (this.ChatMessage != null)
{
for (int i = 0; i < this.ChatMessage.Contents.Count; i++)
{
var content = this.ChatMessage.Contents[i];
builder.AddContent(0, this.ResolveContentRenderer(content));
}
}
else if (this.ResponseUpdates != null)
{
for (int i = 0; i < this.ResponseUpdates.Count; i++)
{
var update = this.ResponseUpdates[i];
for (int j = 0; j < update.Contents.Count; j++)
{
var content = update.Contents[j];
if (this._contentRenderers.TryGetValue(content, out var contentRenderer))
{
builder.AddContent(0, contentRenderer);
}
}
}
}
else
{
throw new InvalidOperationException("MessageContext must have either a ChatMessage or ResponseUpdates to render contents.");
}
};
return this._contentsRenderer;
}
private RenderFragment ResolveContentRenderer(AIContent content)
{
if (this._contentRenderers.TryGetValue(content, out var contentRenderer))
{
return contentRenderer;
}
Debug.Assert(this._template != null);
contentRenderer = this._template.GetContentTemplate(content);
if (contentRenderer != null)
{
this._contentRenderers[content] = contentRenderer;
return contentRenderer;
}
contentRenderer = this._messageListContext.GetContentTemplate(content);
this._contentRenderers[content] = contentRenderer;
return contentRenderer;
}
}
@@ -1,322 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
// Roughly lifted from src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs
internal static class MessageHelpers
{
internal static void CoalesceContent(IList<AIContent> contents)
{
Coalesce<TextContent>(
contents,
mergeSingle: false,
canMerge: null,
static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() });
Coalesce(
contents,
mergeSingle: false,
canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does
static (contents, start, end) =>
{
TextReasoningContent content = new(MergeText(contents, start, end))
{
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
};
#if DEBUG
for (int i = start; i < end - 1; i++)
{
Debug.Assert(contents[i] is TextReasoningContent { ProtectedData: null }, "Expected all but the last to have a null ProtectedData");
}
#endif
if (((TextReasoningContent)contents[end - 1]).ProtectedData is { } protectedData)
{
content.ProtectedData = protectedData;
}
return content;
});
Coalesce(
contents,
mergeSingle: false,
canMerge: static (r1, r2) => r1.MediaType == r2.MediaType && r1.HasTopLevelMediaType("text") && r1.Name == r2.Name,
static (contents, start, end) =>
{
Debug.Assert(end - start > 1, "Expected multiple contents to merge");
MemoryStream ms = new();
for (int i = start; i < end; i++)
{
var current = (DataContent)contents[i];
#if NET
ms.Write(current.Data.Span);
#else
if (!MemoryMarshal.TryGetArray(current.Data, out var segment))
{
segment = new(current.Data.ToArray());
}
ms.Write(segment.Array!, segment.Offset, segment.Count);
#endif
}
var first = (DataContent)contents[start];
return new DataContent(new ReadOnlyMemory<byte>(ms.GetBuffer(), 0, (int)ms.Length), first.MediaType) { Name = first.Name };
});
Coalesce(
contents,
mergeSingle: true,
canMerge: static (r1, r2) => r1.CallId == r2.CallId,
static (contents, start, end) =>
{
var firstContent = (CodeInterpreterToolCallContent)contents[start];
if (start == end - 1)
{
if (firstContent.Inputs is not null)
{
CoalesceContent(firstContent.Inputs);
}
return firstContent;
}
List<AIContent>? inputs = null;
for (int i = start; i < end; i++)
{
(inputs ??= []).AddRange(((CodeInterpreterToolCallContent)contents[i]).Inputs ?? []);
}
if (inputs is not null)
{
CoalesceContent(inputs);
}
return new()
{
CallId = firstContent.CallId,
Inputs = inputs,
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
};
});
Coalesce(
contents,
mergeSingle: true,
canMerge: static (r1, r2) => r1.CallId is not null && r2.CallId is not null && r1.CallId == r2.CallId,
static (contents, start, end) =>
{
var firstContent = (CodeInterpreterToolResultContent)contents[start];
if (start == end - 1)
{
if (firstContent.Outputs is not null)
{
CoalesceContent(firstContent.Outputs);
}
return firstContent;
}
List<AIContent>? output = null;
for (int i = start; i < end; i++)
{
(output ??= []).AddRange(((CodeInterpreterToolResultContent)contents[i]).Outputs ?? []);
}
if (output is not null)
{
CoalesceContent(output);
}
return new()
{
CallId = firstContent.CallId,
Outputs = output,
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
};
});
static string MergeText(IList<AIContent> contents, int start, int end)
{
Debug.Assert(end - start > 1, "Expected multiple contents to merge");
StringBuilder sb = new();
for (int i = start; i < end; i++)
{
_ = sb.Append(contents[i]);
}
return sb.ToString();
}
static void Coalesce<TContent>(
IList<AIContent> contents,
bool mergeSingle,
Func<TContent, TContent, bool>? canMerge,
Func<IList<AIContent>, int, int, TContent> merge)
where TContent : AIContent
{
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
int start = 0;
while (start < contents.Count)
{
if (!TryAsCoalescable(contents[start], out var firstContent))
{
start++;
continue;
}
// Iterate until we find a non-coalescable item.
int i = start + 1;
TContent prev = firstContent;
while (i < contents.Count && TryAsCoalescable(contents[i], out TContent? next) && (canMerge is null || canMerge(prev, next)))
{
i++;
prev = next;
}
// If there's only one item in the run, and we don't want to merge single items, skip it.
if (start == i - 1 && !mergeSingle)
{
start++;
continue;
}
// Store the replacement node and null out all of the nodes that we coalesced.
// We can then remove all coalesced nodes in one O(N) operation via RemoveAll.
// Leave start positioned at the start of the next run.
contents[start] = merge(contents, start, i);
start++;
while (start < i)
{
contents[start++] = null!;
}
static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent? coalescable)
{
if (content is TContent tmp && tmp.Annotations is not { Count: > 0 })
{
coalescable = tmp;
return true;
}
coalescable = null;
return false;
}
}
// Remove all of the null slots left over from the coalescing process.
RemoveNullContents(contents);
}
}
private static void RemoveNullContents<T>(IList<T> contents)
where T : class
{
if (contents is List<AIContent> contentsList)
{
_ = contentsList.RemoveAll(u => u is null);
}
else
{
int nextSlot = 0;
int contentsCount = contents.Count;
for (int i = 0; i < contentsCount; i++)
{
if (contents[i] is { } content)
{
contents[nextSlot++] = content;
}
}
for (int i = contentsCount - 1; i >= nextSlot; i--)
{
contents.RemoveAt(i);
}
Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length.");
}
}
internal static bool ProcessUpdate(ChatResponseUpdate update, List<ChatMessage> messages)
{
// If there is no message created yet, or if the last update we saw had a different
// identifying parts, create a new message.
bool isNewMessage = true;
if (messages.Count != 0)
{
var lastMessage = messages[messages.Count - 1];
isNewMessage =
NotEmptyOrEqual(update.AuthorName, lastMessage.AuthorName) ||
NotEmptyOrEqual(update.MessageId, lastMessage.MessageId) ||
NotNullOrEqual(update.Role, lastMessage.Role);
}
// Get the message to target, either a new one or the last ones.
ChatMessage message;
if (isNewMessage)
{
message = new(ChatRole.Assistant, []);
messages.Add(message);
}
else
{
message = messages[messages.Count - 1];
}
// Some members on ChatResponseUpdate map to members of ChatMessage.
// Incorporate those into the latest message; in cases where the message
// stores a single value, prefer the latest update's value over anything
// stored in the message.
if (update.AuthorName is not null)
{
message.AuthorName = update.AuthorName;
}
if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt))
{
message.CreatedAt = update.CreatedAt;
}
if (update.Role is ChatRole role)
{
message.Role = role;
}
if (update.MessageId is { Length: > 0 })
{
// Note that this must come after the message checks earlier, as they depend
// on this value for change detection.
message.MessageId = update.MessageId;
}
foreach (var content in update.Contents)
{
message.Contents.Add(content);
}
return isNewMessage;
}
/// <summary>Gets whether both strings are not null/empty and not the same as each other.</summary>
private static bool NotEmptyOrEqual(string? s1, string? s2) =>
s1 is { Length: > 0 } str1 && s2 is { Length: > 0 } str2 && str1 != str2;
/// <summary>Gets whether two roles are not null and not the same as each other.</summary>
private static bool NotNullOrEqual(ChatRole? r1, ChatRole? r2) =>
r1.HasValue && r2.HasValue && r1.Value != r2.Value;
}
@@ -1,139 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
#pragma warning disable CA1812 // Internal class is apparently never instantiated
internal sealed partial class MessageList : IComponent, IDisposable
#pragma warning restore CA1812 // Internal class is apparently never instantiated
{
private RenderHandle _renderHandle;
private MessageSubscription _messageSubscription;
private ResponseUpdateSubscription _responseSubscription;
[CascadingParameter] public MessageListContext MessageListContext { get; set; } = default!;
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
var previousContext = this.MessageListContext;
parameters.SetParameterProperties(this);
if (previousContext != null && this.MessageListContext != previousContext)
{
throw new InvalidOperationException(
$"{nameof(MessageList)} does not support changing the {nameof(this.MessageListContext)} once it has been set.");
}
// Subscribe to message updates and response updates for streaming
this._messageSubscription = this.MessageListContext.AgentBoundaryContext.SubscribeToMessageChanges(this.ProcessUpdate);
this._responseSubscription = this.MessageListContext.AgentBoundaryContext.SubscribeToResponseUpdates(this.ProcessUpdate);
// Initial render. This component will only render once since the only parameter is a cascading parameter and it's fixed.
this._renderHandle.Render(this.Render);
return Task.CompletedTask;
}
private void ProcessUpdate()
{
// Scan all messages for function calls and results to track invocations.
// This allows us to associate results with their calls when results arrive.
foreach (var message in this.MessageListContext.AgentBoundaryContext.CompletedMessages)
{
this.ProcessMessageContents(message);
}
foreach (var message in this.MessageListContext.AgentBoundaryContext.PendingMessages)
{
this.ProcessMessageContents(message);
}
this._renderHandle.Render(this.Render);
}
private void ProcessMessageContents(ChatMessage message)
{
foreach (var content in message.Contents)
{
if (content is FunctionCallContent call)
{
this.MessageListContext.GetOrCreateInvocation(call);
}
else if (content is FunctionResultContent result)
{
this.MessageListContext.AssociateResult(result);
}
}
}
public void Render(RenderTreeBuilder builder)
{
// Track all render keys to detect duplicates
var allRenderKeys = new HashSet<string?>();
// Open container div for message list with flex layout
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "messages-container");
foreach (var message in this.MessageListContext.AgentBoundaryContext.CompletedMessages)
{
var renderKey = GetUniqueRenderKey(message);
// Calling GetTemplate will stop template collection on the first message if it
// was still ongoing.
builder.OpenComponent<ContentBlock>(2);
builder.SetKey(renderKey);
builder.AddComponentParameter(3, "ChildContent", this.MessageListContext.GetTemplate(message));
builder.CloseComponent();
}
foreach (var message in this.MessageListContext.AgentBoundaryContext.PendingMessages)
{
var renderKey = GetUniqueRenderKey(message);
builder.OpenComponent<ContentBlock>(4);
builder.SetKey(renderKey);
builder.AddComponentParameter(5, "ChildContent", this.MessageListContext.GetTemplate(message));
builder.CloseComponent();
}
// Close container div
builder.CloseElement();
}
/// <summary>
/// Gets a unique render key for a message.
/// For tool result messages (role=tool), we use MessageId + CallId to ensure uniqueness
/// because the AGUI protocol may reuse MessageId for multiple tool results in the same batch.
/// </summary>
private static string? GetUniqueRenderKey(ChatMessage message)
{
// For tool result messages, combine MessageId with CallId to ensure uniqueness
// This works around a bug in the AGUI protocol where multiple tool results
// can share the same MessageId when processed in the same update batch.
if (message.Role == ChatRole.Tool)
{
var resultContent = message.Contents.OfType<FunctionResultContent>().FirstOrDefault();
if (resultContent != null && !string.IsNullOrEmpty(resultContent.CallId))
{
return $"{message.MessageId}_{resultContent.CallId}";
}
}
return message.MessageId;
}
public void Dispose()
{
((IDisposable)this._messageSubscription).Dispose();
((IDisposable)this._responseSubscription).Dispose();
}
}
@@ -1,137 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public sealed partial class MessageListContext
{
private bool _collectingTemplates;
private readonly List<MessageTemplateBase> _templates = [];
private readonly List<ContentTemplateBase> _contentTemplates = [];
// We compute a render fragment to render each message only once and cache it here.
private readonly Dictionary<ChatMessage, RenderFragment> _templateCache = [];
// Track function invocations by CallId to associate calls with results.
private readonly Dictionary<string, InvocationContext> _invocationMap = [];
public MessageListContext(IAgentBoundaryContext context)
{
this.AgentBoundaryContext = context;
}
public IAgentBoundaryContext AgentBoundaryContext { get; }
public void BeginCollectingTemplates()
{
// This is triggered by the Messages component before rendering its children.
// In this situation we are going to render again the MessageTemplates and
// ContentTemplates and since we can't tell if they have changed we have to
// recompute all the templates again.
this._collectingTemplates = true;
this._templates.Clear();
this._contentTemplates.Clear();
this._templateCache.Clear();
}
public void RegisterTemplate(MessageTemplateBase template)
{
if (this._collectingTemplates)
{
this._templates.Add(template);
}
}
public void RegisterContentTemplate(ContentTemplateBase template)
{
if (this._collectingTemplates)
{
this._contentTemplates.Add(template);
}
}
/// <summary>
/// Gets or creates an invocation context for the given function call.
/// </summary>
/// <param name="call">The function call content.</param>
/// <returns>The invocation context for this call.</returns>
public InvocationContext GetOrCreateInvocation(FunctionCallContent call)
{
if (!this._invocationMap.TryGetValue(call.CallId, out var context))
{
context = new InvocationContext(call);
this._invocationMap[call.CallId] = context;
}
return context;
}
/// <summary>
/// Associates a function result with its corresponding call.
/// </summary>
/// <param name="result">The function result content.</param>
public void AssociateResult(FunctionResultContent result)
{
if (this._invocationMap.TryGetValue(result.CallId, out var context))
{
context.SetResult(result);
}
}
/// <summary>
/// Gets an invocation context by call ID.
/// </summary>
/// <param name="callId">The call ID.</param>
/// <returns>The invocation context, or null if not found.</returns>
public InvocationContext? GetInvocation(string callId)
{
return this._invocationMap.TryGetValue(callId, out var context) ? context : null;
}
internal RenderFragment GetTemplate(ChatMessage message)
{
// We are about to render the first message. If we were collecting templates, stop now.
this._collectingTemplates = false;
if (this._templateCache.TryGetValue(message, out var cachedTemplate))
{
return cachedTemplate;
}
var messageContext = new MessageContext(message, this);
foreach (var template in this._templates)
{
if (template.When(messageContext))
{
var chosen = template;
messageContext.SetTemplate(chosen);
// We ask the template to create a RenderFragment for the message.
// The template will render a wrapper and use the messageContext to
// render the contents.
// The template might call back through the messageContext to get renderers for
// contents if the message template doesn't override the full rendering or
// if it doesn't define the rendering for a content type.
var renderer = chosen.ChildContent(messageContext);
this._templateCache[message] = renderer;
return renderer;
}
}
throw new InvalidOperationException($"No message template found for message of type {message.Role}.");
}
internal RenderFragment GetContentTemplate(AIContent content)
{
foreach (var template in this._contentTemplates)
{
var contentContext = new ContentContext(content);
if (template.When(contentContext))
{
return template.ChildContent(contentContext);
}
}
throw new InvalidOperationException($"No content template found for content of type {content.GetType().Name}.");
}
}
@@ -1,30 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
public abstract class MessageTemplateBase : IComponent
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public abstract bool When(MessageContext context);
[Parameter] public RenderFragment<MessageContext> ChildContent { get; set; } = (message) => builder => { };
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterTemplate(this);
return Task.CompletedTask;
}
internal RenderFragment? GetContentTemplate(AIContent content)
{
return this.Context.GetContentTemplate(content);
}
}
@@ -1,94 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.AI;
public partial class Messages : IComponent
{
private RenderHandle _renderHandle;
private RenderFragment? _renderContents;
private IAgentBoundaryContext? _context;
private MessageListContext? _messageListContext;
[CascadingParameter] public IAgentBoundaryContext? AgentContext { get; set; }
[Parameter]
public RenderFragment MessageTemplates { get; set; } = builder =>
{
builder.OpenComponent<DefaultMessageTemplate>(0);
builder.CloseComponent();
};
[Parameter]
public RenderFragment ContentTemplates { get; set; } = builder =>
{
builder.OpenComponent<TextTemplate>(0);
builder.CloseComponent();
builder.OpenComponent<FunctionCallTemplate>(1);
builder.CloseComponent();
builder.OpenComponent<FunctionResultTemplate>(2);
builder.CloseComponent();
builder.OpenComponent<ErrorTemplate>(3);
builder.CloseComponent();
builder.OpenComponent<DataContentTemplate>(4);
builder.CloseComponent();
};
public void Attach(RenderHandle renderHandle)
{
this._renderHandle = renderHandle;
this._renderContents = this.RenderContents;
}
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (this.AgentContext == null)
{
throw new InvalidOperationException("Messages component must be used within an AgentBoundary.");
}
if (this._context != null && this.AgentContext != this._context)
{
throw new InvalidOperationException("Messages component cannot change AgentBoundaryContext.");
}
this._context = this.AgentContext;
if (this._messageListContext == null)
{
this._messageListContext = new MessageListContext(this.AgentContext);
this.Render();
}
return Task.CompletedTask;
}
private void Render()
{
this._renderHandle.Render(this.RenderCore);
}
private void RenderCore(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<MessageListContext>>(0);
builder.AddComponentParameter(1, "Value", this._messageListContext);
builder.AddComponentParameter(2, "IsFixed", true);
builder.AddComponentParameter(3, "ChildContent", this._renderContents);
builder.CloseComponent();
}
private void RenderContents(RenderTreeBuilder builder)
{
Debug.Assert(this._messageListContext != null);
this._messageListContext.BeginCollectingTemplates();
builder.AddContent(1, this.MessageTemplates);
builder.AddContent(2, this.ContentTemplates);
builder.OpenComponent<MessageList>(3);
builder.CloseComponent();
}
}
@@ -1,309 +0,0 @@
/* Copyright (c) Microsoft. All rights reserved. */
/* ==========================================================================
AG-UI Components - Shared Styles
These styles provide consistent appearance for AI components across demos.
========================================================================== */
/* ==========================================================================
Agent Input Component
========================================================================== */
.agent-input {
display: flex;
align-items: flex-end;
gap: 12px;
width: 100%;
}
.agent-input textarea {
flex: 1;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
resize: none;
min-height: 44px;
max-height: 200px;
line-height: 1.5;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.agent-input textarea:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.agent-input textarea::placeholder {
color: #9ca3af;
}
.agent-input textarea:disabled {
background-color: #f9fafb;
color: #9ca3af;
cursor: not-allowed;
}
.agent-input .send-button {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: none;
border-radius: 8px;
background-color: #2563eb;
color: white;
cursor: pointer;
transition: background-color 0.2s, opacity 0.2s;
flex-shrink: 0;
}
.agent-input .send-button:hover:not(:disabled) {
background-color: #1d4ed8;
}
.agent-input .send-button:disabled {
background-color: #d1d5db;
color: #9ca3af;
cursor: not-allowed;
}
.agent-input .send-button svg {
display: block;
}
/* ==========================================================================
Agent Suggestions Component
========================================================================== */
.agent-suggestions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-start;
margin-bottom: 0.75rem;
}
.suggestion-button {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 1rem;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
.suggestion-button:hover:not(:disabled) {
background: #f0f0f0;
border-color: #0078d4;
color: #0078d4;
}
.suggestion-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ==========================================================================
Loading Indicator - Triple Dot Animation
========================================================================== */
.agent-loading-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: #6b7280;
font-size: 14px;
}
.agent-loading-dots {
display: flex;
gap: 4px;
align-items: center;
}
.agent-loading-dots span {
width: 6px;
height: 6px;
background-color: #2563eb;
border-radius: 50%;
animation: agent-dot-bounce 1.4s ease-in-out infinite both;
}
.agent-loading-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.agent-loading-dots span:nth-child(2) {
animation-delay: -0.16s;
}
.agent-loading-dots span:nth-child(3) {
animation-delay: 0s;
}
@keyframes agent-dot-bounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* Inline loading indicator for use within messages */
.agent-loading-inline {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background-color: #f3f4f6;
border-radius: 12px;
}
.agent-loading-inline span {
width: 5px;
height: 5px;
background-color: #6b7280;
border-radius: 50%;
animation: agent-dot-bounce 1.4s ease-in-out infinite both;
}
.agent-loading-inline span:nth-child(1) {
animation-delay: -0.32s;
}
.agent-loading-inline span:nth-child(2) {
animation-delay: -0.16s;
}
.agent-loading-inline span:nth-child(3) {
animation-delay: 0s;
}
/* ==========================================================================
Processing State Overlay
========================================================================== */
.agent-input-container {
position: relative;
}
.agent-input-container.processing .agent-input textarea {
background-color: #f9fafb;
}
/* ==========================================================================
Message Styles
========================================================================== */
.message-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
flex-direction: column;
gap: 4px;
}
.message-user {
align-items: flex-end;
}
.message-assistant {
align-items: flex-start;
}
.message-content {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.5;
}
.message-user .message-content {
background-color: #2563eb;
color: white;
border-bottom-right-radius: 4px;
}
.message-assistant .message-content {
background-color: #f3f4f6;
color: #1f2937;
border-bottom-left-radius: 4px;
}
/* ==========================================================================
Common Chat Layout Styles
========================================================================== */
.chat-layout {
display: flex;
flex-direction: column;
height: 100%;
max-width: 1200px;
margin: 0 auto;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e5e5e5;
}
.chat-title {
font-size: 1.5rem;
font-weight: 600;
color: #1a1a1a;
}
.new-chat-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
.new-chat-button:hover {
background: #f5f5f5;
border-color: #b3b3b3;
}
.button-icon {
font-size: 1.2rem;
line-height: 1;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
}
.chat-input-container {
padding: 1.5rem 2rem 2rem;
border-top: 1px solid #e5e5e5;
background: white;
}
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="AGUIDojoClient.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="@renderMode" />
</head>
<body>
<Routes @rendermode="@renderMode" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@code {
private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
}
@@ -1,72 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@using Microsoft.AspNetCore.Components.AI
@using Microsoft.Agents.AI
@using Microsoft.Extensions.AI
@using Microsoft.Extensions.DependencyInjection
@using AGUIDojoClient.Services
@inject IServiceProvider ServiceProvider
@inject IBackgroundColorService BackgroundColorService
@implements IDisposable
<PageTitle>Agentic Chat</PageTitle>
<div class="chat-layout" style="@GetBackgroundStyle()">
<div class="chat-header">
<div class="chat-title">AGUI WebChat</div>
<button class="new-chat-button" @onclick="ResetConversationAsync">
<span class="button-icon">+</span> New chat
</button>
</div>
<AgentBoundary Agent="@agent">
<div class="chat-content">
<Messages />
<AgentLoadingIndicator />
</div>
<div class="chat-input-container">
<AgentSuggestions Suggestions="@suggestions" />
<AgentInput Placeholder="Type your message..." />
</div>
</AgentBoundary>
</div>
@code {
private AIAgent? agent;
private string? _backgroundColor;
private Suggestion[] suggestions = [
new Suggestion("Change background", new ChatMessage(ChatRole.User, "Change background to light blue")),
new Suggestion("Generate sonnet")
];
[Parameter]
public string ScenarioId { get; set; } = "agentic_chat";
protected override void OnInitialized()
{
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("agentic-chat");
BackgroundColorService.ColorChanged += OnColorChanged;
}
private string GetBackgroundStyle()
{
return _backgroundColor != null ? $"background-color: {_backgroundColor}" : "";
}
private async void OnColorChanged(object? sender, BackgroundColorChangedEventArgs e)
{
_backgroundColor = e.Color;
await InvokeAsync(StateHasChanged);
}
private void ResetConversationAsync()
{
// Reset would need to be implemented in AgentBoundary
StateHasChanged();
}
public void Dispose()
{
BackgroundColorService.ColorChanged -= OnColorChanged;
}
}
@@ -1,74 +0,0 @@
/* Copyright (c) Microsoft. All rights reserved. */
.chat-layout {
display: flex;
flex-direction: column;
height: 100%;
max-width: 1200px;
margin: 0 auto;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e5e5e5;
}
.chat-title {
font-size: 1.5rem;
font-weight: 600;
color: #1a1a1a;
}
.new-chat-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
.new-chat-button:hover {
background: #f5f5f5;
border-color: #b3b3b3;
}
.button-icon {
font-size: 1.2rem;
line-height: 1;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
}
.chat-input-container {
padding: 1rem 2rem 1.5rem;
border-top: 1px solid #e5e5e5;
background: white;
}
.agentic-chat-demo {
height: 100%;
display: flex;
flex-direction: column;
}
.chat-container {
padding: 0 2rem 1.5rem;
border-top: 1px solid #e0e0e0;
}
@@ -1,111 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@using Microsoft.AspNetCore.Components.AI
@using Microsoft.Agents.AI
@using Microsoft.Extensions.AI
@using Microsoft.Extensions.DependencyInjection
@using AGUIDojoClient.Components.Shared
@using System.Text.Json
@inject IServiceProvider ServiceProvider
<PageTitle>Agentic Generative UI</PageTitle>
<div class="chat-layout">
<div class="chat-header">
<div class="chat-title">Agentic Generative UI</div>
<button class="new-chat-button" @onclick="ResetConversation">
<span class="button-icon">+</span> New chat
</button>
</div>
<AgentBoundary Agent="@agent">
<AgentState TState="Plan"
CurrentState="@currentPlan"
OnSnapshot="@DeserializePlan"
OnDelta="@ApplyPlanDelta"
CurrentStateChanged="@OnPlanChanged">
<div class="chat-content">
<Messages>
<ContentTemplates>
<TaskProgressTemplate />
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
</div>
<div class="chat-input-container">
<AgentSuggestions Suggestions="@suggestions" />
<AgentInput Placeholder="Ask me to plan something..." />
</div>
</AgentState>
</AgentBoundary>
</div>
@code {
private AIAgent? agent;
private Plan? currentPlan;
private Suggestion[] suggestions = [
new Suggestion("Simple plan", new ChatMessage(ChatRole.User, "Please build a plan to go to mars in 5 steps.")),
new Suggestion("Complex plan", new ChatMessage(ChatRole.User, "Please build a plan to make pizza in 10 steps."))
];
[Parameter]
public string ScenarioId { get; set; } = "agentic_generative_ui";
protected override void OnInitialized()
{
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("agentic-generative-ui");
}
private Plan? DeserializePlan(ReadOnlyMemory<byte> data)
{
try
{
return JsonSerializer.Deserialize<Plan>(data.Span);
}
catch
{
return null;
}
}
private Plan? ApplyPlanDelta(Plan? current, ReadOnlyMemory<byte> deltaData)
{
if (current is null)
{
return null;
}
try
{
var operations = JsonSerializer.Deserialize<List<JsonPatchOperation>>(deltaData.Span);
if (operations is not null)
{
PlanPatcher.Apply(current, operations);
}
}
catch
{
// Ignore deserialization errors
}
return current;
}
private void OnPlanChanged(Plan? plan)
{
currentPlan = plan;
StateHasChanged();
}
private void ResetConversation()
{
currentPlan = null;
StateHasChanged();
}
}
@@ -1,86 +0,0 @@
/* Copyright (c) Microsoft. All rights reserved. */
.chat-layout {
display: flex;
flex-direction: column;
height: 100%;
max-width: 1200px;
margin: 0 auto;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e5e5e5;
}
.chat-title {
font-size: 1.5rem;
font-weight: 600;
color: #1a1a1a;
}
.new-chat-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
.new-chat-button:hover {
background: #f5f5f5;
border-color: #b3b3b3;
}
.button-icon {
font-size: 1.2rem;
line-height: 1;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
}
.chat-input-container {
padding: 1.5rem 2rem 2rem;
border-top: 1px solid #e5e5e5;
background: white;
}
::deep .agent-suggestions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-start;
margin-bottom: 0.75rem;
}
::deep .suggestion-button {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 1rem;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
::deep .suggestion-button:hover {
background: #f0f0f0;
border-color: #0078d4;
color: #0078d4;
}
@@ -1,29 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
/// <summary>
/// Represents a JSON Patch operation (RFC 6902).
/// </summary>
public sealed class JsonPatchOperation
{
/// <summary>
/// The operation to perform (e.g., "replace", "add", "remove").
/// </summary>
[JsonPropertyName("op")]
public string Op { get; set; } = string.Empty;
/// <summary>
/// The JSON Pointer path to the target location.
/// </summary>
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
/// <summary>
/// The value for the operation (used with "replace", "add", "test").
/// </summary>
[JsonPropertyName("value")]
public object? Value { get; set; }
}
@@ -1,35 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
/// <summary>
/// Represents a plan with multiple steps.
/// </summary>
public sealed class Plan
{
/// <summary>
/// The list of steps in the plan.
/// </summary>
[JsonPropertyName("steps")]
public List<Step> Steps { get; set; } = [];
/// <summary>
/// Gets the count of completed steps.
/// </summary>
[JsonIgnore]
public int CompletedCount => this.Steps.Count(s => s.IsCompleted);
/// <summary>
/// Gets the total number of steps.
/// </summary>
[JsonIgnore]
public int TotalCount => this.Steps.Count;
/// <summary>
/// Gets whether all steps are completed.
/// </summary>
[JsonIgnore]
public bool IsComplete => this.Steps.Count > 0 && this.Steps.All(s => s.IsCompleted);
}
@@ -1,73 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.RegularExpressions;
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
/// <summary>
/// Applies JSON Patch operations to a Plan object.
/// </summary>
public static partial class PlanPatcher
{
/// <summary>
/// Applies a list of JSON Patch operations to the given plan.
/// </summary>
/// <param name="plan">The plan to modify.</param>
/// <param name="operations">The patch operations to apply.</param>
public static void Apply(Plan plan, IEnumerable<JsonPatchOperation> operations)
{
foreach (var operation in operations)
{
ApplyOperation(plan, operation);
}
}
private static void ApplyOperation(Plan plan, JsonPatchOperation operation)
{
// Parse paths like "/steps/0/status" or "/steps/0/description"
var match = StepPathRegex().Match(operation.Path);
if (!match.Success)
{
return;
}
if (!int.TryParse(match.Groups["index"].Value, out var index))
{
return;
}
if (index < 0 || index >= plan.Steps.Count)
{
return;
}
var property = match.Groups["property"].Value;
var step = plan.Steps[index];
if (string.Equals(operation.Op, "replace", StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(property, "status", StringComparison.OrdinalIgnoreCase))
{
step.Status = GetStringValue(operation.Value) ?? step.Status;
}
else if (string.Equals(property, "description", StringComparison.OrdinalIgnoreCase))
{
step.Description = GetStringValue(operation.Value) ?? step.Description;
}
}
}
private static string? GetStringValue(object? value)
{
return value switch
{
string s => s,
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
_ => value?.ToString()
};
}
[GeneratedRegex(@"^/steps/(?<index>\d+)/(?<property>\w+)$")]
private static partial Regex StepPathRegex();
}
@@ -1,29 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
/// <summary>
/// Represents a single step in a plan.
/// </summary>
public sealed class Step
{
/// <summary>
/// The description of the step.
/// </summary>
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
/// <summary>
/// The status of the step (pending or completed).
/// </summary>
[JsonPropertyName("status")]
public string Status { get; set; } = "pending";
/// <summary>
/// Gets whether this step is completed.
/// </summary>
[JsonIgnore]
public bool IsCompleted => string.Equals(this.Status, "completed", StringComparison.OrdinalIgnoreCase);
}
@@ -1,42 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@if (Plan is not null && Plan.Steps.Count > 0)
{
<div class="task-progress-card @(Plan.IsComplete ? "completed" : "in-progress")">
<div class="task-header">
<h3>Task Progress</h3>
<span class="task-counter">@Plan.CompletedCount/@Plan.TotalCount Complete</span>
</div>
<div class="task-steps">
@for (int i = 0; i < Plan.Steps.Count; i++)
{
var step = Plan.Steps[i];
var isCurrentStep = !step.IsCompleted &&
(i == 0 || Plan.Steps[i - 1].IsCompleted);
<div class="task-step @(step.IsCompleted ? "step-completed" : isCurrentStep ? "step-current" : "step-pending")">
<span class="step-icon">
@if (step.IsCompleted)
{
<span class="check-icon">✓</span>
}
else if (isCurrentStep)
{
<span class="current-icon">●</span>
}
else
{
<span class="pending-icon">○</span>
}
</span>
<span class="step-description">@step.Description</span>
</div>
}
</div>
</div>
}
@code {
[CascadingParameter]
public Plan? Plan { get; set; }
}
@@ -1,139 +0,0 @@
/* Copyright (c) Microsoft. All rights reserved. */
.task-progress-card {
background: #ffffff;
border: 1px solid #e5e5e5;
border-radius: 12px;
padding: 16px;
margin: 8px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.task-progress-card.completed {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border-color: #86efac;
}
.task-progress-card.in-progress {
background: #ffffff;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e5e5;
}
.task-header h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #1a1a1a;
}
.task-counter {
font-size: 0.875rem;
color: #666666;
background: #f5f5f5;
padding: 4px 10px;
border-radius: 20px;
}
.task-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-step {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
transition: all 0.3s ease;
}
.task-step.step-completed {
background: rgba(34, 197, 94, 0.1);
border-left: 3px solid #22c55e;
}
.task-step.step-current {
background: rgba(0, 120, 212, 0.1);
border-left: 3px solid #0078d4;
animation: pulse-border 2s infinite;
}
.task-step.step-pending {
background: #fafafa;
border-left: 3px solid #d1d1d1;
opacity: 0.7;
}
@keyframes pulse-border {
0%, 100% {
border-left-color: #0078d4;
box-shadow: 0 0 0 0 rgba(0, 120, 212, 0.4);
}
50% {
border-left-color: #50a0e8;
box-shadow: 0 0 8px 0 rgba(0, 120, 212, 0.2);
}
}
.step-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.check-icon {
color: #22c55e;
font-weight: bold;
font-size: 14px;
}
.current-icon {
color: #0078d4;
font-size: 12px;
animation: pulse 1.5s infinite;
}
.pending-icon {
color: #999999;
font-size: 12px;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.2);
}
}
.step-description {
flex: 1;
font-size: 0.9rem;
line-height: 1.5;
color: #424242;
}
.step-completed .step-description {
color: #166534;
}
.step-pending .step-description {
color: #666666;
}
@@ -1,62 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@using Microsoft.AspNetCore.Components.AI
@using Microsoft.Agents.AI
@using Microsoft.Extensions.AI
@using Microsoft.Extensions.DependencyInjection
@using AGUIDojoClient.Components.Shared
@inject IServiceProvider ServiceProvider
<PageTitle>Backend Tool Rendering</PageTitle>
<div class="chat-layout">
<div class="chat-header">
<div class="chat-title">Backend Tool Rendering</div>
<button class="new-chat-button" @onclick="ResetConversation">
<span class="button-icon">+</span> New chat
</button>
</div>
<AgentBoundary Agent="@agent">
<div class="chat-content">
<Messages>
<ContentTemplates>
<TextTemplate />
<WeatherCallTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
</div>
<div class="chat-input-container">
<AgentSuggestions Suggestions="@suggestions" />
<AgentInput Placeholder="Ask about the weather..." />
</div>
</AgentBoundary>
</div>
@code {
private AIAgent? agent;
private Suggestion[] suggestions = [
new Suggestion("Weather in San Francisco", new ChatMessage(ChatRole.User, "What's the weather like in San Francisco?")),
new Suggestion("Weather in New York", new ChatMessage(ChatRole.User, "What's the weather like in New York?")),
new Suggestion("Weather in Tokyo", new ChatMessage(ChatRole.User, "What's the weather like in Tokyo?"))
];
[Parameter]
public string ScenarioId { get; set; } = "backend_tool_rendering";
protected override void OnInitialized()
{
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("backend-tool-rendering");
}
private void ResetConversation()
{
// Reset would need to be implemented - for now just trigger re-render
StateHasChanged();
}
}
@@ -1,86 +0,0 @@
/* Copyright (c) Microsoft. All rights reserved. */
.chat-layout {
display: flex;
flex-direction: column;
height: 100%;
max-width: 1200px;
margin: 0 auto;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e5e5e5;
}
.chat-title {
font-size: 1.5rem;
font-weight: 600;
color: #1a1a1a;
}
.new-chat-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
.new-chat-button:hover {
background: #f5f5f5;
border-color: #b3b3b3;
}
.button-icon {
font-size: 1.2rem;
line-height: 1;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
}
.chat-input-container {
padding: 1.5rem 2rem 2rem;
border-top: 1px solid #e5e5e5;
background: white;
}
::deep .agent-suggestions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-start;
margin-bottom: 0.75rem;
}
::deep .suggestion-button {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #d1d1d1;
border-radius: 1rem;
cursor: pointer;
font-size: 0.875rem;
color: #424242;
transition: all 0.2s;
}
::deep .suggestion-button:hover {
background: #f0f0f0;
border-color: #0078d4;
color: #0078d4;
}
@@ -1,59 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Template for rendering weather function call content with its result.
/// Uses InvocationContext to access both the call arguments and result.
/// </summary>
public class WeatherCallTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
// This component never renders anything by itself.
this.ChildContent = this.RenderWeatherCall;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Determines if this template should handle the given content.
/// Matches FunctionCallContent for the get_weather function.
/// </summary>
public override bool When(ContentContext context)
{
// Only match FunctionCallContent for the get_weather function
return context.Content is FunctionCallContent call &&
string.Equals(call.Name, "get_weather", StringComparison.OrdinalIgnoreCase);
}
private RenderFragment RenderWeatherCall(ContentContext content) => builder =>
{
if (content.Content is FunctionCallContent call)
{
// Get the invocation context which tracks both call and result
var invocation = this.Context.GetOrCreateInvocation(call);
// Provide the invocation context to child components
builder.OpenComponent<CascadingValue<InvocationContext>>(0);
builder.AddComponentParameter(1, "Value", invocation);
builder.AddComponentParameter(2, "IsFixed", true);
builder.AddComponentParameter(3, "ChildContent", (RenderFragment)(innerBuilder =>
{
// Render the WeatherCard component which uses InvocationContext
innerBuilder.OpenComponent<AGUIDojoClient.Components.Demos.BackendToolRendering.WeatherCard>(0);
innerBuilder.CloseComponent();
}));
builder.CloseComponent();
}
};
}

Some files were not shown because too many files have changed in this diff Show More