Compare commits

...
Author SHA1 Message Date
Dmytro StrukandGitHub aba3076203 Updated package versions (#2208) 2025-11-14 01:21:29 +00:00
Dmytro StrukandGitHub 8458b4ade7 Agent name as required for AzureAIClient (#2198) 2025-11-14 01:17:50 +00:00
e516322918 Python: Fix Readme and samples for AzureFunctions (#2197)
* Fix REadme and samples

* Update instanceId placeholder in demo.http

* Update python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/README.md

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-14 01:15:16 +00:00
b19860b8a8 .NET: Implement Purview middleware in dotnet (#1949)
* Move Purview integration logic into middleware

* Improve error handling and user id management

* Rename purview package

* Handle 402s more explicitly; add Middleware generation methods; don't ignore exceptions

* Use DI container; pass scope id to PC

* Add protection scope caching

* Wrap more exceptions in PurviewClient

* Remove block check dedup; add tests

* Refactor PurviewWrapper intialization; Add unit tests

* Use different .Use method and add IDisposable stub

* Add background job processing for Purview

* Misc comment cleanup

* Apply copilot comments

* Fix formatting

* Formatting other files to fix pipeline

* Small updates to settings and exceptions

* Add README

* Move Purview sample

* Address review comments and update XML comments

* Newline after namespace

* Move public Purview classes to single namespace; Clean up csproj and slnx

* Commit the renames

* Remove unused openAI dependency

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2025-11-14 00:50:08 +00:00
Evan MattsonandGitHub a75590eb9b Python: ChatKit sample fixes (#2174)
* sample fixes

* Update thread naming
2025-11-13 23:02:31 +00:00
15d0bda8a2 Python: Added an Azure OpenAI Responses API Hosted MCP sample (#2108)
* Add files via upload

* Update python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py

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

* Updated README.md

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 22:44:49 +00:00
Giles OdigweandGitHub f04f5ef297 Python: Added Samples for Bing Grounding and Custom Search (#2200)
* bing grounding and custom search samples

* readme
2025-11-13 21:57:16 +00:00
Giles OdigweandGitHub 21dceca482 Python: Enhance Azure AI Search Citations with Complete URL Information (#2066)
* add get_url to raw rep for absolute path url

* fixes

* add real url to citation annotation

* small fix

* project client + openapi fix

* openapi sample revert

* tool call list fix
2025-11-13 19:23:11 +00:00
SergeyMenshykhandGitHub 6d890e46ed suppress the MEAI001 and OPENAI001 errors that appear when building the catalog sample as a standalone project. (#2191) 2025-11-13 18:48:50 +00:00
Shyju KrishnankuttyandGitHub de2abdf573 Updated the Azure Functions samples to use the latest stable Azure Functions Worker packages. (#2189) 2025-11-13 17:38:42 +00:00
westeyandGitHub f273ca7353 Fix InMemoryChatMessageStore serialization bug. (#2185) 2025-11-13 15:43:01 +00:00
Victor DibiaandGitHub 7e5de8f920 Python: Fix HIL regression (#2167)
* fix devui regression from #2021 where all input is stringified but devui HIL input does not handle stringified json strings correctly.

* update incorrect test

* add devui hil input tests
2025-11-13 05:26:24 +00:00
Evan MattsonandGitHub e92dcb3d5d Python: fix tool call id mismatch in ag-ui (#2166)
* Fix state for pending requests bug

* Bump ver

* Update changelog
2025-11-13 04:57:59 +00:00
Dmytro StrukandGitHub 3c874d0073 Python: Updated package versions (#2165)
* Updated package versions

* Reverted package version update for ag-ui

* Updated changelog file
2025-11-13 04:23:57 +00:00
665aacf1ad Fix chat middleware: add streaming support, terminate flag, and check only last message (#2120)
This commit fixes three issues in the security_filter_middleware:

1. Missing context.terminate flag - Without this, middleware continues processing after setting blocked response
2. No streaming support - When context.is_streaming is True, middleware now returns async generator with ChatResponseUpdate
3. Checks all messages - Changed to check only context.messages[-1] (most recent user message) instead of iterating through conversation history

Changes:
- Added AsyncIterable import
- Added ChatResponseUpdate and TextContent imports
- Modified security_filter_middleware to handle both streaming and non-streaming modes
- Added context.terminate = True to properly stop execution
- Changed message checking logic to only inspect the last user message

Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2025-11-13 04:20:44 +00:00
Dmytro StrukandGitHub a4e82f4e04 Updated package version (#2164) 2025-11-13 02:53:31 +00:00
67a8147151 .NET: Python: Azure Functions feature branch (#1916)
* Python: Add Scaffolding for Durable AzureFunctions package to Agent Framework (#1823)

* Add scafolding

* update readme

* add code owners and label

* update owners

* .NET: Durable extension: initial src and unit tests (#1900)

* Python: Add Durable Agent Wrapper code (#1913)

* add initial changes

* Move code and add single sample

* Update logger

* Remove unused code

* address PR comments

* cleanup code and address comments

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Azure Functions .NET samples (#1939)

* Python: Add Unit tests for Azurefunctions package (#1976)

* Add Unit tests for Azurefunctions

* remove duplicate import

* .NET: [Feature Branch] Migrate state schema updates and support for agents as MCP tools (#1979)

* Python: Add more samples for Azure Functions (#1980)

* Move all samples

* fix comments

* remove dead lines

* Make samples simpler

* .NET: [Feature Branch] Durable Task extension integration tests (#2017)

* .NET: [Feature Branch] Update OpenAI config for integration tests (#2063)

* Python: Add Integration tests for AzureFunctions  (#2020)

* Add Integration tests

* Remove DTS extension

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Add pyi file for type safety

* Add samples in readme

* Updated all readme instructions

* Address comments

* Update readmes

* Fix requirements

* Address comments

---------

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

* .NET: [Feature Branch] Update dotnet-build-and-test.yml to support integration tests (#2070)

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

* Fix DTS startup issue and improve logging (#2103)

* .NET: [Feature Branch] Introduce Azure OpenAI config for .NET pipeline (#2106)

Also fixes an issue where we were trying to start docker containers for integration tests on Windows, which doesn't work.

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

* Fix uv.lock after merge

* Python: Add README for Azure Functions samples setup (#2100)

* Add README for Azure Functions samples setup

Added setup instructions for Azure Functions samples, including environment setup, virtual environment creation, and running samples.

* Update python/samples/getting_started/azure_functions/README.md

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

* Apply suggestions from code review

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

* Apply suggestion from @Copilot

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

* Apply suggestions from code review

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

---------

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

* Fix or remove broken markdown file links (#2115)

* .NET: [Feature Branch] Update HTTP API to be consistent across languages (#2118)

* Python: Fix AzureFunctions Integration Tests (#2116)

* Add Identity Auth to samples

* Update python/samples/getting_started/azure_functions/README.md

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

* Update python/samples/getting_started/azure_functions/01_single_agent/function_app.py

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

* Update python/samples/getting_started/azure_functions/02_multi_agent/function_app.py

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

* Update python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/README.md

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

---------

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

* Python: Fix Http Schema (#2112)

* Rename to threadid

* Respond in plain text

* Make snake-case

* Add http prefix

* rename to wait-for-response

* Add query param check

* address comments

* .NET: Remove IsPackable=false in preparation for nuget release (#2142)

* Python: Move `azurefunctions` to `azure` for import (#2141)

* Move import to Azure

* fix mypy

* Update python/packages/azurefunctions/README.md

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

* Add missing types

* Address comments

---------

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

* Update python/packages/azurefunctions/pyproject.toml

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

* Update python/packages/azurefunctions/agent_framework_azurefunctions/__init__.py

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

* Fix imports

* Address PR feedback from westey-m (#2150)

- Adds a link from the /dotnet/samples/README.md to /dotnet/samples/AzureFunctions
- Make DurableAgentThread deserialization internal for future-proofing
- Update JSON serialization logic to address recently discovered issues with source generator serialization

* Address comments (#2160)

---------

Co-authored-by: Laveesh Rohra <larohra@microsoft.com>
Co-authored-by: Chris Gillum <cgillum@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Anirudh Garg <anirudhg@microsoft.com>
2025-11-13 02:00:53 +00:00
Evan MattsonandGitHub 5537b1da79 Python: Fix ag-ui regressions (#2114)
* Bump ag-ui package version. Update CHANGELOG

* Fix ag-ui bugs

* Revert port test change

* Cleanup

* Intro factory funcs for samples

* Revert package ver change
2025-11-13 00:18:24 +00:00
Tao ChenandGitHub 348ac764e6 Add samples for web search and image gen tools (#2155) 2025-11-12 23:58:23 +00:00
Dmytro StrukandGitHub e8243b7d11 Updated package versions (#2154) 2025-11-12 23:58:02 +00:00
Reuben BondandGitHub 4b0f724e62 .NET: Expose more agent metadata through DevUI discovery endpoint (#2138)
* Exposes more agent metadata through DevUI discovery endpoint

* Exposes more agent metadata through DevUI discovery endpoint

* pr feedback

* pr feedback

* Don't expose Workflows as agents to DevUI
2025-11-12 23:38:06 +00:00
+20
Dmytro StrukGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>ChrisCopilotkzucopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Reuben BondPeter IbekweJeff HandleyDaniel RothVictor DibiaMark WallaceShawn HenryJavier Calvarro NelsonEvan MattsonEduard van ValkenburgKorolev Dmitrywesteydependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Reuben BondTao ChenwuwengRoger BarretoSergeyMenshykhCopilotJacob AlberGiles OdigweDaniel Cazzulinodependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
edb367a2b9 Python: Azure AI client based on new azure-ai-projects package (#1910)
* Added changes (#1909)

* Python: [Feature Branch] Renamed Azure AI agent and small fixes (#1919)

* Renaming

* Small fixes

* Update python/packages/core/agent_framework/openai/_shared.py

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

---------

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

* Small fix

* Python: [Feature Branch] Added use_latest_version parameter to AzureAIClient (#1959)

* Added use_latest_version parameter to AzureAIClient

* Added unit tests

* Update python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py

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

* Update python/packages/azure-ai/agent_framework_azure_ai/_client.py

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

---------

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

* Python: [Feature Branch] Structured Outputs and more examples for AzureAIClient (#1987)

* Small updates

* Added support for structured outputs

* Added code interpreter example

* More examples and fixes

* Added more examples and README

* Small fix

* Addressed PR feedback

* Removed optional ID from FunctionResultContent (#2011)

* Added hosted MCP support (#2018)

* Python: [Feature Branch] Fixed "store" parameter handling (#2069)

* Fixed store parameter handling

* Small fix

* Python: [Feature Branch] Added more examples and fixes for Azure AI agent (#2077)

* Updated azure-ai-projects package version

* Added an example of hosted MCP with approval required

* Updated code interpreter example

* Added file search example

* Update python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py

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

* Update python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py

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

* Small fix

---------

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

* Added handling for conversation_id (#2098)

* Merge from main

* Revert "Merge from main"

This reverts commit b8206a85d7.

* Python: [Feature Branch] Merge from main to Azure AI branch (#2111)

* Do not build DevUI assets during .NET project build (#2010)

* .NET: Add unit tests for declarative executor SetMultipleVariables (#2016)

* Add unit tests for create conversation executor

* Update indentation and comment typo.

* Added unit tests for declarative executor SetMultipleVariablesExecutor

* Updated comments and syntactic sugar

* Python: DevUI: Use metadata.entity_id instead of model field (#1984)

* DevUI: Use metadata.entity_id for agent/workflow name instead of model field

* OpenAI Responses: add explicit request validation

* Review feedback

* .NET: DevUI - Do not automatically add/map OpenAI services/endpoints (#2014)

* Don't add OpenAIResponses as part of Dev UI

You should be able to add and remove Dev UI without impacting your other production endpoints.

* Remove `AddDevUI()` and do not map OpenAI endpoints from `MapDevUI()`

* Fix comment wording

* Revise documentation

---------

Co-authored-by: Daniel Roth <daroth@microsoft.com>

* Python: DevUI: Add OpenAI Responses API proxy support  + HIL for Workflows (#1737)

* DevUI: Add OpenAI Responses API proxy support with enhanced UI features

This commit adds support for proxying requests to OpenAI's Responses API,
allowing DevUI to route conversations to OpenAI models when configured to enable testing.

Backend changes:
- Add OpenAI proxy executor with conversation routing logic
- Enhance event mapper to support OpenAI Responses API format
- Extend server endpoints to handle OpenAI proxy mode
- Update models with OpenAI-specific response types
- Remove emojis from logging and CLI output for cleaner text

Frontend changes:
- Add settings modal with OpenAI proxy configuration UI
- Enhance agent and workflow views with improved state management
- Add new UI components (separator, switch) for settings
- Update debug panel with better event filtering
- Improve message renderers for OpenAI content types
- Update types and API client for OpenAI integration

* update ui, settings modal and workflow input form, add register cleanup hooks.

* add workflow HIL support, user mode, other fixes

* feat(devui): add human-in-the-loop (HIL) support with dynamic response schemas

Implement  HIL workflow support allowing workflows to pause for user input
with dynamically generated JSON schemas based on response handler type hints.

Key Features:
- Automatic response schema extraction from @response_handler decorators
- Dynamic form generation in UI based on Pydantic/dataclass response types
- Checkpoint-based conversation storage for HIL requests/responses
- Resume workflow execution after user provides HIL response

Backend Changes:
- Add extract_response_type_from_executor() to introspect response handlers
- Enrich RequestInfoEvent with response_schema via _enrich_request_info_event_with_response_schema()
- Map RequestInfoEvent to response.input.requested OpenAI event format
- Store HIL responses in conversation history and restore checkpoints

Frontend Changes:
- Add HILInputModal component with SchemaFormRenderer for dynamic forms
- Support Pydantic BaseModel and dataclass response types
- Render enum fields as dropdowns, strings as text/textarea, numbers, booleans, arrays, objects
- Display original request context alongside response form

Testing:
- Add  tests for checkpoint storage (test_checkpoints.py)
- Add schema generation tests for all input types (test_schema_generation.py)
- Validate end-to-end HIL flow with spam workflow sample

This enables workflows to seamlessly pause execution and request structured user input
with type-safe, validated forms generated automatically from response type annotations.

* improve HIL support, improve workflow execution view

* ui updates

* ui updates

* improve HIL for workflows, add auth and view modes

* update workflow

* security improvements , ui fixes

* fix mypy error

* update loading spinner in ui

---------

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

* .NET: Remove launchSettings.json from .gitignore in dotnet/samples (#2006)

* Remove launchSettings.json from .gitignore in dotnet/samples

* Update dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Properties/launchSettings.json

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

* Update dotnet/samples/AGUIClientServer/AGUIServer/Properties/launchSettings.json

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

---------

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

* DevUI: Serialize workflow input as string to maintain conformance with OpenAI Responses format (#2021)

Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>

* Add Microsoft Agent Framework logo to assets (#2007)

* Updated package versions (#2027)

* DevUI: Prevent line breaks within words in the agent view (#2024)

Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>

* .NET [AG-UI]: Adds support for shared state. (#1996)

* Product changes

* Tests

* Dojo project

* Cleanups

* Python: Fix underlying tool choice bug and all for return to previous Handoff subagent (#2037)

* Fix tool_choice override bug and add enable_return_to_previous support

* Add unit test for handoff checkpointing

* Handle tools when we have them

* added missing chatAgent params (#2044)

* .NET: fix ChatCompletions Tools serialization (#2043)

* fix serialization in chat completions on tools

* nit

* .NET: assign AgentCard's URL to mapped-endpoint if not defined explicitly (#2047)

* fix serialization in chat completions on tools

* nit

* write e2e test for agent card resolve + adjust behavior

* nit

* Version 1.0.0-preview.251110.1 (#2048)

* .NET: Remove moved OpenAPI sample and point to SK one. (#1997)

* Remove moved OpenAPI sample and point to SK one.

* Update dotnet/samples/GettingStarted/Agents/README.md

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

---------

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

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.2 to 4.0.4.6 (#2031)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.6
  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>

* .NET: Separate all memory and rag samples into their own folders (#2000)

* Separate all memory and rag samples into their own folders

* Fix broken link.

* Python: .Net: Dotnet devui compatibility fixes (#2026)

* DevUI: Add OpenAI Responses API proxy support with enhanced UI features

This commit adds support for proxying requests to OpenAI's Responses API,
allowing DevUI to route conversations to OpenAI models when configured to enable testing.

Backend changes:
- Add OpenAI proxy executor with conversation routing logic
- Enhance event mapper to support OpenAI Responses API format
- Extend server endpoints to handle OpenAI proxy mode
- Update models with OpenAI-specific response types
- Remove emojis from logging and CLI output for cleaner text

Frontend changes:
- Add settings modal with OpenAI proxy configuration UI
- Enhance agent and workflow views with improved state management
- Add new UI components (separator, switch) for settings
- Update debug panel with better event filtering
- Improve message renderers for OpenAI content types
- Update types and API client for OpenAI integration

* update ui, settings modal and workflow input form, add register cleanup hooks.

* add workflow HIL support, user mode, other fixes

* feat(devui): add human-in-the-loop (HIL) support with dynamic response schemas

Implement  HIL workflow support allowing workflows to pause for user input
with dynamically generated JSON schemas based on response handler type hints.

Key Features:
- Automatic response schema extraction from @response_handler decorators
- Dynamic form generation in UI based on Pydantic/dataclass response types
- Checkpoint-based conversation storage for HIL requests/responses
- Resume workflow execution after user provides HIL response

Backend Changes:
- Add extract_response_type_from_executor() to introspect response handlers
- Enrich RequestInfoEvent with response_schema via _enrich_request_info_event_with_response_schema()
- Map RequestInfoEvent to response.input.requested OpenAI event format
- Store HIL responses in conversation history and restore checkpoints

Frontend Changes:
- Add HILInputModal component with SchemaFormRenderer for dynamic forms
- Support Pydantic BaseModel and dataclass response types
- Render enum fields as dropdowns, strings as text/textarea, numbers, booleans, arrays, objects
- Display original request context alongside response form

Testing:
- Add  tests for checkpoint storage (test_checkpoints.py)
- Add schema generation tests for all input types (test_schema_generation.py)
- Validate end-to-end HIL flow with spam workflow sample

This enables workflows to seamlessly pause execution and request structured user input
with type-safe, validated forms generated automatically from response type annotations.

* improve HIL support, improve workflow execution view

* ui updates

* ui updates

* improve HIL for workflows, add auth and view modes

* update workflow

* security improvements , ui fixes

* fix mypy error

* update loading spinner in ui

* DevUI: Serialize workflow input as string to maintain conformance with OpenAI Responses format

* Phase 1: Add /meta endpoint and fix workflow event naming for .NET DevUI compatibility

* additional fixes for .NET DevUI workflow visualization item ID tracking

**Problem:**
.NET DevUI was generating different item IDs for ExecutorInvokedEvent and
ExecutorCompletedEvent, causing only the first executor to highlight in the
workflow graph. Long executor names and error messages also broke UI layout.

**Changes:**
- Add ExecutorActionItemResource to match Python DevUI implementation
- Track item IDs per executor using dictionary in AgentRunResponseUpdateExtensions
- Reuse same item ID across invoked/completed/failed events for proper pairing
- Add truncateText() utility to workflow-utils.ts
- Truncate executor names to 35 chars in execution timeline
- Truncate error messages to 150 chars in workflow graph nodes

** Details:**
- ExecutorActionItemResource registered with JSON source generation context
- Dictionary cleaned up after executor completion/failure to prevent memory leaks
- Frontend item tracking by unique item.id supports multiple executor runs
- All changes follow existing codebase patterns and conventions

Tested with review-workflow showing correct executor highlighting and state
transitions for sequential and concurrent executors.

* format fixes, remove cors tests

* remove unecessary attributes

---------

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Reuben Bond <reuben.bond@gmail.com>

* DevUI: support having both an agent and a workflow with the same id in discovery (#2023)

* Python: Fix Model ID attribute not showing up in `invoke_agent` span (#2061)

* Best effort to surface the model id to invoke agent span

* Fix tests

* Fix tests

* Version 1.0.0-preview.251107.2 (#2065)

* Version 1.0.0-preview.251110.2 (#2067)

* Update README.md to change Grafana links to Azure portal links for dashboard access (#1983)

* .NET - Enable build & test on branch `feature-foundry-agents` (#2068)

* Tests good, mkay

* Update .github/workflows/dotnet-build-and-test.yml

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

* Enable feature build pipelines

---------

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

* Python: Add concrete AGUIChatClient (#2072)

* Add concrete AGUIChatClient

* Update logging docstrings and conventions

* PR feedback

* Updates to support client-side tool calls

* .NET: Move catalog samples to the HostedAgents folder (#2090)

* move catalog samples to the HostedAgents folder

* move the catalog samples' projects to the HostedAgents folder

* Bump OpenTelemetry.Instrumentation.Runtime from 1.12.0 to 1.13.0 (#1856)

---
updated-dependencies:
- dependency-name: OpenTelemetry.Instrumentation.Runtime
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* .NET: Bump Microsoft.SemanticKernel.Agents.Abstractions from 1.66.0 to 1.67.0 (#1962)

* Bump Microsoft.SemanticKernel.Agents.Abstractions from 1.66.0 to 1.67.0

---
updated-dependencies:
- dependency-name: Microsoft.SemanticKernel.Agents.Abstractions
  dependency-version: 1.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* .NET: Bump all Microsoft.SemanticKernel packages from 1.66.* to 1.67.* (#1969)

* Initial plan

* Update all Microsoft.SemanticKernel packages to 1.67.*

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

* Remove unrelated changes to package-lock.json and yarn.lock

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

---------

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: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>

---------

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

* .NET: fix: WorkflowAsAgent Sample (#1787)

* fix: WorkflowAsAgent Sample

* Also makes ChatForwardingExecutor public

* feat: Expand ChatForwardingExecutor handled types

Make ChatForwardingExecutor match the input types of ChatProtocolExecutor.

* fix: Update for the new AgentRunResponseUpdate merge logic

AIAgent always sends out List<ChatMessage> now.

* Updated (#2076)

* Bump vite in /python/samples/demos/chatkit-integration/frontend (#1918)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.1.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.12
  dependency-type: direct:development
...

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

* Bump Roslynator.Analyzers from 4.14.0 to 4.14.1 (#1857)

---
updated-dependencies:
- dependency-name: Roslynator.Analyzers
  dependency-version: 4.14.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Bump MishaKav/pytest-coverage-comment from 1.1.57 to 1.1.59 (#2034)

Bumps [MishaKav/pytest-coverage-comment](https://github.com/mishakav/pytest-coverage-comment) from 1.1.57 to 1.1.59.
- [Release notes](https://github.com/mishakav/pytest-coverage-comment/releases)
- [Changelog](https://github.com/MishaKav/pytest-coverage-comment/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mishakav/pytest-coverage-comment/compare/v1.1.57...v1.1.59)

---
updated-dependencies:
- dependency-name: MishaKav/pytest-coverage-comment
  dependency-version: 1.1.59
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Python: Handle agent user input request in AgentExecutor (#2022)

* Handle agent user input request in AgentExecutor

* fix test

* Address comments

* Fix tests

* Fix tests

* Address comments

* Address comments

* Python: OpenAI Responses Image Generation Stream Support, Sample and Unit Tests (#1853)

* support for image gen streaming

* small fixes

* fixes

* added comment

* Python: Fix MCP Tool Parameter Descriptions Not Propagated to LLMs (#1978)

* mcp tool description fix

* small fix

* .NET: Allow extending agent run options via additional properties (#1872)

* Allow extending agent run options via additional properties

This mirrors the M.E.AI model in ChatOptions.AdditionalProperties which is very useful when building functionality pipelines.

Fixes https://github.com/microsoft/agent-framework/issues/1815

* Expand XML documentation

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

* Add AdditionalProperties tests to AgentRunOptions

Co-authored-by: kzu <169707+kzu@users.noreply.github.com>

---------

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

* Python: Use the last entry in the task history to avoid empty responses (#2101)

* Use the last entry in the task history to avoid empty responses

* History only contains Messages

* Updated package versions (#2104)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com>
Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com>
Co-authored-by: Daniel Roth <daroth@microsoft.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Shawn Henry <sphenry@gmail.com>
Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Reuben Bond <reuben.bond@gmail.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: wuweng <wuweng@microsoft.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Daniel Cazzulino <daniel@cazzulino.com>
Co-authored-by: kzu <169707+kzu@users.noreply.github.com>

* Updated azure-ai-projects package version and small fixes (#2139)

* Python: [Feature Branch] Resolve CI issues (#2143)

* Small documentation and code fixes

* Small fix in documentation

* Addressed PR feedback

* Added AI Search example

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com>
Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com>
Co-authored-by: Daniel Roth <daroth@microsoft.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Shawn Henry <sphenry@gmail.com>
Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Reuben Bond <reuben.bond@gmail.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: wuweng <wuweng@microsoft.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Daniel Cazzulino <daniel@cazzulino.com>
Co-authored-by: kzu <169707+kzu@users.noreply.github.com>
2025-11-12 23:20:53 +00:00
Giles OdigweandGitHub 4bffe1ebc8 update openai samples to use agents (#2012) 2025-11-12 22:58:41 +00:00
406a8560c6 .NET: Add AgentWithHostedMCP sample (#2129)
* add AgentWithHostedMCP sample

* Update dotnet/samples/HostedAgents/AgentWithHostedMCP/README.md

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

* Update dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj

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

* Update dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj

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

* Update dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj

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

* Use explicit type for mcpTool

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 19:52:05 +00:00
c1830c20c9 .NET: Add hosting capabilities to hosted agents (#2124)
* move catalog samples to the HostedAgents folder

* move the catalog samples' projects to the HostedAgents folder

* host agents, add dockerfile, add http for testing, and use explicit nuget packages

* add agents` manifests

* Update dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile

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

* Update dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile

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

* Update dotnet/samples/HostedAgents/AgentWithTextSearchRag/agent.yaml

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

* Update dotnet/samples/HostedAgents/AgentsInWorkflows/agent.yaml

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

* remove BOMs, decrease identation and remove unecessary `parameters` node.

* remove unnecessary analyzers and align the packages' versions

* remove end-of-line breaks in agents' descriptions, eliminated unnecessary double quotes around versions, and changed the descriptions to be more agent-centric rather than sample-centric

* use DefaultAzureCredential

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
2025-11-12 14:35:25 +00:00
Eduard van ValkenburgandGitHub ef60f38084 Python: fixed image handling in anthropic and added convenience method on datacontent (#2083)
* fixed image handling in anthropic and added convenience method on datacontent

* remove wheel path

* updated docstrings

* fixed image handling in anthropic and added convenience method on datacontent

* remove wheel path

* updated lock
2025-11-12 12:07:27 +00:00
Stephen ToubandGitHub c8bb3d1835 Update .NET packages to 10.0.0 (#2092) 2025-11-12 11:14:53 +00:00
SergeyMenshykhandGitHub caf3c4b92d .NET: Move deep research sample (#2094)
* move catalog samples to the HostedAgents folder

* move the catalog samples' projects to the HostedAgents folder

* move deep research sample out of the HostedAgents folder
2025-11-12 09:22:11 +00:00
Dmytro StrukandGitHub 863a632ccf Updated package versions (#2104) 2025-11-11 23:30:34 +00:00
Mark WallaceandGitHub 8d8c94b312 Python: Use the last entry in the task history to avoid empty responses (#2101)
* Use the last entry in the task history to avoid empty responses

* History only contains Messages
2025-11-11 22:56:53 +00:00
Daniel CazzulinoGitHubCopilotkzucopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
180f82373b .NET: Allow extending agent run options via additional properties (#1872)
* Allow extending agent run options via additional properties

This mirrors the M.E.AI model in ChatOptions.AdditionalProperties which is very useful when building functionality pipelines.

Fixes https://github.com/microsoft/agent-framework/issues/1815

* Expand XML documentation

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

* Add AdditionalProperties tests to AgentRunOptions

Co-authored-by: kzu <169707+kzu@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kzu <169707+kzu@users.noreply.github.com>
2025-11-11 20:00:59 +00:00
Giles OdigweandGitHub d59bd20765 Python: Fix MCP Tool Parameter Descriptions Not Propagated to LLMs (#1978)
* mcp tool description fix

* small fix
2025-11-11 19:43:26 +00:00
Giles OdigweandGitHub 1d7292fba6 Python: OpenAI Responses Image Generation Stream Support, Sample and Unit Tests (#1853)
* support for image gen streaming

* small fixes

* fixes

* added comment
2025-11-11 19:01:37 +00:00
Tao ChenandGitHub cd9073aa11 Python: Handle agent user input request in AgentExecutor (#2022)
* Handle agent user input request in AgentExecutor

* fix test

* Address comments

* Fix tests

* Fix tests

* Address comments

* Address comments
2025-11-11 18:59:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
5fd2a0c287 Bump MishaKav/pytest-coverage-comment from 1.1.57 to 1.1.59 (#2034)
Bumps [MishaKav/pytest-coverage-comment](https://github.com/mishakav/pytest-coverage-comment) from 1.1.57 to 1.1.59.
- [Release notes](https://github.com/mishakav/pytest-coverage-comment/releases)
- [Changelog](https://github.com/MishaKav/pytest-coverage-comment/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mishakav/pytest-coverage-comment/compare/v1.1.57...v1.1.59)

---
updated-dependencies:
- dependency-name: MishaKav/pytest-coverage-comment
  dependency-version: 1.1.59
  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-11-11 17:52:50 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b565b25b04 Bump Roslynator.Analyzers from 4.14.0 to 4.14.1 (#1857)
---
updated-dependencies:
- dependency-name: Roslynator.Analyzers
  dependency-version: 4.14.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-11-11 17:48:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
24298cd89e Bump vite in /python/samples/demos/chatkit-integration/frontend (#1918)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.1.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.12
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 17:33:52 +00:00
ChrisandGitHub ad2ebfc0c8 Updated (#2076) 2025-11-11 17:17:34 +00:00
Jacob AlberandGitHub a39e6561fd .NET: fix: WorkflowAsAgent Sample (#1787)
* fix: WorkflowAsAgent Sample

* Also makes ChatForwardingExecutor public

* feat: Expand ChatForwardingExecutor handled types

Make ChatForwardingExecutor match the input types of ChatProtocolExecutor.

* fix: Update for the new AgentRunResponseUpdate merge logic

AIAgent always sends out List<ChatMessage> now.
2025-11-11 16:35:22 +00:00
dependabot[bot]GitHubrogerbarretomarkwallace-microsoftcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Copilot
69dd532cd4 .NET: Bump Microsoft.SemanticKernel.Agents.Abstractions from 1.66.0 to 1.67.0 (#1962)
* Bump Microsoft.SemanticKernel.Agents.Abstractions from 1.66.0 to 1.67.0

---
updated-dependencies:
- dependency-name: Microsoft.SemanticKernel.Agents.Abstractions
  dependency-version: 1.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* .NET: Bump all Microsoft.SemanticKernel packages from 1.66.* to 1.67.* (#1969)

* Initial plan

* Update all Microsoft.SemanticKernel packages to 1.67.*

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

* Remove unrelated changes to package-lock.json and yarn.lock

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

---------

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: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>
2025-11-11 16:26:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cf7c9fce40 Bump OpenTelemetry.Instrumentation.Runtime from 1.12.0 to 1.13.0 (#1856)
---
updated-dependencies:
- dependency-name: OpenTelemetry.Instrumentation.Runtime
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 16:23:30 +00:00
SergeyMenshykhandGitHub 04b662543c .NET: Move catalog samples to the HostedAgents folder (#2090)
* move catalog samples to the HostedAgents folder

* move the catalog samples' projects to the HostedAgents folder
2025-11-11 15:40:58 +00:00
Evan MattsonandGitHub 32bd884bfd Python: Add concrete AGUIChatClient (#2072)
* Add concrete AGUIChatClient

* Update logging docstrings and conventions

* PR feedback

* Updates to support client-side tool calls
2025-11-11 14:39:30 +00:00
93ab43d788 .NET - Enable build & test on branch feature-foundry-agents (#2068)
* Tests good, mkay

* Update .github/workflows/dotnet-build-and-test.yml

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

* Enable feature build pipelines

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2025-11-11 10:53:35 +00:00
wuwengandGitHub 177b0c95be Update README.md to change Grafana links to Azure portal links for dashboard access (#1983) 2025-11-11 05:08:33 +00:00
Mark WallaceandGitHub 297d9d7fb3 Version 1.0.0-preview.251110.2 (#2067) 2025-11-11 00:23:01 +00:00
Mark WallaceandGitHub d3827e8c11 Version 1.0.0-preview.251107.2 (#2065) 2025-11-10 22:47:49 +00:00
Tao ChenandGitHub 448aff536a Python: Fix Model ID attribute not showing up in invoke_agent span (#2061)
* Best effort to surface the model id to invoke agent span

* Fix tests

* Fix tests
2025-11-10 22:32:04 +00:00
Reuben BondandGitHub 12fc19b360 DevUI: support having both an agent and a workflow with the same id in discovery (#2023) 2025-11-10 21:36:07 +00:00
7a45929807 Python: .Net: Dotnet devui compatibility fixes (#2026)
* DevUI: Add OpenAI Responses API proxy support with enhanced UI features

This commit adds support for proxying requests to OpenAI's Responses API,
allowing DevUI to route conversations to OpenAI models when configured to enable testing.

Backend changes:
- Add OpenAI proxy executor with conversation routing logic
- Enhance event mapper to support OpenAI Responses API format
- Extend server endpoints to handle OpenAI proxy mode
- Update models with OpenAI-specific response types
- Remove emojis from logging and CLI output for cleaner text

Frontend changes:
- Add settings modal with OpenAI proxy configuration UI
- Enhance agent and workflow views with improved state management
- Add new UI components (separator, switch) for settings
- Update debug panel with better event filtering
- Improve message renderers for OpenAI content types
- Update types and API client for OpenAI integration

* update ui, settings modal and workflow input form, add register cleanup hooks.

* add workflow HIL support, user mode, other fixes

* feat(devui): add human-in-the-loop (HIL) support with dynamic response schemas

Implement  HIL workflow support allowing workflows to pause for user input
with dynamically generated JSON schemas based on response handler type hints.

Key Features:
- Automatic response schema extraction from @response_handler decorators
- Dynamic form generation in UI based on Pydantic/dataclass response types
- Checkpoint-based conversation storage for HIL requests/responses
- Resume workflow execution after user provides HIL response

Backend Changes:
- Add extract_response_type_from_executor() to introspect response handlers
- Enrich RequestInfoEvent with response_schema via _enrich_request_info_event_with_response_schema()
- Map RequestInfoEvent to response.input.requested OpenAI event format
- Store HIL responses in conversation history and restore checkpoints

Frontend Changes:
- Add HILInputModal component with SchemaFormRenderer for dynamic forms
- Support Pydantic BaseModel and dataclass response types
- Render enum fields as dropdowns, strings as text/textarea, numbers, booleans, arrays, objects
- Display original request context alongside response form

Testing:
- Add  tests for checkpoint storage (test_checkpoints.py)
- Add schema generation tests for all input types (test_schema_generation.py)
- Validate end-to-end HIL flow with spam workflow sample

This enables workflows to seamlessly pause execution and request structured user input
with type-safe, validated forms generated automatically from response type annotations.

* improve HIL support, improve workflow execution view

* ui updates

* ui updates

* improve HIL for workflows, add auth and view modes

* update workflow

* security improvements , ui fixes

* fix mypy error

* update loading spinner in ui

* DevUI: Serialize workflow input as string to maintain conformance with OpenAI Responses format

* Phase 1: Add /meta endpoint and fix workflow event naming for .NET DevUI compatibility

* additional fixes for .NET DevUI workflow visualization item ID tracking

**Problem:**
.NET DevUI was generating different item IDs for ExecutorInvokedEvent and
ExecutorCompletedEvent, causing only the first executor to highlight in the
workflow graph. Long executor names and error messages also broke UI layout.

**Changes:**
- Add ExecutorActionItemResource to match Python DevUI implementation
- Track item IDs per executor using dictionary in AgentRunResponseUpdateExtensions
- Reuse same item ID across invoked/completed/failed events for proper pairing
- Add truncateText() utility to workflow-utils.ts
- Truncate executor names to 35 chars in execution timeline
- Truncate error messages to 150 chars in workflow graph nodes

** Details:**
- ExecutorActionItemResource registered with JSON source generation context
- Dictionary cleaned up after executor completion/failure to prevent memory leaks
- Frontend item tracking by unique item.id supports multiple executor runs
- All changes follow existing codebase patterns and conventions

Tested with review-workflow showing correct executor highlighting and state
transitions for sequential and concurrent executors.

* format fixes, remove cors tests

* remove unecessary attributes

---------

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Reuben Bond <reuben.bond@gmail.com>
2025-11-10 18:56:31 +00:00
westeyandGitHub c0c12df851 .NET: Separate all memory and rag samples into their own folders (#2000)
* Separate all memory and rag samples into their own folders

* Fix broken link.
2025-11-10 15:34:20 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6cc0e2a0d8 Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.2 to 4.0.4.6 (#2031)
---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.6
  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-11-10 13:02:43 +00:00
01f3a3d881 .NET: Remove moved OpenAPI sample and point to SK one. (#1997)
* Remove moved OpenAPI sample and point to SK one.

* Update dotnet/samples/GettingStarted/Agents/README.md

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-10 12:29:26 +00:00
Mark WallaceandGitHub f41e103ee2 Version 1.0.0-preview.251110.1 (#2048) 2025-11-10 12:23:22 +00:00
Korolev DmitryandGitHub 866b4198bf .NET: assign AgentCard's URL to mapped-endpoint if not defined explicitly (#2047)
* fix serialization in chat completions on tools

* nit

* write e2e test for agent card resolve + adjust behavior

* nit
2025-11-10 12:06:59 +00:00
Korolev DmitryandGitHub 42da3cb6a4 .NET: fix ChatCompletions Tools serialization (#2043)
* fix serialization in chat completions on tools

* nit
2025-11-10 11:35:54 +00:00
Eduard van ValkenburgandGitHub e7224b5efb added missing chatAgent params (#2044) 2025-11-10 11:02:45 +00:00
Evan MattsonandGitHub 548e0f028e Python: Fix underlying tool choice bug and all for return to previous Handoff subagent (#2037)
* Fix tool_choice override bug and add enable_return_to_previous support

* Add unit test for handoff checkpointing

* Handle tools when we have them
2025-11-10 10:55:52 +00:00
Javier Calvarro NelsonandGitHub 45dc0ff073 .NET [AG-UI]: Adds support for shared state. (#1996)
* Product changes

* Tests

* Dojo project

* Cleanups
2025-11-10 09:50:11 +00:00
0e7183dbd8 DevUI: Prevent line breaks within words in the agent view (#2024)
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
2025-11-10 01:16:24 +00:00
Dmytro StrukandGitHub c394f3f52b Updated package versions (#2027) 2025-11-08 18:09:28 +00:00
Shawn HenryandGitHub 66a9976b31 Add Microsoft Agent Framework logo to assets (#2007) 2025-11-08 07:27:33 +00:00
4201b9a122 DevUI: Serialize workflow input as string to maintain conformance with OpenAI Responses format (#2021)
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
2025-11-08 03:25:08 +00:00
1aaf37dab8 .NET: Remove launchSettings.json from .gitignore in dotnet/samples (#2006)
* Remove launchSettings.json from .gitignore in dotnet/samples

* Update dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Properties/launchSettings.json

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

* Update dotnet/samples/AGUIClientServer/AGUIServer/Properties/launchSettings.json

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 23:45:15 +00:00
94eae24082 Python: DevUI: Add OpenAI Responses API proxy support + HIL for Workflows (#1737)
* DevUI: Add OpenAI Responses API proxy support with enhanced UI features

This commit adds support for proxying requests to OpenAI's Responses API,
allowing DevUI to route conversations to OpenAI models when configured to enable testing.

Backend changes:
- Add OpenAI proxy executor with conversation routing logic
- Enhance event mapper to support OpenAI Responses API format
- Extend server endpoints to handle OpenAI proxy mode
- Update models with OpenAI-specific response types
- Remove emojis from logging and CLI output for cleaner text

Frontend changes:
- Add settings modal with OpenAI proxy configuration UI
- Enhance agent and workflow views with improved state management
- Add new UI components (separator, switch) for settings
- Update debug panel with better event filtering
- Improve message renderers for OpenAI content types
- Update types and API client for OpenAI integration

* update ui, settings modal and workflow input form, add register cleanup hooks.

* add workflow HIL support, user mode, other fixes

* feat(devui): add human-in-the-loop (HIL) support with dynamic response schemas

Implement  HIL workflow support allowing workflows to pause for user input
with dynamically generated JSON schemas based on response handler type hints.

Key Features:
- Automatic response schema extraction from @response_handler decorators
- Dynamic form generation in UI based on Pydantic/dataclass response types
- Checkpoint-based conversation storage for HIL requests/responses
- Resume workflow execution after user provides HIL response

Backend Changes:
- Add extract_response_type_from_executor() to introspect response handlers
- Enrich RequestInfoEvent with response_schema via _enrich_request_info_event_with_response_schema()
- Map RequestInfoEvent to response.input.requested OpenAI event format
- Store HIL responses in conversation history and restore checkpoints

Frontend Changes:
- Add HILInputModal component with SchemaFormRenderer for dynamic forms
- Support Pydantic BaseModel and dataclass response types
- Render enum fields as dropdowns, strings as text/textarea, numbers, booleans, arrays, objects
- Display original request context alongside response form

Testing:
- Add  tests for checkpoint storage (test_checkpoints.py)
- Add schema generation tests for all input types (test_schema_generation.py)
- Validate end-to-end HIL flow with spam workflow sample

This enables workflows to seamlessly pause execution and request structured user input
with type-safe, validated forms generated automatically from response type annotations.

* improve HIL support, improve workflow execution view

* ui updates

* ui updates

* improve HIL for workflows, add auth and view modes

* update workflow

* security improvements , ui fixes

* fix mypy error

* update loading spinner in ui

---------

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
2025-11-07 23:28:32 +00:00
85484c0259 .NET: DevUI - Do not automatically add/map OpenAI services/endpoints (#2014)
* Don't add OpenAIResponses as part of Dev UI

You should be able to add and remove Dev UI without impacting your other production endpoints.

* Remove `AddDevUI()` and do not map OpenAI endpoints from `MapDevUI()`

* Fix comment wording

* Revise documentation

---------

Co-authored-by: Daniel Roth <daroth@microsoft.com>
2025-11-07 23:03:54 +00:00
Reuben BondandGitHub f71faa80f9 Python: DevUI: Use metadata.entity_id instead of model field (#1984)
* DevUI: Use metadata.entity_id for agent/workflow name instead of model field

* OpenAI Responses: add explicit request validation

* Review feedback
2025-11-07 22:16:55 +00:00
Peter IbekweandGitHub 778a9fec5c .NET: Add unit tests for declarative executor SetMultipleVariables (#2016)
* Add unit tests for create conversation executor

* Update indentation and comment typo.

* Added unit tests for declarative executor SetMultipleVariablesExecutor

* Updated comments and syntactic sugar
2025-11-07 21:44:22 +00:00
Reuben BondandGitHub f2e697b634 Do not build DevUI assets during .NET project build (#2010) 2025-11-07 19:06:16 +00:00
Mark WallaceandGitHub cfcec83f0f Version preview.251107.1 (#2008) 2025-11-07 17:31:45 +00:00
Javier Calvarro NelsonandGitHub e859edc2a4 .NET: AG-UI support for .NET: Support for tool calling (#1896)
* Initial implementation

* tmp

* Replace function calling with a FunctionInvokingChatClient

* Cleanups

* Remove custom thread

* Fixing function calling server and client

* Cleanup

* Cleanup serialization

* Run dotnet format

* Pass logger factory

* Populate message properties

* Remove files

* Cleanups

* cleanup

* Cleanups

* More cleanup

* Simplify things

* Cleanup

* Clean up json serialization

* Additional tests

* Add service collection extensions for serialization

* Combine options in AGUIChatClient

* Additional tests

* Include tool calling in the sample, fix mixed server and client tool calls

* Fix tests

* More cleanups

* Fix tests

* Cleanups

* Dojo project and fixes

* Fix build

* Remove dojo

* Cleanup

* Address feedback

* address feedback

* Additional feedback

* Fix build

* Fix build

* Make packages packable
2025-11-07 17:23:21 +00:00
Reuben BondandGitHub 3d94ae57ed .NET: DevUI: Use relative URLs for backend API by default (#2005)
* DevUI: Use relative URLs for backend API by default

* dotnet format

* rebuild application
2025-11-07 17:01:04 +00:00
Korolev DmitryandGitHub 00b67e191b move a2a tests; add extensions for a2a+openairesponses (#1992) 2025-11-07 16:32:06 +00:00
Korolev DmitryandGitHub 32b984b09b .NET: Add WithAITool extensions for Hosting AIAgents (#1990)
* add extensions to register tools via fluentAPI

* fix
2025-11-07 16:28:28 +00:00
Korolev DmitryandGitHub b7cddaaba8 .NET: Remove sequential\concurrent workflow extensions (#1731)
* remove non-standard workflow extensions, create overloads for IHostedAgentBuilder

* rework

* rollback extensions in a2a
2025-11-07 11:58:21 +00:00
e5d9d74c2d .NET: Chathistory memory provider add (#1867)
* Add ChatHistoryMemoryProvider with unit tests

* Set new project to not packable.

* Fix bugs

* Add serialization support.

* Update dotnet/src/Microsoft.Agents.AI.VectorDataMemory/ChatHistoryMemoryProvider.cs

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

* Remove unnecessary line

* Convert ChatHistoryMemoryProvider to use Dynamic collections.

* Sealing options and scope classes.

* Add sample, add scope to logs and improve scope validation

* Move ChatHistoryMemoryProvider to MAAI project.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 11:46:35 +00:00
Rishabh ChawlaandGitHub 64826b8f56 Python: [Purview] Add Caching and background processing in Python Purview Middleware (#1844)
* [PythonPurview] Add Caching and background processing

* [PythonPurview] Updates based on comments
2025-11-07 07:43:22 +00:00
820c6afe09 .NET: Fix the ordering of chained resolvers in JsonSerializerOptions (#1974)
* Fix the ordering of chained resolvers in JsonSerializerOptions

We want the resolvers from AIJsonUtilities to be used before the ones from the source generator, in case the source generator emits its own copy in that assembly for the M.E.AI types.

* Apply suggestions from code review

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

* Update dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs

* Update dotnet/src/Microsoft.Agents.AI.Mem0/Mem0JsonUtilities.cs

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

* Remove unused using directive in Mem0JsonUtilities

Removed unused using directive for Microsoft.Extensions.AI.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
2025-11-06 22:23:37 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
cb50f3e070 Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.1 to 4.0.4.2 (#1707)
---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.2
  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-11-06 22:13:25 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
25f405c7ce Bump CommunityToolkit.Aspire.OllamaSharp from 9.8.0 to 9.9.0 (#1961)
---
updated-dependencies:
- dependency-name: CommunityToolkit.Aspire.OllamaSharp
  dependency-version: 9.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-06 22:12:45 +00:00
Jeff HandleyandGitHub b374ff0e10 Rename nightly build GitHub source in FAQS.md to be consistent (#1755) 2025-11-06 22:11:00 +00:00
westeyandGitHub 6e205445be .NET: Add tool calling sample with OpenAPI (#1968)
* Add tool calling sample with OpenAPI

* Address PR comments.

* Rename folders and moved literal to inline.

* Fix broken link.
2025-11-06 14:34:27 +00:00
Evan MattsonandGitHub 708556e4ee Python: Update changelog with ag-ui changes (#1954)
* Update changelog with ag-ui changes

* Changed -> Fixed
2025-11-06 12:21:54 +09:00
Giles OdigweandGitHub ee1661ecb7 Python: Thread Samples Fix (#1945)
* thread samples fix

* custom chat message store fix
2025-11-06 02:59:22 +00:00
Evan MattsonandGitHub ac018f700b Python: Fix ag-ui examples packaging for PyPI publish (#1953)
* Fix ag-ui examples packaging for PyPI publish

* Fix markdown links
2025-11-06 11:31:24 +09:00
Dmytro StrukandGitHub 6fec8a61e3 Updated packages configuration (#1952) 2025-11-06 01:04:48 +00:00
Evan MattsonandGitHub 99e2875fc8 Bump ag-ui package to 1.0.0b251106 for a release. Update CHANGELOG. (#1951) 2025-11-06 09:57:23 +09:00
Dmytro StrukandGitHub 573aff4825 Updated chatkit package version (#1950) 2025-11-06 00:13:52 +00:00
9b471cc479 Add simple workflow sample that mixes agents and executors (#1946)
* Add workflow mix agent executor samples

* Apply suggestion from @Copilot

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-11-05 22:40:25 +00:00
Evan MattsonandGitHub afd8b7ecb4 Export sample agents (#1927) 2025-11-05 22:26:18 +00:00
Dmytro StrukandGitHub 327c304339 .NET: Python: Updated package versions (#1944)
* Updated .NET version

* Updated Python package versions

* Updated changelog
2025-11-05 22:20:31 +00:00
Tao ChenandGitHub 3f835c8118 .NET: AgentHostExecutor should use agent DisplayName as Executor ID (#1840)
* AgentHostExecutor should use agent DisplayName as Executor ID

* Address comments

* Remove unintended changes

* Remove unintended changes

* Fix tests and address comments
2025-11-05 22:16:53 +00:00
Jacob AlberandGitHub 0bf6d437d8 .NET: [BREAKING] .NET: Add Workflow on-Build() Orphan Validation (#1943)
* fix: Workflow Validation

* adds orphan validation to workflow builder
* adds tests for workflow validation
* expands on the underlying reasoning why type validation is not supported

* fixup: CodeGen template
2025-11-05 22:12:27 +00:00
d701e796cb Python: [BREAKING] Replaced AIProjectClient with AgentsClient in Foundry (#1936)
* Replaced AIProjectClient with AgentsClient in Foundry

* Update python/samples/getting_started/observability/azure_ai_agent_observability.py

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

* Update python/samples/getting_started/observability/azure_ai_chat_client_with_observability.py

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

* Small fix

* Removed TODO item

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 21:42:36 +00:00
Reuben BondandGitHub 8855bfb065 .NET: Add DevUI package for .NET (#1603)
* Implement DevUI

* Review feedback

* Fix build
2025-11-05 20:02:48 +00:00
Jacob AlberandGitHub 94a5ba3448 .NET: [BREAKING] refactor: Normalize WorkflowBuilder APIs (#1935)
* [BREAKING] refactor: Normalize WorkflowBuilder APIs

* "partitioner" => "assigner"
* normalize ordering so sources always to the left of targets for edges
* normalize parameter ordering so sources and targets are always first arguments
* remove `params` (users should use collection expressions instead)

* refactor: Align name with Python
2025-11-05 19:50:01 +00:00
Reuben BondandGitHub 33f84f9ed2 .NET: Improve fidelity of OpenAI Responses server and add Conversations (#1907)
* Improve fidelity of OpenAI Responses server and add Conversations

* Merge

* nit

* Undo prior change

* Undo prior change

* Review feedback

* Review feedback

* Fix test

* Use simpler JsonDocument approach for polymorphic deserialization

* More review feedback

* dotnet format
2025-11-05 18:46:19 +00:00
Eduard van ValkenburgandGitHub e2282ebe42 Python: fix missing packaging dependency (#1929)
* fix missing packaging dependency

* add aiohttp to azureai
2025-11-05 18:09:54 +00:00
Jacob AlberandGitHub b8a55dccb4 [BREAKING] refactor: Fix unintuitive binding of StreamAsync (#1930)
In #1551 we added a mechanism to open a Streaming workflow run without providing any input. This caused unintuitive behaviour when passing a string as input without providing a runId, resulting in the input being misinterpreted as the runId and the workflow not executing.

As well, the name caused confusion about why the Workflow was not running when using the input-less StreamAsync (since the Workflow cannot run without any messages to drive its execution).
2025-11-05 17:26:46 +00:00
Dmytro StrukandGitHub 14aee7e334 Python: Added parameter to disable agent cleanup in AzureAIAgentClient (#1882)
* Removed automatic agent cleanup in AzureAIAgentClient

* Revert "Removed automatic agent cleanup in AzureAIAgentClient"

This reverts commit 89846c7212.

* Exposed boolean flag to control deletion behavior

* Update sample
2025-11-05 16:56:19 +00:00
Javier Calvarro NelsonandGitHub b03a4fb95e .NET: AG-UI support for .NET (#1776)
* Initial plan

* Infrastructure setup

* Plan for minimal client

* Plan update

* Basic agentic chat

* cleanup

* Cleanups

* More cleanups

* Cleanups

* More cleanups

* Test plan

* Sample

* Fix streaming and error handling

* Fix notifications

* Cleanups

* cleanup sample

* Additional tests

* Additional tests

* Run dotnet format

* Remove unnecessary files

* Mark packages as non packable

* Fix build

* Address feedback

* Fix build

* Fix remaining warnings

* Feedback

* Feedback and cleanup

* Cleanup

* Cleanups

* Cleanups

* Cleanups

* Retrieve existing messages from the store to send them along the way and update the sample client

* Run dotnet format

* Add ADR for AG-UI

* Switch to use the SG and use a convention for run ids

* Cleanup MapAGUI API

* Fix formatting

* Fix solution

* Fix solution
2025-11-05 15:51:37 +00:00
Jeff HandleyandGitHub 77d882e2b4 Fix workflow lookup with AddAsAIAgent(name) when name differs from workflow name (#1925) 2025-11-05 15:31:36 +00:00
Roger BarretoandGitHub 6ca907f23f .NET: [Breaking Change] Moving MAAI.AzureAI V1 Package -> MAAI.AzureAI.Persistent (V1) (#1902)
* Update AzureAI -> AzureAI.Persistent

* Fix sample reference
2025-11-05 13:28:00 +00:00
westeyandGitHub 5e38c63455 .NET: [BREAKING] Simplify TextSearchProvider construction and improve Mem0Provider scoping. (#1905)
* Simplify TextSearchProvider construction and improve Mem0Provider scoping

* Fixing indentation.
2025-11-05 13:00:18 +00:00
Korolev DmitryandGitHub bb8ef466de .NET: Improve fidelity of OpenAI ChatCompletions Hosting (#1785)
* rename, support json serialization

* wip

* non-streaming

* streaming?

* proper streaming types

* comments + fix audio parse

* copilot suggestions

* proper stopsequences type

* build options as i could

* annotations

* proper generation of Id for chatcompletions

* string length as in chatcompletions api ref

* image url

* support tools

* rework API

* introduce tests for chatcompletions

* function calling / serialization tests / fixes

* more tests and coverage

* fix format

* sort usings

* nit

* address PR comments

* nits
2025-11-05 09:55:26 +00:00
Eduard van ValkenburgandGitHub 54db13c22f Python: add support for Python 3.14 (#1904)
* add tests for py3.14 and add classifier

* remove macos

* allow openai v2
2025-11-05 09:42:39 +00:00
Eduard van ValkenburgandGitHub 51b32ed1ac Python: Updates to Tools (#1835)
* updated tool samples

* mypy and readme fixes

* updated call logic

* added function invocation config

* added include detailed error

* added tests

* updated FRC exception handling

* updated tests

* fix oai test

* fix name in sample

* imporoved tests coverage and removed some dead code paths
2025-11-05 08:33:19 +00:00
985 changed files with 85971 additions and 8494 deletions
+2
View File
@@ -12,6 +12,8 @@ ignorePatterns:
- pattern: "https:\/\/platform.openai.com"
- pattern: "http:\/\/localhost"
- pattern: "http:\/\/127.0.0.1"
- pattern: "https:\/\/localhost"
- pattern: "https:\/\/127.0.0.1"
- pattern: "0001-spec.md"
- pattern: "0001-madr-architecture-decisions.md"
- pattern: "https://api.powerplatform.com/.default"
+4
View File
@@ -0,0 +1,4 @@
# Code ownership assignments
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
@@ -0,0 +1,36 @@
name: Azure Functions Integration Test Setup
description: Prepare local emulators and tools for Azure Functions integration tests
runs:
using: "composite"
steps:
- name: Start Durable Task Scheduler Emulator
shell: bash
run: |
if [ "$(docker ps -aq -f name=dts-emulator)" ]; then
echo "Stopping and removing existing Durable Task Scheduler Emulator"
docker rm -f dts-emulator
fi
echo "Starting Durable Task Scheduler Emulator"
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
echo "Waiting for Durable Task Scheduler Emulator to be ready"
timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
echo "Durable Task Scheduler Emulator is ready"
- name: Start Azurite (Azure Storage emulator)
shell: bash
run: |
if [ "$(docker ps -aq -f name=azurite)" ]; then
echo "Stopping and removing existing Azurite (Azure Storage emulator)"
docker rm -f azurite
fi
echo "Starting Azurite (Azure Storage emulator)"
docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
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: Install Azure Functions Core Tools
shell: bash
run: |
echo "Installing Azure Functions Core Tools"
npm install -g azure-functions-core-tools@4 --unsafe-perm true
func --version
@@ -1052,7 +1052,7 @@ AgentThread thread = agent.GetNewThread();
**Add Agent Framework Packages:**
```xml
<PackageReference Include="Microsoft.Agents.AI.AzureAI" />
<PackageReference Include="Microsoft.Agents.AI.AzureAI.Persistent" />
<PackageReference Include="Azure.Identity" />
```
</configuration_changes>
+15 -3
View File
@@ -8,11 +8,11 @@ name: dotnet-build-and-test
on:
workflow_dispatch:
pull_request:
branches: ["main"]
branches: ["main", "feature*"]
merge_group:
branches: ["main"]
branches: ["main", "feature*"]
push:
branches: ["main"]
branches: ["main", "feature*"]
schedule:
- cron: "0 0 * * *" # Run at midnight UTC daily
@@ -74,6 +74,7 @@ jobs:
.
.github
dotnet
python
workflow-samples
- name: Setup dotnet
@@ -150,6 +151,14 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# This setup action is required for both Durable Task and Azure Functions integration tests.
# We only run it on Ubuntu since the Durable Task and Azure Functions features are not available
# on .NET Framework (net472) which is what we use the Windows runner for.
- name: Set up Durable Task and Azure Functions Integration Test Emulators
if: github.event_name != 'pull_request' && matrix.integration-tests && matrix.os == 'ubuntu-latest'
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Run Integration Tests
shell: bash
if: github.event_name != 'pull_request' && matrix.integration-tests
@@ -171,6 +180,9 @@ jobs:
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
# Azure OpenAI Models
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
# Azure AI Foundry
AzureAI__Endpoint: ${{ secrets.AZUREAI__ENDPOINT }}
AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
python-version: ["3.10", "3.14"]
runs-on: ubuntu-latest
continue-on-error: true
defaults:
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
# TODO(ekzhu): re-enable macos-latest when this is fixed: https://github.com/actions/runner-images/issues/11881
os: [ubuntu-latest, windows-latest]
env:
+8
View File
@@ -66,6 +66,11 @@ jobs:
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
# For Azure Functions integration tests
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
defaults:
run:
working-directory: python
@@ -87,6 +92,9 @@ jobs:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Azure Functions Integration Test Emulators
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Test with pytest
timeout-minutes: 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
@@ -39,7 +39,7 @@ jobs:
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@v1.1.57
uses: MishaKav/pytest-coverage-comment@v1.1.59
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
issue-number: ${{ env.PR_NUMBER }}
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
# todo: add macos-latest when problems are resolved
os: [ubuntu-latest, windows-latest]
env:
+13 -1
View File
@@ -205,10 +205,22 @@ agents.md
.claude/
WARP.md
# Azurite storage emulator files
*/__azurite_db_blob__.json
*/__azurite_db_blob_extent__.json
*/__azurite_db_queue__.json
*/__azurite_db_queue_extent__.json
*/__azurite_db_table__.json
*/__blobstorage__/
*/__queuestorage__/
# Azure Functions local settings
local.settings.json
# Frontend
**/frontend/node_modules/
**/frontend/.vite/
**/frontend/dist/
# Database files
*.db
*.db
+6 -6
View File
@@ -23,23 +23,23 @@ To download nightly builds follow the following steps:
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="github" value="https://nuget.pkg.github.com/microsoft/index.json" />
<add key="GitHubMicrosoft" value="https://nuget.pkg.github.com/microsoft/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="github">
<packageSource key="GitHubMicrosoft">
<package pattern="*nightly"/>
</packageSource>
</packageSourceMapping>
<packageSourceCredentials>
<github>
<add key="Username" value="<Your GitHub Id>" />
<add key="ClearTextPassword" value="<Your Personal Access Token>" />
</github>
<GitHubMicrosoft>
<add key="Username" value="<Your GitHub Id>" />
<add key="ClearTextPassword" value="<Your Personal Access Token>" />
</GitHubMicrosoft>
</packageSourceCredentials>
</configuration>
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

+95
View File
@@ -0,0 +1,95 @@
---
status: accepted
contact: javiercn
date: 2025-10-29
deciders: javiercn, DeagleGross, moonbox3, markwallace-microsoft
consulted: Agent Framework team
informed: .NET community
---
# AG-UI Protocol Support for .NET Agent Framework
## Context and Problem Statement
The .NET Agent Framework needed a standardized way to enable communication between AI agents and user-facing applications with support for streaming, real-time updates, and bidirectional communication. Without AG-UI protocol support, .NET agents could not interoperate with the growing ecosystem of AG-UI-compatible frontends and agent frameworks (LangGraph, CrewAI, Pydantic AI, etc.), limiting the framework's adoption and utility.
The AG-UI (Agent-User Interaction) protocol is an open, lightweight, event-based protocol that addresses key challenges in agentic applications including streaming support for long-running agents, event-driven architecture for nondeterministic behavior, and protocol interoperability that complements MCP (tool/context) and A2A (agent-to-agent) protocols.
## Decision Drivers
- Need for streaming communication between agents and client applications
- Requirement for protocol interoperability with other AI frameworks
- Support for long-running, multi-turn conversation sessions
- Real-time UI updates for nondeterministic agent behavior
- Standardized approach to agent-to-UI communication
- Framework abstraction to protect consumers from protocol changes
## Considered Options
1. **Implement AG-UI event types as public API surface** - Expose AG-UI event models directly to consumers
2. **Use custom AIContent types for lifecycle events** - Create new content types (RunStartedContent, RunFinishedContent, RunErrorContent)
3. **Current approach** - Internal event types with framework-native abstractions
## Decision Outcome
Chosen option: "Current approach with internal event types and framework-native abstractions", because it:
- Protects consumers from protocol changes by keeping AG-UI events internal
- Maintains framework abstractions through conversion at boundaries
- Uses existing framework types (AgentRunResponseUpdate, ChatMessage) for public API
- Focuses on core text streaming functionality
- Leverages existing properties (ConversationId, ResponseId, ErrorContent) instead of custom types
- Provides bidirectional client and server support
### Implementation Details
**In Scope:**
1. **Client-side AG-UI consumption** (`Microsoft.Agents.AI.AGUI` package)
- `AGUIAgent` class for connecting to remote AG-UI servers
- `AGUIAgentThread` for managing conversation threads
- HTTP/SSE streaming support
- Event-to-framework type conversion
2. **Server-side AG-UI hosting** (`Microsoft.Agents.AI.Hosting.AGUI.AspNetCore` package)
- `MapAGUIAgent` extension method for ASP.NET Core
- Server-Sent Events (SSE) response formatting
- Framework-to-event type conversion
- Agent factory pattern for per-request instantiation
3. **Text streaming events**
- Lifecycle events: `RunStarted`, `RunFinished`, `RunError`
- Text message events: `TextMessageStart`, `TextMessageContent`, `TextMessageEnd`
- Thread and run ID management via `ConversationId` and `ResponseId`
### Key Design Decisions
1. **Event Models as Internal Types** - AG-UI event types are internal with conversion via extension methods; public API uses the existing types in Microsoft.Extensions.AI as those are the abstractions people are familiar with
2. **No Custom Content Types** - Run lifecycle communicated through existing `ChatResponseUpdate` properties (`ConversationId`, `ResponseId`) and standard `ErrorContent` type
3. **Agent Factory Pattern** - `MapAGUIAgent` uses factory function `(messages) => AIAgent` to allow request-specific agent configuration supporting multi-tenancy
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentRunResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentRunResponseUpdate`)
5. **Thread Management** - `AGUIAgentThread` stores only `ThreadId` with thread ID communicated via `ConversationId`; applications manage persistence for parity with other implementations and to be compliant with the protocol. Future extensions will support having the server manage the conversation.
6. **Custom JSON Converter** - Uses custom polymorphic deserialization via `BaseEventJsonConverter` instead of built-in System.Text.Json support to handle AG-UI protocol's flexible discriminator positioning
### Consequences
**Positive:**
- .NET developers can consume AG-UI servers from any framework
- .NET agents accessible from any AG-UI-compatible client
- Standardized streaming communication patterns
- Protected from protocol changes through internal implementation
- Symmetric conversion logic between client and server
- Framework-native public API surface
**Negative:**
- Custom JSON converter required (internal implementation detail)
- Shared code uses preprocessor directives (`#if ASPNETCORE`)
- Additional abstraction layer between protocol and public API
**Neutral:**
- Initial implementation focused on text streaming
- Applications responsible for thread persistence
+62 -44
View File
@@ -7,32 +7,33 @@
</PropertyGroup>
<PropertyGroup>
<!-- Aspire -->
<AspireAppHostSdkVersion>9.5.2</AspireAppHostSdkVersion>
<AspireAppHostSdkVersion>13.0.0</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
<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="9.8.0" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.435" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.8.0" />
<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="9.0.10" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.10" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
<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.Threading.Tasks.Extensions" Version="4.6.3" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
@@ -43,36 +44,39 @@
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
<!-- Microsoft.AspNetCore.* -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.10.2" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.10.2" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.10.0-preview.1.25513.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.0-preview.1.25559.3" />
<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" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<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.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.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
<!-- Vector Stores -->
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
<!-- Semantic Kernel -->
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.67.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.67.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
<!-- A2A -->
@@ -82,7 +86,7 @@
<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.1" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.6" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.6.0" />
@@ -93,15 +97,29 @@
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1003.2" />
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1003.2" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0" />
<!-- 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" />
<!-- 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.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" />
<!-- Community -->
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="Moq" Version="[4.18.4]" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.66.0-beta" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.67.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.67.0-beta" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
@@ -111,7 +129,7 @@
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<!-- Toolset -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -131,7 +149,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.0]" />
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.1]" />
<PackageReference Include="Roslynator.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+54 -12
View File
@@ -18,6 +18,22 @@
<Project Path="samples/AgentWebChat/AgentWebChat.ServiceDefaults/AgentWebChat.ServiceDefaults.csproj" />
<Project Path="samples/AgentWebChat/AgentWebChat.Web/AgentWebChat.Web.csproj" />
</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/AzureFunctions/">
<File Path="samples/AzureFunctions/.editorconfig" />
<File Path="samples/AzureFunctions/README.md" />
<Project Path="samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<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" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
</Folder>
@@ -53,24 +69,36 @@
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step13_Memory/Agent_Step13_Memory.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Agent_Step13_BackgroundResponsesWithToolsAndPersistence.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step14_Middleware/Agent_Step14_Middleware.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step15_Plugins/Agent_Step15_Plugins.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Agent_Step18_TextSearchRag.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Agent_Step19_Mem0Provider.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Agent_Step20_BackgroundResponsesWithToolsAndPersistence.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/DevUI/">
<File Path="samples/GettingStarted/DevUI/README.md" />
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/AgentWithMemory_Step03_CustomMemory.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
</Folder>
<Folder Name="/Samples/Purview/AgentWithPurview/">
<Project Path="samples/Purview/AgentWithPurview/AgentWithPurview.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithRAG/">
<File Path="samples/GettingStarted/AgentWithRAG/README.md" />
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj" />
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/AgentWithRAG_Step02_ExternalDataSourceRAG.csproj" />
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/ModelContextProtocol/">
<File Path="samples/GettingStarted/ModelContextProtocol/README.md" />
@@ -145,10 +173,10 @@
<Project Path="samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj" />
<Project Path="samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/Catalog/">
<Project Path="samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj" />
<Project Path="samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj" />
<Folder Name="/Samples/HostedAgents/">
<Project Path="samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
@@ -272,14 +300,20 @@
<Folder Name="/src/">
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
@@ -289,6 +323,9 @@
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj" />
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" />
@@ -298,12 +335,17 @@
<Folder Name="/Tests/UnitTests/">
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Microsoft.Agents.AI.Hosting.A2A.Tests.csproj" Id="2a1c544d-237d-4436-8732-ba0c447ac06b" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
+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).251104.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251104.1</PackageVersion>
<GitTag>1.0.0-preview.251104.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251113.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251113.1</PackageVersion>
<GitTag>1.0.0-preview.251113.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
-1
View File
@@ -1 +0,0 @@
launchSettings.json
@@ -20,7 +20,7 @@
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>
<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>
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
using System.Text.Json.Serialization;
namespace AGUIClient;
[JsonSerializable(typeof(SensorRequest))]
[JsonSerializable(typeof(SensorResponse))]
internal sealed partial class AGUIClientSerializerContext : JsonSerializerContext;
@@ -0,0 +1,213 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
using System.CommandLine;
using System.ComponentModel;
using System.Reflection;
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AGUIClient;
public static class Program
{
public static async Task<int> Main(string[] args)
{
// Create root command with options
RootCommand rootCommand = new("AGUIClient");
rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
// Run the command
return await rootCommand.Parse(args).InvokeAsync();
}
private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
{
// Set up the logging
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
ILogger logger = loggerFactory.CreateLogger("AGUIClient");
// Retrieve configuration settings
IConfigurationRoot configRoot = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.Build();
string serverUrl = configRoot["AGUI_SERVER_URL"] ?? "http://localhost:5100";
logger.LogInformation("Connecting to AG-UI server at: {ServerUrl}", serverUrl);
// Create the AG-UI client agent
using HttpClient httpClient = new()
{
Timeout = TimeSpan.FromSeconds(60)
};
var changeBackground = AIFunctionFactory.Create(
() =>
{
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.WriteLine("Changing color to blue");
},
name: "change_background_color",
description: "Change the console background color to dark blue."
);
var readClientClimateSensors = AIFunctionFactory.Create(
([Description("The sensors measurements to include in the response")] SensorRequest request) =>
{
return new SensorResponse()
{
Temperature = 22.5,
Humidity = 45.0,
AirQualityIndex = 75
};
},
name: "read_client_climate_sensors",
description: "Reads the climate sensor data from the client device.",
serializerOptions: AGUIClientSerializerContext.Default.Options
);
var chatClient = new AGUIChatClient(
httpClient,
serverUrl,
jsonSerializerOptions: AGUIClientSerializerContext.Default.Options);
AIAgent agent = chatClient.CreateAIAgent(
name: "agui-client",
description: "AG-UI Client Agent",
tools: [changeBackground, readClientClimateSensors]);
AgentThread thread = agent.GetNewThread();
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
try
{
while (true)
{
// Get user message
Console.Write("\nUser (:q or quit to exit): ");
string? message = Console.ReadLine();
if (string.IsNullOrWhiteSpace(message))
{
Console.WriteLine("Request cannot be empty.");
continue;
}
if (message is ":q" or "quit")
{
break;
}
messages.Add(new(ChatRole.User, message));
// Call RunStreamingAsync to get streaming updates
bool isFirstUpdate = true;
string? threadId = null;
var updates = new List<ChatResponseUpdate>();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
{
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
updates.Add(chatUpdate);
if (chatUpdate.ConversationId != null)
{
threadId = chatUpdate.ConversationId;
}
// Display run started information from the first update
if (isFirstUpdate && threadId != null && update.ResponseId != null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
// Display different content types with appropriate formatting
foreach (AIContent content in update.Contents)
{
switch (content)
{
case TextContent textContent:
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write(textContent.Text);
Console.ResetColor();
break;
case FunctionCallContent functionCallContent:
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}, Arguments: {PrintArguments(functionCallContent.Arguments)}]");
Console.ResetColor();
break;
case FunctionResultContent functionResultContent:
Console.ForegroundColor = ConsoleColor.Magenta;
if (functionResultContent.Exception != null)
{
Console.WriteLine($"\n[Function Result - Exception: {functionResultContent.Exception}]");
}
else
{
Console.WriteLine($"\n[Function Result - Result: {functionResultContent.Result}]");
}
Console.ResetColor();
break;
case ErrorContent errorContent:
Console.ForegroundColor = ConsoleColor.Red;
string code = errorContent.AdditionalProperties?["Code"] as string ?? "Unknown";
Console.WriteLine($"\n[Error - Code: {code}, Message: {errorContent.Message}]");
Console.ResetColor();
break;
}
}
}
if (updates.Count > 0 && !updates[^1].Contents.Any(c => c is TextContent))
{
var lastUpdate = updates[^1];
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine();
Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
Console.ResetColor();
}
messages.Clear();
Console.WriteLine();
}
}
catch (OperationCanceledException)
{
logger.LogInformation("AGUIClient operation was canceled.");
}
catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException and not ThreadAbortException and not AccessViolationException)
{
logger.LogError(ex, "An error occurred while running the AGUIClient");
return;
}
}
private static string PrintArguments(IDictionary<string, object?>? arguments)
{
if (arguments == null)
{
return "";
}
var builder = new StringBuilder();
builder.AppendLine();
foreach (var kvp in arguments)
{
builder.AppendLine($" Name: {kvp.Key}");
builder.AppendLine($" Value: {kvp.Value}");
}
return builder.ToString();
}
}
@@ -0,0 +1,34 @@
# AG-UI Client
This is a console application that demonstrates how to connect to an AG-UI server and interact with remote agents using the AG-UI protocol.
## Features
- Connects to an AG-UI server endpoint
- Displays streaming updates with color-coded output:
- **Yellow**: Run started notifications
- **Cyan**: Agent text responses (streamed)
- **Green**: Run finished notifications
- **Red**: Error messages (if any)
- Interactive prompt loop for sending messages
## Configuration
Set the following environment variable to specify the AG-UI server URL:
```powershell
$env:AGUI_SERVER_URL="http://localhost:5100"
```
If not set, the default is `http://localhost:5100`.
## Running the Client
1. Make sure the AG-UI server is running
2. Run the client:
```bash
cd AGUIClient
dotnet run
```
3. Enter your messages and observe the streaming updates
4. Type `:q` or `quit` to exit
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
namespace AGUIClient;
internal sealed class SensorRequest
{
public bool IncludeTemperature { get; set; } = true;
public bool IncludeHumidity { get; set; } = true;
public bool IncludeAirQualityIndex { get; set; } = true;
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
// and display streaming updates including conversation/response metadata, text content, and errors.
namespace AGUIClient;
internal sealed class SensorResponse
{
public double Temperature { get; set; }
public double Humidity { get; set; }
public int AirQualityIndex { get; set; }
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer;
[JsonSerializable(typeof(WeatherInfo))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
[JsonSerializable(typeof(RecipeResponse))]
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ChatClient = OpenAI.Chat.ChatClient;
namespace AGUIDojoServer;
internal static class ChatClientAgentFactory
{
private static AzureOpenAIClient? s_azureOpenAIClient;
private static string? s_deploymentName;
public static void Initialize(IConfiguration configuration)
{
string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
s_azureOpenAIClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential());
}
public static ChatClientAgent CreateAgenticChat()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().CreateAIAgent(
name: "AgenticChat",
description: "A simple chat agent using Azure OpenAI");
}
public static ChatClientAgent CreateBackendToolRendering()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().CreateAIAgent(
name: "BackendToolRenderer",
description: "An agent that can render backend tools using Azure OpenAI",
tools: [AIFunctionFactory.Create(
GetWeather,
name: "get_weather",
description: "Get the weather for a given location.",
AGUIDojoServerSerializerContext.Default.Options)]);
}
public static ChatClientAgent CreateHumanInTheLoop()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().CreateAIAgent(
name: "HumanInTheLoopAgent",
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
}
public static ChatClientAgent CreateToolBasedGenerativeUI()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().CreateAIAgent(
name: "ToolBasedGenerativeUIAgent",
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
}
public static ChatClientAgent CreateAgenticUI()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
return chatClient.AsIChatClient().CreateAIAgent(
name: "AgenticUIAgent",
description: "An agent that generates agentic user interfaces using Azure OpenAI");
}
public static AIAgent CreateSharedState(JsonSerializerOptions options)
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(
name: "SharedStateAgent",
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
return new SharedStateAgent(baseAgent, options);
}
[Description("Get the weather for a given location.")]
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
{
Temperature = 20,
Conditions = "sunny",
Humidity = 50,
WindSpeed = 10,
FeelsLike = 25
};
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer;
internal sealed class Ingredient
{
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("amount")]
public string Amount { get; set; } = string.Empty;
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUIDojoServer;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.Extensions.Options;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
logging.RequestBodyLogLimit = int.MaxValue;
logging.ResponseBodyLogLimit = int.MaxValue;
});
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIDojoServerSerializerContext.Default));
builder.Services.AddAGUI();
WebApplication app = builder.Build();
app.UseHttpLogging();
// Initialize the factory
ChatClientAgentFactory.Initialize(app.Configuration);
// Map the AG-UI agent endpoints for different scenarios
app.MapAGUI("/agentic_chat", ChatClientAgentFactory.CreateAgenticChat());
app.MapAGUI("/backend_tool_rendering", ChatClientAgentFactory.CreateBackendToolRendering());
app.MapAGUI("/human_in_the_loop", ChatClientAgentFactory.CreateHumanInTheLoop());
app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI());
app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI());
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));
await app.RunAsync();
public partial class Program { }
@@ -0,0 +1,12 @@
{
"profiles": {
"AGUIDojoServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5018"
}
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer;
internal sealed class Recipe
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;
[JsonPropertyName("cooking_time")]
public string CookingTime { get; set; } = string.Empty;
[JsonPropertyName("special_preferences")]
public List<string> SpecialPreferences { get; set; } = [];
[JsonPropertyName("ingredients")]
public List<Ingredient> Ingredients { get; set; } = [];
[JsonPropertyName("instructions")]
public List<string> Instructions { get; set; } = [];
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer;
#pragma warning disable CA1812 // Used for the JsonSchema response format
internal sealed class RecipeResponse
#pragma warning restore CA1812
{
[JsonPropertyName("recipe")]
public Recipe Recipe { get; set; } = new();
}
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")]
internal sealed class SharedStateAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
{
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
yield break;
}
var firstRunOptions = new ChatClientAgentRunOptions
{
ChatOptions = chatRunOptions.ChatOptions.Clone(),
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
ContinuationToken = chatRunOptions.ContinuationToken,
ChatClientFactory = chatRunOptions.ChatClientFactory,
};
// Configure JSON schema response format for structured state output
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<RecipeResponse>(
schemaName: "RecipeResponse",
schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions");
ChatMessage stateUpdateMessage = new(
ChatRole.System,
[
new TextContent("Here is the current state in JSON format:"),
new TextContent(state.GetRawText()),
new TextContent("The new state is:")
]);
var firstRunMessages = messages.Append(stateUpdateMessage);
var allUpdates = new List<AgentRunResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
// Yield all non-text updates (tool calls, etc.)
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
if (hasNonTextContent)
{
yield return update;
}
}
var response = allUpdates.ToAgentRunResponse();
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
{
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentRunResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
}
else
{
yield break;
}
var secondRunMessages = messages.Concat(response.Messages).Append(
new ChatMessage(
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer;
internal sealed class WeatherInfo
{
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("conditions")]
public string Conditions { get; init; } = string.Empty;
[JsonPropertyName("humidity")]
public int Humidity { get; init; }
[JsonPropertyName("wind_speed")]
public int WindSpeed { get; init; }
[JsonPropertyName("feelsLike")]
public int FeelsLike { get; init; }
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}
@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
},
"AllowedHosts": "*"
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
@host = http://localhost:5100
### Send a message to the AG-UI agent
POST {{host}}/
Content-Type: application/json
{
"threadId": "thread_123",
"runId": "run_456",
"messages": [
{
"role": "user",
"content": "What is the capital of France?"
}
],
"context": {}
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIServer;
[JsonSerializable(typeof(ServerWeatherForecastRequest))]
[JsonSerializable(typeof(ServerWeatherForecastResponse))]
internal sealed partial class AGUIServerSerializerContext : JsonSerializerContext;
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using AGUIServer;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
using OpenAI;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIServerSerializerContext.Default));
builder.Services.AddAGUI();
WebApplication app = builder.Build();
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Create the AI agent with tools
var agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(
name: "AGUIAssistant",
tools: [
AIFunctionFactory.Create(
() => DateTimeOffset.UtcNow,
name: "get_current_time",
description: "Get the current UTC time."
),
AIFunctionFactory.Create(
([Description("The weather forecast request")]ServerWeatherForecastRequest request) => {
return new ServerWeatherForecastResponse()
{
Summary = "Sunny",
TemperatureC = 25,
Date = request.Date
};
},
name: "get_server_weather_forecast",
description: "Gets the forecast for a specific location and date",
AGUIServerSerializerContext.Default.Options)
]);
// Map the AG-UI agent endpoint
app.MapAGUI("/", agent);
await app.RunAsync();
@@ -0,0 +1,12 @@
{
"profiles": {
"AGUIServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5100;https://localhost:5101"
}
}
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
namespace AGUIServer;
internal sealed class ServerWeatherForecastRequest
{
public DateTime Date { get; set; }
public string Location { get; set; } = "Seattle";
}
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
namespace AGUIServer;
internal sealed class ServerWeatherForecastResponse
{
public string Summary { get; set; } = "";
public int TemperatureC { get; set; }
public DateTime Date { get; set; }
}
+208
View File
@@ -0,0 +1,208 @@
# AG-UI Client and Server Sample
This sample demonstrates how to use the AG-UI (Agent UI) protocol to enable communication between a client application and a remote agent server. The AG-UI protocol provides a standardized way for clients to interact with AI agents.
## Overview
The demonstration has two components:
1. **AGUIServer** - An ASP.NET Core web server that hosts an AI agent and exposes it via the AG-UI protocol
2. **AGUIClient** - A console application that connects to the AG-UI server and displays streaming updates
> **Warning**
> The AG-UI protocol is still under development and changing.
> We will try to keep these samples updated as the protocol evolves.
## Configuring Environment Variables
Configure the required Azure OpenAI environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="<<your-model-endpoint>>"
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4.1-mini"
```
> **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables).
## Running the Sample
### Step 1: Start the AG-UI Server
```bash
cd AGUIServer
dotnet build
dotnet run --urls "http://localhost:5100"
```
The server will start and listen on `http://localhost:5100`.
### Step 2: Testing with the REST Client (Optional)
Before running the client, you can test the server using the included `.http` file:
1. Open [./AGUIServer/AGUIServer.http](./AGUIServer/AGUIServer.http) in Visual Studio or VS Code with the REST Client extension
2. Send a test request to verify the server is working
3. Observe the server-sent events stream in the response
Sample request:
```http
POST http://localhost:5100/
Content-Type: application/json
{
"threadId": "thread_123",
"runId": "run_456",
"messages": [
{
"role": "user",
"content": "What is the capital of France?"
}
],
"context": {}
}
```
### Step 3: Run the AG-UI Client
In a new terminal window:
```bash
cd AGUIClient
dotnet run
```
Optionally, configure a different server URL:
```powershell
$env:AGUI_SERVER_URL="http://localhost:5100"
```
### Step 4: Interact with the Agent
1. The client will connect to the AG-UI server
2. Enter your message at the prompt
3. Observe the streaming updates with color-coded output:
- **Yellow**: Run started notification showing thread and run IDs
- **Cyan**: Agent's text response (streamed character by character)
- **Green**: Run finished notification
- **Red**: Error messages (if any occur)
4. Type `:q` or `quit` to exit
## Sample Output
```
AGUIClient> dotnet run
info: AGUIClient[0]
Connecting to AG-UI server at: http://localhost:5100
User (:q or quit to exit): What is the capital of France?
[Run Started - Thread: thread_abc123, Run: run_xyz789]
The capital of France is Paris. It is known for its rich history, culture, and iconic landmarks such as the Eiffel Tower and the Louvre Museum.
[Run Finished - Thread: thread_abc123, Run: run_xyz789]
User (:q or quit to exit): Tell me a fun fact about space
[Run Started - Thread: thread_abc123, Run: run_def456]
Here's a fun fact: A day on Venus is longer than its year! Venus takes about 243 Earth days to rotate once on its axis, but only about 225 Earth days to orbit the Sun.
[Run Finished - Thread: thread_abc123, Run: run_def456]
User (:q or quit to exit): :q
```
## How It Works
### Server Side
The `AGUIServer` uses the `MapAGUI` extension method to expose an agent through the AG-UI protocol:
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(
instructions: "You are a helpful assistant.",
name: "AGUIAssistant");
app.MapAGUI("/", agent);
```
This automatically handles:
- HTTP POST requests with message payloads
- Converting agent responses to AG-UI event streams
- Server-sent events (SSE) formatting
- Thread and run management
### Client Side
The `AGUIClient` uses the `AGUIChatClient` to connect to the remote server:
```csharp
using HttpClient httpClient = new();
var chatClient = new AGUIChatClient(
httpClient,
endpoint: serverUrl,
modelId: "agui-client",
jsonSerializerOptions: null);
AIAgent agent = chatClient.CreateAIAgent(
instructions: null,
name: "agui-client",
description: "AG-UI Client Agent",
tools: []);
bool isFirstUpdate = true;
AgentRunResponseUpdate? currentUpdate = null;
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
// First update indicates run started
if (isFirstUpdate)
{
Console.WriteLine($"[Run Started - Thread: {update.ConversationId}, Run: {update.ResponseId}]");
isFirstUpdate = false;
}
currentUpdate = update;
foreach (AIContent content in update.Contents)
{
switch (content)
{
case TextContent textContent:
// Display streaming text
Console.Write(textContent.Text);
break;
case ErrorContent errorContent:
// Display error notification
Console.WriteLine($"[Error: {errorContent.Message}]");
break;
}
}
}
// Last update indicates run finished
if (currentUpdate != null)
{
Console.WriteLine($"\n[Run Finished - Thread: {currentUpdate.ConversationId}, Run: {currentUpdate.ResponseId}]");
}
```
The `RunStreamingAsync` method:
1. Sends messages to the server via HTTP POST
2. Receives server-sent events (SSE) stream
3. Parses events into `AgentRunResponseUpdate` objects
4. Yields updates as they arrive for real-time display
## Key Concepts
- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
- **AgentRunResponseUpdate**: Contains the response data with:
- `ResponseId`: The unique run identifier
- `ConversationId`: The thread/conversation identifier
- `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
- **Run Lifecycle**:
- The **first** `AgentRunResponseUpdate` in a run indicates the run has started
- Subsequent updates contain streaming content as the agent processes
- The **last** `AgentRunResponseUpdate` in a run indicates the run has finished
- If an error occurs, the update will contain `ErrorContent`
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@@ -13,7 +13,7 @@
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<ProjectReference Include="..\AgentWebChat.ServiceDefaults\AgentWebChat.ServiceDefaults.csproj" />
</ItemGroup>
@@ -37,4 +37,4 @@
</ItemGroup>
<!-- A2A dependency -->
</Project>
</Project>
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace AgentWebChat.AgentHost.Custom;
public class CustomAITool : AITool
{
}
public class CustomFunctionTool : AIFunction
{
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
{
return new ValueTask<object?>(arguments.Context?.Count ?? 0);
}
}
@@ -2,6 +2,7 @@
using A2A.AspNetCore;
using AgentWebChat.AgentHost;
using AgentWebChat.AgentHost.Custom;
using AgentWebChat.AgentHost.Utilities;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
@@ -20,14 +21,16 @@ builder.Services.AddProblemDetails();
// Configure the chat model and our agent.
builder.AddKeyedChatClient("chat-model");
builder.AddAIAgent(
var pirateAgentBuilder = builder.AddAIAgent(
"pirate",
instructions: "You are a pirate. Speak like a pirate",
description: "An agent that speaks like a pirate.",
chatClientServiceKey: "chat-model")
.WithAITool(new CustomAITool())
.WithAITool(new CustomFunctionTool())
.WithInMemoryThreadStore();
builder.AddAIAgent("knights-and-knaves", (sp, key) =>
var knightsKnavesAgentBuilder = builder.AddAIAgent("knights-and-knaves", (sp, key) =>
{
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
@@ -78,8 +81,21 @@ var literatureAgent = builder.AddAIAgent("literator",
description: "An agent that helps with literature.",
chatClientServiceKey: "chat-model");
builder.AddSequentialWorkflow("science-sequential-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
builder.AddConcurrentWorkflow("science-concurrent-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
var scienceSequentialWorkflow = builder.AddWorkflow("science-sequential-workflow", (sp, key) =>
{
List<IHostedAgentBuilder> usedAgents = [chemistryAgent, mathsAgent, literatureAgent];
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents);
}).AddAsAIAgent();
var scienceConcurrentWorkflow = builder.AddWorkflow("science-concurrent-workflow", (sp, key) =>
{
List<IHostedAgentBuilder> usedAgents = [chemistryAgent, mathsAgent, literatureAgent];
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
return AgentWorkflowBuilder.BuildConcurrent(workflowName: key, agents: agents);
}).AddAsAIAgent();
builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
var app = builder.Build();
@@ -91,8 +107,8 @@ app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Agents
app.UseExceptionHandler();
// attach a2a with simple message communication
app.MapA2A(agentName: "pirate", path: "/a2a/pirate");
app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", agentCard: new()
app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate");
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard: new()
{
Name = "Knights and Knaves",
Description = "An agent that helps you solve the knights and knaves puzzle.",
@@ -104,8 +120,8 @@ app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", age
app.MapOpenAIResponses();
app.MapOpenAIChatCompletions("pirate");
app.MapOpenAIChatCompletions("knights-and-knaves");
app.MapOpenAIChatCompletions(pirateAgentBuilder);
app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder);
// Map the agents HTTP endpoints
app.MapAgentDiscovery("/agents");
@@ -0,0 +1,10 @@
# .editorconfig
[*.cs]
# See https://github.com/Azure/azure-functions-durable-extension/issues/3173
dotnet_diagnostic.DURABLE0001.severity = none
dotnet_diagnostic.DURABLE0002.severity = none
dotnet_diagnostic.DURABLE0003.severity = none
dotnet_diagnostic.DURABLE0004.severity = none
dotnet_diagnostic.DURABLE0005.severity = none
dotnet_diagnostic.DURABLE0006.severity = none
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>SingleAgent</AssemblyName>
<RootNamespace>SingleAgent</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstructions, JokerName);
// Configure the function app to host the AI agent.
// This will automatically generate HTTP API endpoints for the agent.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.Build();
app.Run();
@@ -0,0 +1,89 @@
# Single Agent Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
## Key Concepts Demonstrated
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
- Registering agents with the Function app and running them using HTTP.
- Conversation management (via session IDs) for isolated interactions.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/agents/Joker/run \
-H "Content-Type: text/plain" \
-d "Tell me a joke about a pirate."
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/agents/Joker/run `
-ContentType text/plain `
-Body "Tell me a joke about a pirate."
```
You can also send JSON requests:
```bash
curl -X POST http://localhost:7071/api/agents/Joker/run \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"message": "Tell me a joke about a pirate."}'
```
To continue a conversation, include the `thread_id` in the query string or JSON body:
```bash
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=@dafx-joker@your-thread-id" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"message": "Tell me another one."}'
```
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
```text
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
```
The expected `application/json` output will look something like:
```json
{
"status": 200,
"thread_id": "@dafx-joker@your-thread-id",
"response": {
"Messages": [
{
"AuthorName": "Joker",
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
"Role": "assistant",
"Contents": [
{
"Type": "text",
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
}
]
}
],
"Usage": {
"InputTokenCount": 78,
"OutputTokenCount": 36,
"TotalTokenCount": 114
}
}
}
```
@@ -0,0 +1,8 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Prompt the agent
POST {{authority}}/api/agents/Joker/run
Content-Type: text/plain
Tell me a joke about a pirate.
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_Chaining</AssemblyName>
<RootNamespace>AgentOrchestration_Chaining</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace AgentOrchestration_Chaining;
public static class FunctionTriggers
{
public sealed record TextResponse(string Text);
[Function(nameof(RunOrchestrationAsync))]
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentThread writerThread = writer.GetNewThread();
AgentRunResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
thread: writerThread);
AgentRunResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
thread: writerThread);
return refined.Result.Text;
}
// POST /singleagent/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "singleagent/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync));
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "Single-agent orchestration started.",
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// GET /singleagent/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "singleagent/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/singleagent/status/{instanceId}";
}
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
You refine short pieces of text. When given an initial sentence you enhance it;
when given an improved sentence you polish it further.
""";
AIAgent writerAgent = client.GetChatClient(deploymentName).CreateAIAgent(WriterInstructions, WriterName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
.Build();
app.Run();
@@ -0,0 +1,59 @@
# Single Agent Orchestration Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
## Key Concepts Demonstrated
- Orchestrating multiple interactions with the same agent in a deterministic order
- Using the same `AgentThread` across multiple calls to maintain conversational context
- Durable orchestration with automatic checkpointing and resumption from failures
- HTTP API integration for starting and monitoring orchestrations
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request to start the orchestration.
You can use the `demo.http` file to start the orchestration, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/singleagent/run
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post -Uri http://localhost:7071/api/singleagent/run
```
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "Single-agent orchestration started.",
"instanceId": "86313f1d45fb42eeb50b1852626bf3ff",
"statusQueryGetUri": "http://localhost:7071/api/singleagent/status/86313f1d45fb42eeb50b1852626bf3ff"
}
```
The orchestration will proceed to run the WriterAgent twice in sequence:
1. First, it writes an inspirational sentence about learning
2. Then, it refines the initial output using the same conversation thread
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": null,
"instanceId": "86313f1d45fb42eeb50b1852626bf3ff",
"output": "Learning serves as the key, opening doors to boundless opportunities and a brighter future.",
"runtimeStatus": "Completed"
}
```
@@ -0,0 +1,3 @@
### Start the single-agent orchestration
POST http://localhost:7071/api/singleagent/run
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_Concurrency</AssemblyName>
<RootNamespace>AgentOrchestration_Concurrency</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace AgentOrchestration_Concurrency;
public static class FunctionsTriggers
{
public sealed record TextResponse(string Text);
[Function(nameof(RunOrchestrationAsync))]
public static async Task<object> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the prompt from the orchestration input
string prompt = context.GetInput<string>() ?? throw new InvalidOperationException("Prompt is required");
// Get both agents
DurableAIAgent physicist = context.GetAgent("PhysicistAgent");
DurableAIAgent chemist = context.GetAgent("ChemistAgent");
// Start both agent runs concurrently
Task<AgentRunResponse<TextResponse>> physicistTask = physicist.RunAsync<TextResponse>(prompt);
Task<AgentRunResponse<TextResponse>> chemistTask = chemist.RunAsync<TextResponse>(prompt);
// Wait for both tasks to complete using Task.WhenAll
await Task.WhenAll(physicistTask, chemistTask);
// Get the results
TextResponse physicistResponse = (await physicistTask).Result;
TextResponse chemistResponse = (await chemistTask).Result;
// Return the result as a structured, anonymous type
return new
{
physicist = physicistResponse.Text,
chemist = chemistResponse.Text,
};
}
// POST /multiagent/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "multiagent/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
// Read the prompt from the request body
string? prompt = await req.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(prompt))
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Prompt is required" });
return badRequestResponse;
}
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync),
input: prompt);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "Multi-agent concurrent orchestration started.",
prompt,
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// GET /multiagent/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "multiagent/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/multiagent/status/{instanceId}";
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Two agents used by the orchestration to demonstrate concurrent execution.
const string PhysicistName = "PhysicistAgent";
const string PhysicistInstructions = "You are an expert in physics. You answer questions from a physics perspective.";
const string ChemistName = "ChemistAgent";
const string ChemistInstructions = "You are an expert in chemistry. You answer questions from a chemistry perspective.";
AIAgent physicistAgent = client.GetChatClient(deploymentName).CreateAIAgent(PhysicistInstructions, PhysicistName);
AIAgent chemistAgent = client.GetChatClient(deploymentName).CreateAIAgent(ChemistInstructions, ChemistName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
options.AddAIAgent(physicistAgent);
options.AddAIAgent(chemistAgent);
})
.Build();
app.Run();
@@ -0,0 +1,65 @@
# Multi-Agent Concurrent Orchestration Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create an Azure Functions app that orchestrates concurrent execution of multiple AI agents, each with specialized expertise, to provide comprehensive answers to complex questions.
## Key Concepts Demonstrated
- Multi-agent orchestration with specialized AI agents (physics and chemistry)
- Concurrent execution using the fan-out/fan-in pattern for improved performance and distributed processing
- Response aggregation from multiple agents into a unified result
- Durable orchestration with automatic checkpointing and resumption from failures
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request with a custom prompt to the orchestration.
You can use the `demo.http` file to send a message to the agents, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/multiagent/run \
-H "Content-Type: text/plain" \
-d "What is temperature?"
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/multiagent/run `
-ContentType text/plain `
-Body "What is temperature?"
```
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "Multi-agent concurrent orchestration started.",
"prompt": "What is temperature?",
"instanceId": "e7e29999b6b8424682b3539292afc9ed",
"statusQueryGetUri": "http://localhost:7071/api/multiagent/status/e7e29999b6b8424682b3539292afc9ed"
}
```
The orchestration will run both the PhysicistAgent and ChemistAgent concurrently, asking them the same question. Their responses will be combined to provide a comprehensive answer covering both physical and chemical aspects.
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": "What is temperature?",
"instanceId": "e7e29999b6b8424682b3539292afc9ed",
"output": {
"physicist": "Temperature is a measure of the average kinetic energy of particles in a system. From a physics perspective, it represents the thermal energy and determines the direction of heat flow between objects.",
"chemist": "From a chemistry perspective, temperature is crucial for chemical reactions as it affects reaction rates through the Arrhenius equation. It influences the equilibrium position of reversible reactions and determines the physical state of substances."
},
"runtimeStatus": "Completed"
}
```
@@ -0,0 +1,5 @@
### Start the multi-agent concurrent orchestration
POST http://localhost:7071/api/multiagent/run
Content-Type: text/plain
What is temperature?
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_Conditionals</AssemblyName>
<RootNamespace>AgentOrchestration_Conditionals</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace AgentOrchestration_Conditionals;
public static class FunctionTriggers
{
[Function(nameof(RunOrchestrationAsync))]
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the email from the orchestration input
Email email = context.GetInput<Email>() ?? throw new InvalidOperationException("Email is required");
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentThread spamThread = spamDetectionAgent.GetNewThread();
// Step 1: Check if the email is spam
AgentRunResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
message:
$"""
Analyze this email for spam content and return a JSON response with 'is_spam' (boolean) and 'reason' (string) fields:
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
thread: spamThread);
DetectionResult result = spamDetectionResponse.Result;
// Step 2: Conditional logic based on spam detection result
if (result.IsSpam)
{
// Handle spam email
return await context.CallActivityAsync<string>(nameof(HandleSpamEmail), result.Reason);
}
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentThread emailThread = emailAssistantAgent.GetNewThread();
AgentRunResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
$"""
Draft a professional response to this email. Return a JSON response with a 'response' field containing the reply:
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
thread: emailThread);
EmailResponse emailResponse = emailAssistantResponse.Result;
return await context.CallActivityAsync<string>(nameof(SendEmail), emailResponse.Response);
}
[Function(nameof(HandleSpamEmail))]
public static string HandleSpamEmail([ActivityTrigger] string reason)
{
return $"Email marked as spam: {reason}";
}
[Function(nameof(SendEmail))]
public static string SendEmail([ActivityTrigger] string message)
{
return $"Email sent: {message}";
}
// POST /spamdetection/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "spamdetection/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
// Read the email from the request body
Email? email = await req.ReadFromJsonAsync<Email>();
if (email is null || string.IsNullOrWhiteSpace(email.EmailContent))
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Email with content is required" });
return badRequestResponse;
}
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync),
input: email);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "Spam detection orchestration started.",
emailId = email.EmailId,
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// GET /spamdetection/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "spamdetection/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/spamdetection/status/{instanceId}";
}
}
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AgentOrchestration_Conditionals;
/// <summary>
/// Represents an email input for spam detection and response generation.
/// </summary>
public sealed class Email
{
[JsonPropertyName("email_id")]
public string EmailId { get; set; } = string.Empty;
[JsonPropertyName("email_content")]
public string EmailContent { get; set; } = string.Empty;
}
/// <summary>
/// Represents the result of spam detection analysis.
/// </summary>
public sealed class DetectionResult
{
[JsonPropertyName("is_spam")]
public bool IsSpam { get; set; }
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
}
/// <summary>
/// Represents a generated email response.
/// </summary>
public sealed class EmailResponse
{
[JsonPropertyName("response")]
public string Response { get; set; } = string.Empty;
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Two agents used by the orchestration to demonstrate conditional logic.
const string SpamDetectionName = "SpamDetectionAgent";
const string SpamDetectionInstructions = "You are a spam detection assistant that identifies spam emails.";
const string EmailAssistantName = "EmailAssistantAgent";
const string EmailAssistantInstructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
AIAgent spamDetectionAgent = client.GetChatClient(deploymentName)
.CreateAIAgent(SpamDetectionInstructions, SpamDetectionName);
AIAgent emailAssistantAgent = client.GetChatClient(deploymentName)
.CreateAIAgent(EmailAssistantInstructions, EmailAssistantName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
options.AddAIAgent(spamDetectionAgent);
options.AddAIAgent(emailAssistantAgent);
})
.Build();
app.Run();
@@ -0,0 +1,113 @@
# Multi-Agent Orchestration with Conditionals Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a multi-agent orchestration workflow that includes conditional logic. The workflow implements a spam detection system that processes emails and takes different actions based on whether the email is identified as spam or legitimate.
## Key Concepts Demonstrated
- Multi-agent orchestration with conditional logic and different processing paths
- Spam detection using AI agent analysis
- Structured output from agents for reliable processing
- Activity functions for integrating non-agentic workflow actions
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request with email data to the orchestration.
You can use the `demo.http` file to send email data to the agents, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
# Test with a legitimate email
curl -X POST http://localhost:7071/api/spamdetection/run \
-H "Content-Type: application/json" \
-d '{
"email_id": "email-001",
"email_content": "Hi John, I hope you are doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
}'
# Test with a spam email
curl -X POST http://localhost:7071/api/spamdetection/run \
-H "Content-Type: application/json" \
-d '{
"email_id": "email-002",
"email_content": "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!"
}'
```
PowerShell:
```powershell
# Test with a legitimate email
$body = @{
email_id = "email-001"
email_content = "Hi John, I hope you are doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/spamdetection/run `
-ContentType application/json `
-Body $body
# Test with a spam email
$body = @{
email_id = "email-002"
email_content = "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/spamdetection/run `
-ContentType application/json `
-Body $body
```
The response from either input will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "Spam detection orchestration started.",
"emailId": "email-001",
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
"statusQueryGetUri": "http://localhost:7071/api/spamdetection/status/555dbbb63f75406db2edf9f1f092de95"
}
```
The orchestration will:
1. Analyze the email content using the SpamDetectionAgent
2. If spam: Mark the email as spam with a reason
3. If legitimate: Use the EmailAssistantAgent to draft a professional response and "send" it
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response for the legitimate email will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": {
"email_content": "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!",
"email_id": "email-001"
},
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
"output": "Email sent: Subject: Re: Follow-Up on Quarterly Report\n\nHi [Recipient's Name],\n\nI hope this message finds you well. Thank you for your patience. I will ensure the updated figures for the quarterly report are sent to you by Friday.\n\nIf you have any further questions or need additional information, please feel free to reach out.\n\nBest regards,\n\nJohn",
"runtimeStatus": "Completed"
}
```
The response for the spam email will be a JSON object that looks something like the following, which indicates that the email was marked as spam:
```json
{
"failureDetails": null,
"input": {
"email_content": "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!",
"email_id": "email-002"
},
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
"output": "Email marked as spam: The email contains misleading claims of winning a large sum of money and encourages immediate action, which are common characteristics of spam.",
"runtimeStatus": "Completed"
}
```
@@ -0,0 +1,18 @@
### Test spam detection with a legitimate email
POST http://localhost:7071/api/spamdetection/run
Content-Type: application/json
{
"email_id": "email-001",
"email_content": "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
}
### Test spam detection with a spam email
POST http://localhost:7071/api/spamdetection/run
Content-Type: application/json
{
"email_id": "email-002",
"email_content": "URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
}
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_HITL</AssemblyName>
<RootNamespace>AgentOrchestration_HITL</RootNamespace>
<NoWarn>$(NoWarn);DURABLE0001;DURABLE0002;DURABLE0003;DURABLE0004;DURABLE0005;DURABLE0006</NoWarn>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,229 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace AgentOrchestration_HITL;
public static class FunctionTriggers
{
[Function(nameof(RunOrchestrationAsync))]
public static async Task<object> RunOrchestrationAsync(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the input from the orchestration
ContentGenerationInput input = context.GetInput<ContentGenerationInput>()
?? throw new InvalidOperationException("Content generation input is required");
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
AgentThread writerThread = writerAgent.GetNewThread();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
// Step 1: Generate initial content
AgentRunResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
int iterationCount = 0;
while (iterationCount++ < input.MaxReviewAttempts)
{
context.SetCustomStatus(
$"Requesting human feedback. Iteration #{iterationCount}. Timeout: {input.ApprovalTimeoutHours} hour(s).");
// Step 2: Notify user to review the content
await context.CallActivityAsync(nameof(NotifyUserForApproval), content);
// Step 3: Wait for human feedback with configurable timeout
HumanApprovalResponse humanResponse;
try
{
humanResponse = await context.WaitForExternalEvent<HumanApprovalResponse>(
eventName: "HumanApproval",
timeout: TimeSpan.FromHours(input.ApprovalTimeoutHours));
}
catch (OperationCanceledException)
{
// Timeout occurred - treat as rejection
context.SetCustomStatus(
$"Human approval timed out after {input.ApprovalTimeoutHours} hour(s). Treating as rejection.");
throw new TimeoutException($"Human approval timed out after {input.ApprovalTimeoutHours} hour(s).");
}
if (humanResponse.Approved)
{
context.SetCustomStatus("Content approved by human reviewer. Publishing content...");
// Step 4: Publish the approved content
await context.CallActivityAsync(nameof(PublishContent), content);
context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime:s}");
return new { content = content.Content };
}
context.SetCustomStatus("Content rejected by human reviewer. Incorporating feedback and regenerating...");
// Incorporate human feedback and regenerate
writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"""
The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.
Human Feedback: {humanResponse.Feedback}
""",
thread: writerThread);
content = writerResponse.Result;
}
// If we reach here, it means we exhausted the maximum number of iterations
throw new InvalidOperationException(
$"Content could not be approved after {input.MaxReviewAttempts} iterations.");
}
// POST /hitl/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
// Read the input from the request body
ContentGenerationInput? input = await req.ReadFromJsonAsync<ContentGenerationInput>();
if (input is null || string.IsNullOrWhiteSpace(input.Topic))
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Topic is required" });
return badRequestResponse;
}
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync),
input: input);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "HITL content generation orchestration started.",
topic = input.Topic,
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// POST /hitl/approve/{instanceId}
[Function(nameof(SendHumanApprovalAsync))]
public static async Task<HttpResponseData> SendHumanApprovalAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/approve/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
// Read the approval response from the request body
HumanApprovalResponse? approvalResponse = await req.ReadFromJsonAsync<HumanApprovalResponse>();
if (approvalResponse is null)
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Approval response is required" });
return badRequestResponse;
}
// Send the approval event to the orchestration
await client.RaiseEventAsync(instanceId, "HumanApproval", approvalResponse);
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
message = "Human approval sent to orchestration.",
instanceId,
approved = approvalResponse.Approved
});
return response;
}
// GET /hitl/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "hitl/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
workflowStatus = status.SerializedCustomStatus is not null ? (object)status.ReadCustomStatusAs<JsonElement>() : null,
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
[Function(nameof(NotifyUserForApproval))]
public static void NotifyUserForApproval(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(NotifyUserForApproval));
// In a real implementation, this would send notifications via email, SMS, etc.
logger.LogInformation(
"""
NOTIFICATION: Please review the following content for approval:
Title: {Title}
Content: {Content}
Use the approval endpoint to approve or reject this content.
""",
content.Title,
content.Content);
}
[Function(nameof(PublishContent))]
public static void PublishContent(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(PublishContent));
// In a real implementation, this would publish to a CMS, website, etc.
logger.LogInformation(
"""
PUBLISHING: Content has been published successfully.
Title: {Title}
Content: {Content}
""",
content.Title,
content.Content);
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/hitl/status/{instanceId}";
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AgentOrchestration_HITL;
/// <summary>
/// Represents the input for the Human-in-the-Loop content generation workflow.
/// </summary>
public sealed class ContentGenerationInput
{
[JsonPropertyName("topic")]
public string Topic { get; set; } = string.Empty;
[JsonPropertyName("max_review_attempts")]
public int MaxReviewAttempts { get; set; } = 3;
[JsonPropertyName("approval_timeout_hours")]
public float ApprovalTimeoutHours { get; set; } = 72;
}
/// <summary>
/// Represents the content generated by the writer agent.
/// </summary>
public sealed class GeneratedContent
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
/// <summary>
/// Represents the human approval response.
/// </summary>
public sealed class HumanApprovalResponse
{
[JsonPropertyName("approved")]
public bool Approved { get; set; }
[JsonPropertyName("feedback")]
public string Feedback { get; set; } = string.Empty;
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Single agent used by the orchestration to demonstrate human-in-the-loop workflow.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
You are a professional content writer who creates high-quality articles on various topics.
You write engaging, informative, and well-structured content that follows best practices for readability and accuracy.
""";
AIAgent writerAgent = client.GetChatClient(deploymentName).CreateAIAgent(WriterInstructions, WriterName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
.Build();
app.Run();
@@ -0,0 +1,126 @@
# Multi-Agent Orchestration with Human-in-the-Loop Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a human-in-the-loop (HITL) workflow using a single AI agent. The workflow uses a writer agent to generate content and requires human approval on every iteration, emphasizing the human-in-the-loop pattern.
## Key Concepts Demonstrated
- Single-agent orchestration
- Human-in-the-loop feedback loop using external events (`WaitForExternalEvent`)
- Activity functions for non-agentic workflow steps
- Iterative content refinement based on human feedback
- Custom status tracking for workflow visibility
- Error handling with maximum retry attempts and timeout handling for human approval
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request with a topic to start the content generation workflow.
You can use the `demo.http` file to send a topic to the agents, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/hitl/run \
-H "Content-Type: application/json" \
-d '{
"topic": "The Future of Artificial Intelligence",
"max_review_attempts": 3,
"timeout_minutes": 5
}'
```
PowerShell:
```powershell
$body = @{
topic = "The Future of Artificial Intelligence"
max_review_attempts = 3
timeout_minutes = 5
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/hitl/run `
-ContentType application/json `
-Body $body
```
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "HITL content generation orchestration started.",
"topic": "The Future of Artificial Intelligence",
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"statusQueryGetUri": "http://localhost:7071/api/hitl/status/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}
```
The orchestration will:
1. Generate initial content using the WriterAgent
2. Notify the user to review the content
3. Wait for human feedback via external event (configurable timeout)
4. If approved by human, publish the content
5. If rejected by human, incorporate feedback and regenerate content
6. If approval timeout occurs, treat as rejection and fail the orchestration
7. Repeat until human approval is received or maximum loop iterations are reached
Once the orchestration is waiting for human approval, you can send approval or rejection using the approval endpoint:
Bash (Linux/macOS/WSL):
```bash
# Approve the content
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
-H "Content-Type: application/json" \
-d '{
"approved": true,
"feedback": "Great article! The content is well-structured and informative."
}'
# Reject the content with feedback
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
-H "Content-Type: application/json" \
-d '{
"approved": false,
"feedback": "The article needs more technical depth and better examples."
}'
```
PowerShell:
```powershell
# Approve the content
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
-ContentType application/json `
-Body '{ "approved": true, "feedback": "Great article! The content is well-structured and informative." }'
# Reject the content with feedback
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
-ContentType application/json `
-Body '{ "approved": false, "feedback": "The article needs more technical depth and better examples." }'
```
Once the orchestration has completed, you can get the status by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": {
"topic": "The Future of Artificial Intelligence",
"max_review_attempts": 3
},
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"output": {
"content": "The Future of Artificial Intelligence is..."
},
"runtimeStatus": "Completed",
"workflowStatus": "Content published successfully at 2025-10-15T12:00:00Z"
}
```
@@ -0,0 +1,44 @@
### Start the HITL content generation orchestration with default timeout (30 days)
POST http://localhost:7071/api/hitl/run
Content-Type: application/json
{
"topic": "The Future of Artificial Intelligence",
"max_review_attempts": 3
}
### Start the HITL content generation orchestration with very short timeout for demonstration (~4 seconds)
POST http://localhost:7071/api/hitl/run
Content-Type: application/json
{
"topic": "The Future of Artificial Intelligence",
"max_review_attempts": 3,
"approval_timeout_hours": 0.001
}
### Copy/paste the instanceId from the response above
@instanceId=INSTANCE_ID_GOES_HERE
### Check the status of the orchestration (replace {instanceId} with the actual instance ID from the response above)
GET http://localhost:7071/api/hitl/status/{{instanceId}}
### Send human approval (replace {instanceId} with the actual instance ID)
POST http://localhost:7071/api/hitl/approve/{{instanceId}}
Content-Type: application/json
{
"approved": true,
"feedback": "Great article! The content is well-structured and informative."
}
### Send human rejection with feedback (replace {instanceId} with the actual instance ID)
POST http://localhost:7071/api/hitl/approve/{{instanceId}}
Content-Type: application/json
{
"approved": false,
"feedback": "The article needs more technical depth and better examples. Please add more specific use cases and implementation details."
}
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>LongRunningTools</AssemblyName>
<RootNamespace>LongRunningTools</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,151 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
namespace LongRunningTools;
public static class FunctionTriggers
{
[Function(nameof(RunOrchestrationAsync))]
public static async Task<object> RunOrchestrationAsync(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the input from the orchestration
ContentGenerationInput input = context.GetInput<ContentGenerationInput>()
?? throw new InvalidOperationException("Content generation input is required");
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("Writer");
AgentThread writerThread = writerAgent.GetNewThread();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
// Step 1: Generate initial content
AgentRunResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
int iterationCount = 0;
while (iterationCount++ < input.MaxReviewAttempts)
{
context.SetCustomStatus(
new
{
message = "Requesting human feedback.",
approvalTimeoutHours = input.ApprovalTimeoutHours,
iterationCount,
content
});
// Step 2: Notify user to review the content
await context.CallActivityAsync(nameof(NotifyUserForApproval), content);
// Step 3: Wait for human feedback with configurable timeout
HumanApprovalResponse humanResponse;
try
{
humanResponse = await context.WaitForExternalEvent<HumanApprovalResponse>(
eventName: "HumanApproval",
timeout: TimeSpan.FromHours(input.ApprovalTimeoutHours));
}
catch (OperationCanceledException)
{
// Timeout occurred - treat as rejection
context.SetCustomStatus(
new
{
message = $"Human approval timed out after {input.ApprovalTimeoutHours} hour(s). Treating as rejection.",
iterationCount,
content
});
throw new TimeoutException($"Human approval timed out after {input.ApprovalTimeoutHours} hour(s).");
}
if (humanResponse.Approved)
{
context.SetCustomStatus(new
{
message = "Content approved by human reviewer. Publishing content...",
content
});
// Step 4: Publish the approved content
await context.CallActivityAsync(nameof(PublishContent), content);
context.SetCustomStatus(new
{
message = $"Content published successfully at {context.CurrentUtcDateTime:s}",
humanFeedback = humanResponse,
content
});
return new { content = content.Content };
}
context.SetCustomStatus(new
{
message = "Content rejected by human reviewer. Incorporating feedback and regenerating...",
humanFeedback = humanResponse,
content
});
// Incorporate human feedback and regenerate
writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"""
The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.
Human Feedback: {humanResponse.Feedback}
""",
thread: writerThread);
content = writerResponse.Result;
}
// If we reach here, it means we exhausted the maximum number of iterations
throw new InvalidOperationException(
$"Content could not be approved after {input.MaxReviewAttempts} iterations.");
}
[Function(nameof(NotifyUserForApproval))]
public static void NotifyUserForApproval(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(NotifyUserForApproval));
// In a real implementation, this would send notifications via email, SMS, etc.
logger.LogInformation(
"""
NOTIFICATION: Please review the following content for approval:
Title: {Title}
Content: {Content}
Use the approval endpoint to approve or reject this content.
""",
content.Title,
content.Content);
}
[Function(nameof(PublishContent))]
public static void PublishContent(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(PublishContent));
// In a real implementation, this would publish to a CMS, website, etc.
logger.LogInformation(
"""
PUBLISHING: Content has been published successfully.
Title: {Title}
Content: {Content}
""",
content.Title,
content.Content);
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace LongRunningTools;
/// <summary>
/// Represents the input for the content generation workflow.
/// </summary>
public sealed class ContentGenerationInput
{
[JsonPropertyName("topic")]
public string Topic { get; set; } = string.Empty;
[JsonPropertyName("max_review_attempts")]
public int MaxReviewAttempts { get; set; } = 3;
[JsonPropertyName("approval_timeout_hours")]
public float ApprovalTimeoutHours { get; set; } = 72;
}
/// <summary>
/// Represents the content generated by the writer agent.
/// </summary>
public sealed class GeneratedContent
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
/// <summary>
/// Represents the human approval response.
/// </summary>
public sealed class HumanApprovalResponse
{
[JsonPropertyName("approved")]
public bool Approved { get; set; }
[JsonPropertyName("feedback")]
public string Feedback { get; set; } = string.Empty;
}
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using LongRunningTools;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Agent used by the orchestration to write content.
const string WriterAgentName = "Writer";
const string WriterAgentInstructions =
"""
You are a professional content writer who creates high-quality articles on various topics.
You write engaging, informative, and well-structured content that follows best practices for readability and accuracy.
""";
AIAgent writerAgent = client.GetChatClient(deploymentName).CreateAIAgent(WriterAgentInstructions, WriterAgentName);
// Agent that can start content generation workflows using tools
const string PublisherAgentName = "Publisher";
const string PublisherAgentInstructions =
"""
You are a publishing agent that can manage content generation workflows.
You have access to tools to start, monitor, and raise events for content generation workflows.
""";
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
// Add the writer agent used by the orchestration
options.AddAIAgent(writerAgent);
// Define the agent that can start orchestrations from tool calls
options.AddAIAgentFactory(PublisherAgentName, sp =>
{
// Initialize the tools to be used by the agent.
Tools publisherTools = new(sp.GetRequiredService<ILogger<Tools>>());
return client.GetChatClient(deploymentName).CreateAIAgent(
instructions: PublisherAgentInstructions,
name: PublisherAgentName,
services: sp,
tools: [
AIFunctionFactory.Create(publisherTools.StartContentGenerationWorkflow),
AIFunctionFactory.Create(publisherTools.GetWorkflowStatusAsync),
AIFunctionFactory.Create(publisherTools.SubmitHumanApprovalAsync),
]);
});
})
.Build();
app.Run();
@@ -0,0 +1,129 @@
# Long Running Tools Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create agents with long running tools. This sample builds on the [05_AgentOrchestration_HITL](../05_AgentOrchestration_HITL) sample by adding a publisher agent that can start and manage content generation workflows. A key difference is that the publisher agent knows the IDs of the workflows it starts, so it can check the status of the workflows and approve or reject them without being explicitly given the context (instance IDs, etc).
## Key Concepts Demonstrated
The same key concepts as the [05_AgentOrchestration_HITL](../05_AgentOrchestration_HITL) sample are demonstrated, but with the following additional concepts:
- **Long running tools**: Using `DurableAgentContext.Current` to start orchestrations from tool calls
- **Multi-agent orchestration**: Agents can start and manage workflows that orchestrate other agents
- **Human-in-the-loop (with delegation)**: The agent acts as an intermediary between the human and the workflow. The human remains in the loop, but delegates to the agent to start the workflow and approve or reject the content.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request to start the agent, which will then trigger the content generation workflow.
You can use the `demo.http` file to send requests to the agent, or a command line tool like `curl` as shown below.
Bash (Linux/macOS/WSL):
```bash
curl -i -X POST http://localhost:7071/api/agents/publisher/run \
-D headers.txt \
-H "Content-Type: text/plain" \
-d 'Start a content generation workflow for the topic \"The Future of Artificial Intelligence\"'
# Save the thread ID to a variable and print it to the terminal
threadId=$(cat headers.txt | grep "x-ms-thread-id" | cut -d' ' -f2)
echo "Thread ID: $threadId"
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/agents/publisher/run `
-ResponseHeadersVariable ResponseHeaders `
-ContentType text/plain `
-Body 'Start a content generation workflow for the topic \"The Future of Artificial Intelligence\"' `
# Save the thread ID to a variable and print it to the console
$threadId = $ResponseHeaders['x-ms-thread-id']
Write-Host "Thread ID: $threadId"
```
The response will be a text string that looks something like the following, indicating that the agent request has been received and will be processed:
```http
HTTP/1.1 200 OK
Content-Type: text/plain
x-ms-thread-id: @publisher@351ec855-7f4d-4527-a60d-498301ced36d
The content generation workflow for the topic "The Future of Artificial Intelligence" has been successfully started, and the instance ID is **6a04276e8d824d8d941e1dc4142cc254**. If you need any further assistance or updates on the workflow, feel free to ask!
```
The `x-ms-thread-id` response header contains the thread ID, which can be used to continue the conversation by passing it as a query parameter (`thread_id`) to the `run` endpoint. The commands above show how to save the thread ID to a `$threadId` variable for use in subsequent requests.
Behind the scenes, the publisher agent will:
1. Start the content generation workflow via a tool call
1. The workflow will generate initial content using the Writer agent and wait for human approval, which will be visible in the logs
Once the workflow is waiting for human approval, you can send approval or rejection by prompting the publisher agent accordingly (e.g. "Approve the content" or "Reject the content with feedback: The article needs more technical depth and better examples."):
Bash (Linux/macOS/WSL):
```bash
# Approve the content
curl -X POST "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" \
-H "Content-Type: text/plain" \
-d 'Approve the content'
# Reject the content with feedback
curl -X POST "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" \
-H "Content-Type: text/plain" \
-d 'Reject the content with feedback: The article needs more technical depth and better examples.'
```
PowerShell:
```powershell
# Approve the content
Invoke-RestMethod -Method Post `
-Uri "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" `
-ContentType text/plain `
-Body 'Approve the content'
# Reject the content with feedback
Invoke-RestMethod -Method Post `
-Uri "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" `
-ContentType text/plain `
-Body 'Reject the content with feedback: The article needs more technical depth and better examples.'
```
Once the workflow has completed, you can get the status by prompting the publisher agent to give you the status.
Bash (Linux/macOS/WSL):
```bash
curl -X POST "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" \
-H "Content-Type: text/plain" \
-d 'Get the status of the workflow you previously started'
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" `
-ContentType text/plain `
-Body 'Get the status of the workflow you previously started'
```
The response from the publisher agent will look something like the following:
```text
The status of the workflow with instance ID **ab1076d6e7ec49d8a2c2474d09b69ded** is as follows:
- **Execution Status:** Completed
- **Workflow Status:** Content published successfully at `2025-10-24T20:42:02`
- **Created At:** `2025-10-24T20:41:40.7531781+00:00`
- **Last Updated At:** `2025-10-24T20:42:02.1410736+00:00`
The content has been successfully published.
```
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace LongRunningTools;
/// <summary>
/// Tools that demonstrate starting orchestrations from agent tool calls.
/// </summary>
internal sealed class Tools(ILogger<Tools> logger)
{
private readonly ILogger<Tools> _logger = logger;
[Description("Starts a content generation workflow and returns the instance ID for tracking.")]
public string StartContentGenerationWorkflow([Description("The topic for content generation")] string topic)
{
this._logger.LogInformation("Starting content generation workflow for topic: {Topic}", topic);
const int MaxReviewAttempts = 3;
const float ApprovalTimeoutHours = 72;
// Schedule the orchestration, which will start running after the tool call completes.
string instanceId = DurableAgentContext.Current.ScheduleNewOrchestration(
name: nameof(FunctionTriggers.RunOrchestrationAsync),
input: new ContentGenerationInput
{
Topic = topic,
MaxReviewAttempts = MaxReviewAttempts,
ApprovalTimeoutHours = ApprovalTimeoutHours
});
this._logger.LogInformation(
"Content generation workflow scheduled to be started for topic '{Topic}' with instance ID: {InstanceId}",
topic,
instanceId);
return $"Workflow started with instance ID: {instanceId}";
}
[Description("Gets the status of a workflow orchestration.")]
public async Task<object> GetWorkflowStatusAsync(
[Description("The instance ID of the workflow to check")] string instanceId,
[Description("Whether to include detailed information")] bool includeDetails = true)
{
this._logger.LogInformation("Getting status for workflow instance: {InstanceId}", instanceId);
// Get the current agent context using the thread-static property
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
instanceId,
includeDetails);
if (status is null)
{
this._logger.LogInformation("Workflow instance '{InstanceId}' not found.", instanceId);
return new
{
instanceId,
error = $"Workflow instance '{instanceId}' not found.",
};
}
return new
{
instanceId = status.InstanceId,
createdAt = status.CreatedAt,
executionStatus = status.RuntimeStatus,
workflowStatus = status.SerializedCustomStatus,
lastUpdatedAt = status.LastUpdatedAt,
failureDetails = status.FailureDetails
};
}
[Description("Raises a feedback event for the content generation workflow.")]
public async Task SubmitHumanApprovalAsync(
[Description("The instance ID of the workflow to submit feedback for")] string instanceId,
[Description("Feedback to submit")] HumanApprovalResponse feedback)
{
this._logger.LogInformation("Submitting human approval for workflow instance: {InstanceId}", instanceId);
await DurableAgentContext.Current.RaiseOrchestrationEventAsync(instanceId, "HumanApproval", feedback);
}
}
@@ -0,0 +1,27 @@
### Run an agent that can schedule orchestrations as tool calls
POST http://localhost:7071/api/agents/publisher/run
Content-Type: text/plain
Start a content generation workflow for the topic 'The Future of Artificial Intelligence'
### Save the session ID from the response to continue the conversation
@threadId = <YOUR_THREAD_ID>
### Check the status of the workflow
POST http://localhost:7071/api/agents/publisher/run?thread_id={{threadId}}
Content-Type: text/plain
Check the status of the workflow you previously started
### Reject content with feedback
POST http://localhost:7071/api/agents/publisher/run?thread_id={{threadId}}
Content-Type: text/plain
Reject the content with feedback: The article needs more technical depth and better examples.
### Approve content
POST http://localhost:7071/api/agents/publisher/run?thread_id={{threadId}}
Content-Type: text/plain
Approve the content
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentAsMcpTool</AssemblyName>
<RootNamespace>AgentAsMcpTool</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to configure AI agents to be accessible as MCP tools.
// When using AddAIAgent and enabling MCP tool triggers, the Functions host will automatically
// generate a remote MCP endpoint for the app at /runtime/webhooks/mcp with a agent-specific
// query tool name.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Define three AI agents we are going to use in this application.
AIAgent agent1 = client.GetChatClient(deploymentName).CreateAIAgent("You are good at telling jokes.", "Joker");
AIAgent agent2 = client.GetChatClient(deploymentName)
.CreateAIAgent("Check stock prices.", "StockAdvisor");
AIAgent agent3 = client.GetChatClient(deploymentName)
.CreateAIAgent("Recommend plants.", "PlantAdvisor", description: "Get plant recommendations.");
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
options
.AddAIAgent(agent1) // Enables HTTP trigger by default.
.AddAIAgent(agent2, enableHttpTrigger: false, enableMcpToolTrigger: true) // Disable HTTP trigger, enable MCP Tool trigger.
.AddAIAgent(agent3, agentOptions =>
{
agentOptions.McpToolTrigger.IsEnabled = true; // Enable MCP Tool trigger.
});
})
.Build();
app.Run();
@@ -0,0 +1,87 @@
# Agent as MCP Tool Sample
This sample demonstrates how to configure AI agents to be accessible as both HTTP endpoints and [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) tools, enabling flexible integration patterns for AI agent consumption.
## Key Concepts Demonstrated
- **Multi-trigger Agent Configuration**: Configure agents to support HTTP triggers, MCP tool triggers, or both
- **Microsoft Agent Framework Integration**: Use the framework to define AI agents with specific roles and capabilities
- **Flexible Agent Registration**: Register agents with customizable trigger configurations
- **MCP Server Hosting**: Expose agents as MCP tools for consumption by MCP-compatible clients
## Sample Architecture
This sample creates three agents with different trigger configurations:
| Agent | Role | HTTP Trigger | MCP Tool Trigger | Description |
|-------|------|--------------|------------------|-------------|
| **Joker** | Comedy specialist | ✅ Enabled | ❌ Disabled | Accessible only via HTTP requests |
| **StockAdvisor** | Financial data | ❌ Disabled | ✅ Enabled | Accessible only as MCP tool |
| **PlantAdvisor** | Indoor plant recommendations | âś… Enabled | âś… Enabled | Accessible via both HTTP and MCP |
## Environment Setup
See the [README.md](../README.md) file in the parent directory for complete setup instructions, including:
- Prerequisites installation
- Azure OpenAI configuration
- Durable Task Scheduler setup
- Storage emulator configuration
For this sample, you'll also need to install [node.js](https://nodejs.org/en/download) in order to use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) tool.
## Configuration
Update your `local.settings.json` with your Azure OpenAI credentials:
```json
{
"Values": {
"AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com/",
"AZURE_OPENAI_DEPLOYMENT": "your-deployment-name",
"AZURE_OPENAI_KEY": "your-api-key-if-not-using-rbac"
}
}
```
## Running the Sample
1. **Start the Function App**:
```bash
cd dotnet/samples/AzureFunctions/07_AgentAsMcpTool
func start
```
2. **Note the MCP Server Endpoint**: When the app starts, you'll see the MCP server endpoint in the terminal output. It will look like:
```text
MCP server endpoint: http://localhost:7071/runtime/webhooks/mcp
```
## Testing MCP Tool Integration
Any MCP-compatible client can connect to the server endpoint and utilize the exposed agent tools. The agents will appear as callable tools within the MCP protocol.
### Using MCP Inspector
1. Run the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) from the command line:
```bash
npx @modelcontextprotocol/inspector
```
1. Connect using the MCP server endpoint from your terminal output
- For **Transport Type**, select **"Streamable HTTP"**
- For **URL**, enter the MCP server endpoint `http://localhost:7071/runtime/webhooks/mcp`
- Click the **Connect** button
1. Click the **List Tools** button to see the available MCP tools. You should see the `StockAdvisor` and `PlantAdvisor` tools.
1. Test the available MCP tools:
- **StockAdvisor** - Set "MSFT ATH" (ATH is "all time high") as the query and click the **Run Tool** button.
- **PlantAdvisor** - Set "Low light in Seattle" as the query and click the **Run Tool** button.
You'll see the results of the tool calls in the MCP Inspector interface under the **Tool Results** section. You should also see the results in the terminal where you ran the `func start` command.
@@ -0,0 +1,19 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Azure.Functions.DurableAgents": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
+151
View File
@@ -0,0 +1,151 @@
# Azure Functions Samples
This directory contains samples for Azure Functions.
- **[01_SingleAgent](01_SingleAgent)**: A sample that demonstrates how to host a single conversational agent in an Azure Functions app and invoke it directly over HTTP.
- **[02_AgentOrchestration_Chaining](02_AgentOrchestration_Chaining)**: A sample that demonstrates how to host a single conversational agent in an Azure Functions app and invoke it using a durable orchestration.
- **[03_AgentOrchestration_Concurrency](03_AgentOrchestration_Concurrency)**: A sample that demonstrates how to host multiple agents in an Azure Functions app and run them concurrently using a durable orchestration.
- **[04_AgentOrchestration_Conditionals](04_AgentOrchestration_Conditionals)**: A sample that demonstrates how to host multiple agents in an Azure Functions app and run them sequentially using a durable orchestration with conditionals.
- **[05_AgentOrchestration_HITL](05_AgentOrchestration_HITL)**: A sample that demonstrates how to implement a human-in-the-loop workflow using durable orchestration, including external event handling for human approval.
- **[06_LongRunningTools](06_LongRunningTools)**: A sample that demonstrates how agents can start and interact with durable orchestrations from tool calls to enable long-running tool scenarios.
- **[07_AgentAsMcpTool](07_AgentAsMcpTool)**: A sample that demonstrates how to configure durable AI agents to be accessible as Model Context Protocol (MCP) tools.
## Running the Samples
These samples are designed to be run locally in a cloned repository.
### Prerequisites
The following prerequisites are required to run the samples:
- [.NET 9.0 SDK or later](https://dotnet.microsoft.com/download/dotnet)
- [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local) (version 4.x or later)
- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed and authenticated (`az login`) or an API key for the Azure OpenAI service
- [Azure OpenAI Service](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource) with a deployed model (gpt-4o-mini or better is recommended)
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/develop-with-durable-task-scheduler) (local emulator or Azure-hosted)
- [Docker](https://docs.docker.com/get-docker/) installed if running the Durable Task Scheduler emulator locally
### Configuring RBAC Permissions for Azure OpenAI
These samples are configured to use the Azure OpenAI service with RBAC permissions to access the model. You'll need to configure the RBAC permissions for the Azure OpenAI service to allow the Azure Functions app to access the model.
Below is an example of how to configure the RBAC permissions for the Azure OpenAI service to allow the current user to access the model.
Bash (Linux/macOS/WSL):
```bash
az role assignment create \
--assignee "yourname@contoso.com" \
--role "Cognitive Services OpenAI User" \
--scope /subscriptions/<your-subscription-id>/resourceGroups/<your-resource-group-name>/providers/Microsoft.CognitiveServices/accounts/<your-openai-resource-name>
```
PowerShell:
```powershell
az role assignment create `
--assignee "yourname@contoso.com" `
--role "Cognitive Services OpenAI User" `
--scope /subscriptions/<your-subscription-id>/resourceGroups/<your-resource-group-name>/providers/Microsoft.CognitiveServices/accounts/<your-openai-resource-name>
```
More information on how to configure RBAC permissions for Azure OpenAI can be found in the [Azure OpenAI documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=cli).
### Setting an API key for the Azure OpenAI service
As an alternative to configuring Azure RBAC permissions, you can set an API key for the Azure OpenAI service by setting the `AZURE_OPENAI_KEY` environment variable.
Bash (Linux/macOS/WSL):
```bash
export AZURE_OPENAI_KEY="your-api-key"
```
PowerShell:
```powershell
$env:AZURE_OPENAI_KEY="your-api-key"
```
### Start Durable Task Scheduler
Most samples use the Durable Task Scheduler (DTS) to support hosted agents and durable orchestrations. DTS also allows you to view the status of orchestrations and their inputs and outputs from a web UI.
To run the Durable Task Scheduler locally, you can use the following `docker` command:
```bash
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
```
The DTS dashboard will be available at `http://localhost:8080`.
### Start the Azure Storage Emulator
All Function apps require an Azure Storage account to store functions-specific state. You can use the Azure Storage Emulator to run a local instance of the Azure Storage service.
You can run the Azure Storage emulator locally as a standalone process or via a Docker container.
#### Docker
```bash
docker run -d --name storage-emulator -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
```
#### Standalone
```bash
npm install -g azurite
azurite
```
### Environment Configuration
Each sample has its own `local.settings.json` file that contains the environment variables for the sample. You'll need to update the `local.settings.json` file with the correct values for your Azure OpenAI resource.
```json
{
"Values": {
"AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com/",
"AZURE_OPENAI_DEPLOYMENT": "your-deployment-name"
}
}
```
Alternatively, you can set the environment variables in the command line.
### Bash (Linux/macOS/WSL)
```bash
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT="your-deployment-name"
```
### PowerShell
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
$env:AZURE_OPENAI_DEPLOYMENT="your-deployment-name"
```
These environment variables, when set, will override the values in the `local.settings.json` file, making it convenient to test the sample without having to update the `local.settings.json` file.
### Start the Azure Functions app
Navigate to the sample directory and start the Azure Functions app:
```bash
cd dotnet/samples/AzureFunctions/01_SingleAgent
func start
```
The Azure Functions app will be available at `http://localhost:7071`.
### Test the Azure Functions app
The README.md file in each sample directory contains instructions for testing the sample. Each sample also includes a `demo.http` file that can be used to test the sample from the command line. These files can be opened in VS Code with the [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension or in the Visual Studio IDE.
### Viewing the sample output
The Azure Functions app logs are displayed in the terminal where you ran `func start`. This is where most agent output will be displayed. You can adjust logging levels in the `host.json` file as needed.
You can also see the state of agents and orchestrations in the DTS dashboard.

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