Compare commits

..
Author SHA1 Message Date
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
526 changed files with 26284 additions and 18139 deletions
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
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 and share ideas in GitHub Discussions.
+203
View File
@@ -0,0 +1,203 @@
name: Issue Report
description: Report a bug, request a feature, or ask a question about Microsoft Agent Framework
title: "[Issue]: "
labels: ["triage"]
body:
- type: dropdown
id: language
attributes:
label: Language
description: Which language/SDK are you using?
options:
- .NET
- Python
- None / Not Applicable
validations:
required: true
- type: dropdown
id: issue-type
attributes:
label: Type of Issue
description: What type of issue is this?
options:
- Bug
- Feature Request
- Question
validations:
required: true
- type: markdown
attributes:
value: |
## Version Information
Please provide the version of the package(s) you are using. Select the relevant packages below.
- type: markdown
attributes:
value: "### .NET Packages"
- type: input
id: dotnet-agents-ai
attributes:
label: Microsoft.Agents.AI
description: Version of Microsoft.Agents.AI (e.g., 1.0.0)
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-agents-ai-abstractions
attributes:
label: Microsoft.Agents.AI.Abstractions
description: Version of Microsoft.Agents.AI.Abstractions
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-agents-ai-openai
attributes:
label: Microsoft.Agents.AI.OpenAI
description: Version of Microsoft.Agents.AI.OpenAI
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-agents-ai-azureai
attributes:
label: Microsoft.Agents.AI.AzureAI
description: Version of Microsoft.Agents.AI.AzureAI
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-agents-ai-anthropic
attributes:
label: Microsoft.Agents.AI.Anthropic
description: Version of Microsoft.Agents.AI.Anthropic
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-agents-ai-hosting
attributes:
label: Microsoft.Agents.AI.Hosting
description: Version of Microsoft.Agents.AI.Hosting
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-agents-ai-workflows
attributes:
label: Microsoft.Agents.AI.Workflows
description: Version of Microsoft.Agents.AI.Workflows
placeholder: "e.g., 1.0.0"
validations:
required: false
- type: input
id: dotnet-other-packages
attributes:
label: Other .NET Packages
description: List any other Microsoft.Agents.* packages and versions you are using
placeholder: "e.g., Microsoft.Agents.AI.CopilotStudio: 1.0.0, Microsoft.Agents.AI.Purview: 1.0.0"
validations:
required: false
- type: markdown
attributes:
value: "### Python Packages"
- type: input
id: python-core
attributes:
label: agent-framework-core
description: Version of agent-framework-core
placeholder: "e.g., 1.0.0b1"
validations:
required: false
- type: input
id: python-azure-ai
attributes:
label: agent-framework-azure-ai
description: Version of agent-framework-azure-ai
placeholder: "e.g., 1.0.0b1"
validations:
required: false
- type: input
id: python-anthropic
attributes:
label: agent-framework-anthropic
description: Version of agent-framework-anthropic
placeholder: "e.g., 1.0.0b1"
validations:
required: false
- type: input
id: python-azurefunctions
attributes:
label: agent-framework-azurefunctions
description: Version of agent-framework-azurefunctions
placeholder: "e.g., 1.0.0b1"
validations:
required: false
- type: input
id: python-other-packages
attributes:
label: Other Python Packages
description: List any other agent-framework-* packages and versions you are using
placeholder: "e.g., agent-framework-mem0: 1.0.0b1, agent-framework-redis: 1.0.0b1"
validations:
required: false
- type: markdown
attributes:
value: "---"
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and detailed description of the issue, feature request, or question.
placeholder: |
For bugs: Describe what happened, what you expected to happen, and steps to reproduce.
For features: Describe the feature you'd like and why it would be useful.
For questions: Describe what you're trying to accomplish.
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a minimal code sample that demonstrates the issue or your use case.
placeholder: |
```python
# Your code here
```
or
```csharp
// Your code here
```
render: markdown
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context, screenshots, error messages, or stack traces 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: |
@@ -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
+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
+4 -4
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
@@ -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 -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.
+4 -4
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) |
@@ -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). |
@@ -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.
+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
+25 -25
View File
@@ -11,19 +11,19 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="11.0.0" />
<PackageVersion Include="Anthropic" Version="12.0.0" />
<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" />
@@ -33,18 +33,18 @@
<!-- 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" />
<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).251219.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251219.1</PackageVersion>
<GitTag>1.0.0-preview.251219.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -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<AgentRunResponse> 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).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -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<AgentRunResponse> 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).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -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<AgentRunResponse> 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).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
-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();
}
};
}
@@ -1,117 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@using AGUIDojoClient.Components.Shared
@using Microsoft.AspNetCore.Components.AI
@if (Weather is null)
{
<div class="weather-card weather-loading">
<div class="weather-header">
<div class="weather-location">
<h3>@Location</h3>
<p>Loading weather...</p>
</div>
<span class="weather-icon skeleton-icon"></span>
</div>
<div class="weather-main">
<div class="temperature">
<span class="temp-value skeleton-text skeleton-temp"></span>
<span class="temp-fahrenheit skeleton-text skeleton-temp-f"></span>
</div>
<div class="conditions skeleton-text skeleton-conditions"></div>
</div>
<div class="weather-details">
<div class="detail-item">
<p class="detail-label">Humidity</p>
<p class="detail-value skeleton-text skeleton-detail"></p>
</div>
<div class="detail-item">
<p class="detail-label">Wind</p>
<p class="detail-value skeleton-text skeleton-detail"></p>
</div>
<div class="detail-item">
<p class="detail-label">Feels Like</p>
<p class="detail-value skeleton-text skeleton-detail"></p>
</div>
</div>
</div>
}
else
{
<div class="weather-card @GetConditionClass()">
<div class="weather-header">
<div class="weather-location">
<h3>@Location</h3>
<p>Current Weather</p>
</div>
<span class="weather-icon">@Weather.ConditionIcon</span>
</div>
<div class="weather-main">
<div class="temperature">
<span class="temp-value">@Weather.Temperature° C</span>
<span class="temp-fahrenheit">/ @Weather.TemperatureFahrenheit.ToString("F1")° F</span>
</div>
<div class="conditions">@Weather.Conditions</div>
</div>
<div class="weather-details">
<div class="detail-item">
<p class="detail-label">Humidity</p>
<p class="detail-value">@(Weather.Humidity)%</p>
</div>
<div class="detail-item">
<p class="detail-label">Wind</p>
<p class="detail-value">@Weather.WindSpeed mph</p>
</div>
<div class="detail-item">
<p class="detail-label">Feels Like</p>
<p class="detail-value">@(Weather.FeelsLike)°</p>
</div>
</div>
</div>
}
@code {
[CascadingParameter]
public InvocationContext Invocation { get; set; } = default!;
private string Location => Invocation?.GetArgument<string>("location") ?? "Unknown Location";
private WeatherInfo? Weather => Invocation?.HasResult == true
? Invocation.GetResult<WeatherInfo>()
: null;
protected override void OnInitialized()
{
if (Invocation is not null && !Invocation.HasResult)
{
Invocation.ResultArrived += OnResultArrived;
}
}
private void OnResultArrived()
{
InvokeAsync(StateHasChanged);
}
private string GetConditionClass()
{
if (Weather is null)
{
return "condition-default";
}
return Weather.Conditions.ToLowerInvariant() switch
{
"sunny" or "clear" => "condition-sunny",
"cloudy" or "overcast" => "condition-cloudy",
"rainy" or "rain" => "condition-rainy",
"stormy" or "thunderstorm" => "condition-stormy",
"snowy" or "snow" => "condition-snowy",
"foggy" or "fog" => "condition-foggy",
_ => "condition-default"
};
}
}
@@ -1,162 +0,0 @@
.weather-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 20px;
color: white;
max-width: 320px;
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.4);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.weather-card.condition-sunny {
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
}
.weather-card.condition-cloudy {
background: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%);
}
.weather-card.condition-rainy {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.weather-card.condition-stormy {
background: linear-gradient(135deg, #373b44 0%, #4286f4 100%);
}
.weather-card.condition-snowy {
background: linear-gradient(135deg, #e6dada 0%, #274046 100%);
}
.weather-card.condition-foggy {
background: linear-gradient(135deg, #606c88 0%, #3f4c6b 100%);
}
.weather-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.weather-location h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.weather-location p {
margin: 4px 0 0 0;
font-size: 0.85rem;
opacity: 0.8;
}
.weather-icon {
font-size: 3rem;
}
.weather-main {
margin-bottom: 20px;
}
.temperature {
display: flex;
align-items: baseline;
gap: 8px;
}
.temp-value {
font-size: 3rem;
font-weight: 300;
line-height: 1;
}
.temp-fahrenheit {
font-size: 1rem;
opacity: 0.7;
}
.conditions {
font-size: 1.1rem;
text-transform: capitalize;
margin-top: 8px;
opacity: 0.9;
}
.weather-details {
display: flex;
justify-content: space-between;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.detail-item {
text-align: center;
}
.detail-label {
margin: 0;
font-size: 0.75rem;
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
margin: 4px 0 0 0;
font-size: 1.1rem;
font-weight: 500;
}
/* Loading/Skeleton state */
.weather-loading {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.skeleton-text {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
display: inline-block;
}
.skeleton-icon {
width: 48px;
height: 48px;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 8px;
display: inline-block;
}
.skeleton-temp {
width: 80px;
height: 3rem;
}
.skeleton-temp-f {
width: 60px;
height: 1rem;
}
.skeleton-conditions {
width: 100px;
height: 1.1rem;
margin-top: 8px;
}
.skeleton-detail {
width: 40px;
height: 1.1rem;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@@ -1,69 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
/// <summary>
/// Template for rendering create_plan function call content.
/// The PlanCard will subscribe to events to track confirm_plan and update_plan_step calls.
/// </summary>
public class CreatePlanCallTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
// This component never renders anything by itself.
this.ChildContent = this.RenderCreatePlanCall;
}
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 create_plan function.
/// </summary>
public override bool When(ContentContext context)
{
// Only match FunctionCallContent for the create_plan function
return context.Content is FunctionCallContent call &&
string.Equals(call.Name, "create_plan", StringComparison.OrdinalIgnoreCase);
}
private RenderFragment RenderCreatePlanCall(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 both the invocation context and message list context to PlanCard
builder.OpenComponent<CascadingValue<InvocationContext>>(0);
builder.AddComponentParameter(1, "Value", invocation);
builder.AddComponentParameter(2, "IsFixed", true);
builder.AddComponentParameter(3, "ChildContent", (RenderFragment)(innerBuilder =>
{
// Also cascade the MessageListContext so PlanCard can track other tool calls
innerBuilder.OpenComponent<CascadingValue<MessageListContext>>(0);
innerBuilder.AddComponentParameter(1, "Value", this.Context);
innerBuilder.AddComponentParameter(2, "IsFixed", true);
innerBuilder.AddComponentParameter(3, "ChildContent", (RenderFragment)(cardBuilder =>
{
// Render the PlanCard component
cardBuilder.OpenComponent<PlanCard>(0);
cardBuilder.CloseComponent();
}));
innerBuilder.CloseComponent();
}));
builder.CloseComponent();
}
};
}
@@ -1,56 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
/// <summary>
/// A delegating agent that prepends instructions for the human-in-the-loop workflow.
/// </summary>
internal sealed class HumanInTheLoopAgent : DelegatingAIAgent
{
private static readonly ChatMessage InstructionsMessage = new(
ChatRole.System,
"""
You help users create and execute plans. Follow this workflow:
1. When asked to create a plan, use the `create_plan` tool with a list of step descriptions.
2. IMMEDIATELY after creating a plan, call `confirm_plan` with the plan object to ask for user approval.
3. Wait for the user to confirm which steps they want to proceed with.
4. Once confirmed, use `update_plan_step` to mark steps as 'completed' as you execute them.
IMPORTANT:
- Always call `confirm_plan` right after `create_plan` - don't skip this step!
- The plan parameter for `confirm_plan` should be the exact plan object returned from `create_plan`.
- Do NOT start executing steps until the user confirms.
- After receiving confirmation, update each selected step to 'completed' status.
""");
public HumanInTheLoopAgent(AIAgent innerAgent)
: base(innerAgent)
{
}
public override Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
// Prepend instructions message
var messagesWithInstructions = messages.Prepend(InstructionsMessage);
return base.RunAsync(messagesWithInstructions, thread, options, cancellationToken);
}
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
// Prepend instructions message
var messagesWithInstructions = messages.Prepend(InstructionsMessage);
return base.RunStreamingAsync(messagesWithInstructions, thread, options, cancellationToken);
}
}
@@ -1,162 +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
@using System.ComponentModel
@inject IServiceProvider ServiceProvider
<PageTitle>Human in the Loop</PageTitle>
<div class="chat-layout">
<div class="chat-header">
<div class="chat-title">Human in the Loop</div>
<button class="new-chat-button" @onclick="ResetConversation">
<span class="button-icon">+</span> New chat
</button>
</div>
<AgentBoundary Agent="@agent" OnContextCreated="OnContextCreated">
<div class="chat-content">
<Messages>
<ContentTemplates>
<CreatePlanCallTemplate />
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
</div>
<div class="chat-input-container">
<AgentSuggestions Suggestions="@suggestions" />
<AgentInput Placeholder="Ask for a plan..." />
</div>
</AgentBoundary>
</div>
@code {
private AIAgent? agent;
private IAgentBoundaryContext? boundaryContext;
private Plan? currentPlan;
private Suggestion[] suggestions = [
new Suggestion("Simple plan", new ChatMessage(ChatRole.User, "Create a simple 5-step plan for organizing a birthday party")),
new Suggestion("Complex plan", new ChatMessage(ChatRole.User, "Create a detailed 10-step plan for launching a new product"))
];
[Parameter]
public string ScenarioId { get; set; } = "human_in_the_loop";
protected override void OnInitialized()
{
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("human-in-the-loop");
}
private void OnContextCreated(IAgentBoundaryContext context)
{
boundaryContext = context;
// Register all three frontend tools for the human-in-the-loop scenario
// 1. create_plan - creates a plan and stores it
var createPlanTool = AIFunctionFactory.Create(
(List<string> steps) => CreatePlan(steps),
"create_plan",
"Create a plan with multiple steps. Call this first before confirm_plan.");
// 2. confirm_plan - waits for user confirmation via UI, receives the plan to display
var confirmPlanTool = AIFunctionFactory.Create(
(Plan plan) => ConfirmPlanAsync(context, plan),
"confirm_plan",
"Present the plan to the user for confirmation. The user can select which steps to proceed with. Pass the plan returned from create_plan.");
// 3. update_plan_step - updates a step's status
var updatePlanStepTool = AIFunctionFactory.Create(
(int index, string? description, string? status) => UpdatePlanStep(index, description, status),
"update_plan_step",
"Update a step in the plan with new description or status. Use status 'completed' to mark a step as done.");
context.RegisterTools(createPlanTool, confirmPlanTool, updatePlanStepTool);
}
/// <summary>
/// Frontend tool that creates a plan with the given steps.
/// </summary>
[Description("Create a plan with multiple steps.")]
private Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
{
currentPlan = new Plan
{
Steps = [.. steps.Select(s => new Step { Description = s, Status = "pending" })]
};
return currentPlan;
}
/// <summary>
/// Frontend tool that waits for user confirmation via the UI.
/// The PlanCard component will call ProvideResponse when the user confirms/rejects.
/// </summary>
[Description("Present the plan to the user for confirmation.")]
private static async Task<PlanConfirmationResult> ConfirmPlanAsync(
IAgentBoundaryContext context,
[Description("The plan to present to the user for confirmation.")] Plan plan)
{
// The plan parameter is received and will be accessible via InvocationContext in the PlanCard
// Wait for the PlanCard component to provide the response
var response = await context.WaitForResponse("confirm_plan");
return (PlanConfirmationResult)response;
}
/// <summary>
/// Frontend tool that updates a step in the plan.
/// </summary>
[Description("Update a step in the plan with new description or status.")]
private List<JsonPatchOperation> UpdatePlanStep(
[Description("The index of the step to update.")] int index,
[Description("The new description for the step (optional).")] string? description = null,
[Description("The new status for the step: 'pending' or 'completed'.")] string? status = null)
{
var changes = new List<JsonPatchOperation>();
if (currentPlan is null || index < 0 || index >= currentPlan.Steps.Count)
{
return changes;
}
if (description is not null)
{
currentPlan.Steps[index].Description = description;
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/description",
Value = description
});
}
if (status is not null)
{
currentPlan.Steps[index].Status = status.ToLowerInvariant();
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/status",
Value = status.ToLowerInvariant()
});
}
return changes;
}
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.HumanInTheLoop;
/// <summary>
/// Represents a JSON Patch operation.
/// </summary>
public sealed class JsonPatchOperation
{
/// <summary>
/// The operation type (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.
/// </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.HumanInTheLoop;
/// <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,324 +0,0 @@
@* Copyright (c) Microsoft. All rights reserved. *@
@using Microsoft.AspNetCore.Components.AI
@using Microsoft.Extensions.AI
@using System.Text.Json
@implements IDisposable
@if (IsWaitingForPlan)
{
<div class="plan-card plan-loading">
<div class="plan-header">
<h3>Loading Plan...</h3>
<div class="plan-progress">
<span class="skeleton-text skeleton-progress"></span>
</div>
</div>
<div class="plan-steps">
@for (int i = 0; i < 3; i++)
{
<div class="plan-step skeleton-step">
<span class="step-checkbox skeleton-checkbox"></span>
<span class="step-description skeleton-text skeleton-description"></span>
</div>
}
</div>
<div class="plan-actions">
<button class="plan-button plan-button-skeleton" disabled>Loading...</button>
</div>
</div>
}
else if (CurrentPlan is not null && AwaitingConfirmation)
{
<div class="plan-card plan-active">
<div class="plan-header">
<h3>Plan Confirmation</h3>
<div class="plan-progress">
<span class="progress-count">@CurrentPlan.CompletedCount / @CurrentPlan.TotalCount completed</span>
</div>
</div>
<div class="plan-steps">
@for (int i = 0; i < CurrentPlan.Steps.Count; i++)
{
var step = CurrentPlan.Steps[i];
var index = i;
<div class="plan-step @(step.IsCompleted ? "step-completed" : "step-pending") @(selectedSteps.Contains(index) ? "step-selected" : "")">
<label class="step-checkbox-label">
<input type="checkbox"
class="step-checkbox"
checked="@selectedSteps.Contains(index)"
disabled="@step.IsCompleted"
@onchange="() => ToggleStepSelection(index)" />
<span class="checkmark @(step.IsCompleted || selectedSteps.Contains(index) ? "checkmark-completed" : "")">
@if (step.IsCompleted || selectedSteps.Contains(index))
{
<span class="check-icon">✓</span>
}
</span>
</label>
<span class="step-description @(step.IsCompleted ? "description-completed" : "")">@step.Description</span>
<span class="step-status @(step.IsCompleted ? "status-completed" : "status-pending")">
@(step.IsCompleted ? "Done" : "Pending")
</span>
</div>
}
</div>
<div class="plan-actions">
<button class="plan-button plan-button-confirm" @onclick="ConfirmPlan" disabled="@(selectedSteps.Count == 0)">
Confirm Selected (@selectedSteps.Count)
</button>
<button class="plan-button plan-button-reject" @onclick="RejectPlan">
Reject
</button>
</div>
</div>
}
else if (CurrentPlan is not null && !AwaitingConfirmation && !WasRejected)
{
<div class="plan-card @(CurrentPlan.IsComplete ? "plan-completed" : "plan-executing")">
<div class="plan-header">
<h3>@(CurrentPlan.IsComplete ? "Plan Completed" : "Executing Plan")</h3>
@if (CurrentPlan.IsComplete)
{
<span class="completion-badge">✓ All Done</span>
}
else
{
<div class="plan-progress">
<span class="progress-count">@CurrentPlan.CompletedCount / @CurrentPlan.TotalCount completed</span>
</div>
}
</div>
<div class="plan-steps">
@foreach (var step in CurrentPlan.Steps)
{
<div class="plan-step @(step.IsCompleted ? "step-completed" : "step-pending")">
<span class="checkmark @(step.IsCompleted ? "checkmark-completed" : "")">
@if (step.IsCompleted)
{
<span class="check-icon">✓</span>
}
</span>
<span class="step-description @(step.IsCompleted ? "description-completed" : "")">@step.Description</span>
<span class="step-status @(step.IsCompleted ? "status-completed" : "status-pending")">
@(step.IsCompleted ? "Done" : "Pending")
</span>
</div>
}
</div>
</div>
}
else if (WasRejected)
{
<div class="plan-card plan-rejected">
<div class="plan-header">
<h3>Plan Rejected</h3>
<span class="rejection-badge">✗ Cancelled</span>
</div>
</div>
}
@code {
[CascadingParameter]
public InvocationContext Invocation { get; set; } = default!;
[CascadingParameter]
public MessageListContext? MessageListContext { get; set; }
[CascadingParameter]
public IAgentBoundaryContext? BoundaryContext { get; set; }
private Plan? CurrentPlan;
private HashSet<int> selectedSteps = new();
private bool AwaitingConfirmation;
private bool WasRejected;
private bool IsWaitingForPlan => CurrentPlan is null && !WasRejected;
private ResponseUpdateSubscription? _responseSubscription;
private string? _confirmPlanCallId;
protected override void OnInitialized()
{
// The plan comes from the create_plan function result
if (Invocation?.HasResult == true)
{
TryParsePlanFromResult();
}
else if (Invocation is not null)
{
// Subscribe to wait for the result
Invocation.ResultArrived += OnCreatePlanResultArrived;
}
// Subscribe to response updates to detect confirm_plan and update_plan_step calls
if (BoundaryContext is not null)
{
_responseSubscription = BoundaryContext.SubscribeToResponseUpdates(OnResponseUpdate);
}
}
private void OnCreatePlanResultArrived()
{
TryParsePlanFromResult();
InvokeAsync(StateHasChanged);
}
private void TryParsePlanFromResult()
{
var plan = Invocation?.GetResult<Plan>();
if (plan is not null)
{
CurrentPlan = plan;
InitializeSelectedSteps();
}
}
private void InitializeSelectedSteps()
{
// Select all pending steps by default
if (CurrentPlan is not null)
{
selectedSteps.Clear();
for (int i = 0; i < CurrentPlan.Steps.Count; i++)
{
if (!CurrentPlan.Steps[i].IsCompleted)
{
selectedSteps.Add(i);
}
}
}
}
private void OnResponseUpdate()
{
// Check the current update for tool calls
var update = BoundaryContext?.CurrentUpdate;
if (update is null)
{
return;
}
if (update.Contents is not null)
{
foreach (var content in update.Contents)
{
if (content is FunctionCallContent call)
{
if (string.Equals(call.Name, "confirm_plan", StringComparison.OrdinalIgnoreCase))
{
HandleConfirmPlanCall(call);
}
else if (string.Equals(call.Name, "update_plan_step", StringComparison.OrdinalIgnoreCase))
{
HandleUpdatePlanStepCall(call);
}
}
}
}
InvokeAsync(StateHasChanged);
}
private void HandleConfirmPlanCall(FunctionCallContent call)
{
// When confirm_plan is called, show the confirmation UI
if (!AwaitingConfirmation && !WasRejected && _confirmPlanCallId is null)
{
_confirmPlanCallId = call.CallId;
AwaitingConfirmation = true;
}
}
private void HandleUpdatePlanStepCall(FunctionCallContent call)
{
if (CurrentPlan is null)
{
return;
}
// Get the index and status from arguments
if (call.Arguments is null)
{
return;
}
int? index = null;
string? status = null;
string? description = null;
if (call.Arguments.TryGetValue("index", out var indexObj))
{
if (indexObj is int i) index = i;
else if (indexObj is long l) index = (int)l;
else if (indexObj is JsonElement je && je.ValueKind == JsonValueKind.Number) index = je.GetInt32();
}
if (call.Arguments.TryGetValue("status", out var statusObj))
{
if (statusObj is string s) status = s;
else if (statusObj is JsonElement je && je.ValueKind == JsonValueKind.String) status = je.GetString();
}
if (call.Arguments.TryGetValue("description", out var descObj))
{
if (descObj is string s) description = s;
else if (descObj is JsonElement je && je.ValueKind == JsonValueKind.String) description = je.GetString();
}
// Apply the update
if (index.HasValue && index.Value >= 0 && index.Value < CurrentPlan.Steps.Count)
{
if (status is not null)
{
CurrentPlan.Steps[index.Value].Status = status.ToLowerInvariant();
}
if (description is not null)
{
CurrentPlan.Steps[index.Value].Description = description;
}
}
}
private void ToggleStepSelection(int index)
{
if (selectedSteps.Contains(index))
{
selectedSteps.Remove(index);
}
else
{
selectedSteps.Add(index);
}
StateHasChanged();
}
private void ConfirmPlan()
{
AwaitingConfirmation = false;
var result = new PlanConfirmationResult
{
Confirmed = true,
SelectedStepIndices = selectedSteps.ToList()
};
BoundaryContext?.ProvideResponse("confirm_plan", result);
}
private void RejectPlan()
{
WasRejected = true;
AwaitingConfirmation = false;
var result = new PlanConfirmationResult
{
Confirmed = false,
SelectedStepIndices = []
};
BoundaryContext?.ProvideResponse("confirm_plan", result);
}
public void Dispose()
{
if (Invocation is not null)
{
Invocation.ResultArrived -= OnCreatePlanResultArrived;
}
_responseSubscription?.Dispose();
}
}
@@ -1,244 +0,0 @@
.plan-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 20px;
color: white;
max-width: 420px;
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.4);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.plan-card.plan-active {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.plan-card.plan-completed {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.plan-card.plan-rejected {
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
}
.plan-card.plan-loading {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.plan-header h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
}
.plan-progress .progress-count {
font-size: 0.85rem;
opacity: 0.9;
}
.completion-badge,
.rejection-badge {
font-size: 0.9rem;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
}
.completion-badge {
background: rgba(255, 255, 255, 0.25);
}
.rejection-badge {
background: rgba(255, 255, 255, 0.25);
}
.plan-steps {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.plan-step {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
transition: all 0.2s ease;
}
.plan-step.step-selected {
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
}
.plan-step.step-completed {
background: rgba(255, 255, 255, 0.1);
}
.step-checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
}
.step-checkbox {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkmark {
width: 24px;
height: 24px;
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
transition: all 0.2s ease;
flex-shrink: 0;
}
.step-checkbox:checked + .checkmark {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
.checkmark-completed {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
.check-icon {
font-size: 14px;
font-weight: bold;
}
.step-description {
flex: 1;
font-size: 0.95rem;
line-height: 1.4;
}
.description-completed {
opacity: 0.7;
text-decoration: line-through;
}
.step-status {
font-size: 0.75rem;
padding: 3px 8px;
border-radius: 6px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-pending {
background: rgba(255, 255, 255, 0.2);
}
.status-completed {
background: rgba(255, 255, 255, 0.3);
}
.plan-actions {
display: flex;
gap: 12px;
margin-top: 16px;
}
.plan-button {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.plan-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.plan-button-confirm {
background: rgba(255, 255, 255, 0.25);
color: white;
}
.plan-button-confirm:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.35);
transform: translateY(-1px);
}
.plan-button-reject {
background: rgba(0, 0, 0, 0.15);
color: white;
}
.plan-button-reject:hover {
background: rgba(0, 0, 0, 0.25);
transform: translateY(-1px);
}
.plan-button-skeleton {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.5);
}
/* Loading/Skeleton state */
.skeleton-text {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
display: inline-block;
}
.skeleton-progress {
width: 100px;
height: 1rem;
}
.skeleton-step {
padding: 14px 12px;
}
.skeleton-checkbox {
width: 24px;
height: 24px;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 6px;
flex-shrink: 0;
}
.skeleton-description {
width: 200px;
height: 1rem;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@@ -1,12 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
/// <summary>
/// Result of user's plan confirmation decision.
/// </summary>
public class PlanConfirmationResult
{
public bool Confirmed { get; set; }
public List<int> SelectedStepIndices { get; set; } = [];
}
@@ -1,87 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.RegularExpressions;
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
/// <summary>
/// Applies JSON Patch operations to a Plan.
/// Uses hardcoded path parsing for the expected paths:
/// - /steps/{index}/status
/// - /steps/{index}/description
/// </summary>
public static partial class PlanPatcher
{
// Regex to match paths like /steps/0/status or /steps/1/description
[GeneratedRegex(@"^/steps/(\d+)/(status|description)$")]
private static partial Regex StepPropertyPathRegex();
/// <summary>
/// Applies a JSON Patch operation to the plan.
/// Only supports "replace" operations on /steps/{index}/status and /steps/{index}/description paths.
/// </summary>
/// <param name="plan">The plan to modify.</param>
/// <param name="operation">The patch operation to apply.</param>
/// <returns>True if the operation was applied successfully, false otherwise.</returns>
public static bool ApplyPatch(Plan plan, JsonPatchOperation operation)
{
ArgumentNullException.ThrowIfNull(plan);
ArgumentNullException.ThrowIfNull(operation);
// Only support "replace" operations
if (!string.Equals(operation.Op, "replace", StringComparison.OrdinalIgnoreCase))
{
return false;
}
var match = StepPropertyPathRegex().Match(operation.Path);
if (!match.Success)
{
return false;
}
if (!int.TryParse(match.Groups[1].Value, out int stepIndex))
{
return false;
}
if (stepIndex < 0 || stepIndex >= plan.Steps.Count)
{
return false;
}
var propertyName = match.Groups[2].Value;
var step = plan.Steps[stepIndex];
switch (propertyName.ToUpperInvariant())
{
case "STATUS":
step.Status = operation.Value?.ToString() ?? "pending";
return true;
case "DESCRIPTION":
step.Description = operation.Value?.ToString() ?? string.Empty;
return true;
default:
return false;
}
}
/// <summary>
/// Applies multiple JSON Patch operations to the plan.
/// </summary>
/// <param name="plan">The plan to modify.</param>
/// <param name="operations">The patch operations to apply.</param>
/// <returns>The number of operations successfully applied.</returns>
public static int ApplyPatches(Plan plan, IEnumerable<JsonPatchOperation> operations)
{
int appliedCount = 0;
foreach (var operation in operations)
{
if (ApplyPatch(plan, operation))
{
appliedCount++;
}
}
return appliedCount;
}
}
@@ -1,29 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
/// <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,14 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
namespace AGUIDojoClient.Components.Demos.PredictiveStateUpdates;
/// <summary>
/// Represents the result of the confirm_changes frontend tool.
/// </summary>
public sealed class ConfirmChangesResult
{
/// <summary>
/// Gets or sets a value indicating whether the user confirmed the changes.
/// </summary>
public bool Confirmed { get; set; }
}
@@ -1,18 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoClient.Components.Demos.PredictiveStateUpdates;
/// <summary>
/// Represents the document state for the Predictive State Updates demo.
/// This model mirrors the server-side DocumentState and is updated via streaming state updates.
/// </summary>
public sealed class DocumentState
{
/// <summary>
/// Gets or sets the document content in Markdown format.
/// </summary>
[JsonPropertyName("document")]
public string Document { get; set; } = string.Empty;
}
@@ -1,249 +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
@using System.ComponentModel
@implements IDisposable
@inject IServiceProvider ServiceProvider
<PageTitle>AI Document Editor</PageTitle>
<div class="predictive-state-layout">
@* Left panel: Document editor *@
<div class="document-panel">
<div class="document-header">
<h2>Document Editor</h2>
@if (isStreaming)
{
<span class="streaming-indicator">
<span class="streaming-dot"></span>
Writing...
</span>
}
</div>
<div class="document-content">
@if (string.IsNullOrWhiteSpace(currentDocument))
{
<div class="document-placeholder">
Write whatever you want here in Markdown format...
</div>
}
else
{
<pre class="document-text">@currentDocument</pre>
}
</div>
</div>
@* Right panel: Chat sidebar *@
<div class="chat-panel">
<div class="chat-header">
<div class="chat-title">AI Document Editor</div>
<button class="new-chat-button" @onclick="ResetConversation">
<span class="button-icon">+</span> New chat
</button>
</div>
<AgentBoundary Agent="@agent" OnContextCreated="OnContextCreated">
<div class="chat-content">
<Messages>
<ContentTemplates>
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
</div>
<AgentState TState="DocumentState"
CurrentState="@currentDocumentState"
OnSnapshot="@DeserializeDocumentState"
CurrentStateChanged="@OnDocumentStateChanged" />
<div class="chat-input-container">
<AgentSuggestions Suggestions="@suggestions" />
<AgentInput Placeholder="Ask the AI to write or edit..." />
</div>
</AgentBoundary>
</div>
</div>
@* Confirmation modal *@
@if (awaitingConfirmation)
{
<div class="confirmation-overlay">
<div class="confirmation-modal">
<h3>Confirm Changes</h3>
<p>Do you want to accept the changes?</p>
<div class="confirmation-actions">
<button class="confirm-button reject" @onclick="RejectChanges">
Reject
</button>
<button class="confirm-button accept" @onclick="ConfirmChanges">
Confirm
</button>
</div>
</div>
</div>
}
@code {
private AIAgent? agent;
private IAgentBoundaryContext? boundaryContext;
private ResponseUpdateSubscription? responseSubscription;
private DocumentState? currentDocumentState;
private string currentDocument = string.Empty;
private string previousDocument = string.Empty;
private bool isStreaming;
private bool awaitingConfirmation;
private Suggestion[] suggestions = [
new Suggestion("Write a pirate story", new ChatMessage(ChatRole.User, "Please write a story about a pirate named Candy Beard")),
new Suggestion("Write a mermaid story", new ChatMessage(ChatRole.User, "Please write a story about a mermaid named Pearl")),
new Suggestion("Add character", new ChatMessage(ChatRole.User, "Add a new character to the story"))
];
[Parameter]
public string ScenarioId { get; set; } = "predictive_state_updates";
protected override void OnInitialized()
{
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("predictive-state-updates");
}
private void OnContextCreated(IAgentBoundaryContext context)
{
boundaryContext = context;
// Register the confirm_changes frontend tool
var confirmChangesTool = AIFunctionFactory.Create(
() => ConfirmChangesAsync(context),
"confirm_changes",
"Ask the user to confirm or reject the document changes.");
context.RegisterTools(confirmChangesTool);
// Subscribe to response updates to detect when streaming starts/stops
responseSubscription = context.SubscribeToResponseUpdates(OnResponseUpdate);
}
private void OnResponseUpdate()
{
var update = boundaryContext?.CurrentUpdate;
if (update is null)
{
return;
}
// Check for function calls to detect confirm_changes
if (update.Contents is not null)
{
foreach (var content in update.Contents)
{
if (content is FunctionCallContent call &&
string.Equals(call.Name, "confirm_changes", StringComparison.OrdinalIgnoreCase))
{
// Show confirmation dialog when confirm_changes is called
awaitingConfirmation = true;
isStreaming = false;
}
}
}
InvokeAsync(StateHasChanged);
}
private DocumentState? DeserializeDocumentState(ReadOnlyMemory<byte> data)
{
try
{
return JsonSerializer.Deserialize<DocumentState>(data.Span);
}
catch
{
return null;
}
}
private void OnDocumentStateChanged(DocumentState? state)
{
if (state is null)
{
return;
}
currentDocumentState = state;
// Store the previous document before updating (for reject functionality)
if (string.IsNullOrEmpty(previousDocument) && !string.IsNullOrEmpty(currentDocument))
{
previousDocument = currentDocument;
}
// Check if we're starting to stream (document changed)
if (currentDocument != state.Document)
{
isStreaming = true;
}
currentDocument = state.Document;
StateHasChanged();
}
/// <summary>
/// Frontend tool that waits for user confirmation via the UI.
/// </summary>
[Description("Ask the user to confirm or reject the document changes.")]
private static async Task<ConfirmChangesResult> ConfirmChangesAsync(IAgentBoundaryContext context)
{
// Wait for the user to click Confirm or Reject
var response = await context.WaitForResponse("confirm_changes");
return (ConfirmChangesResult)response;
}
private void ConfirmChanges()
{
awaitingConfirmation = false;
isStreaming = false;
// Update the previous document to the current one (changes accepted)
previousDocument = currentDocument;
boundaryContext?.ProvideResponse("confirm_changes", new ConfirmChangesResult { Confirmed = true });
StateHasChanged();
}
private void RejectChanges()
{
awaitingConfirmation = false;
isStreaming = false;
// Revert to the previous document
currentDocument = previousDocument;
boundaryContext?.ProvideResponse("confirm_changes", new ConfirmChangesResult { Confirmed = false });
StateHasChanged();
}
private void ResetConversation()
{
currentDocument = string.Empty;
previousDocument = string.Empty;
currentDocumentState = null;
isStreaming = false;
awaitingConfirmation = false;
StateHasChanged();
}
public void Dispose()
{
responseSubscription?.Dispose();
}
}

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