Compare commits

...
Author SHA1 Message Date
Dmytro StrukandGitHub cf13e35c73 Updated package versions (#2360) 2025-11-20 16:07:10 -08:00
Tao ChenandGitHub 5353b9a2f0 Update Workflow Viz sample comments (#2361) 2025-11-20 23:34:27 +00:00
Farzad SunavalaGitHubClaudeFarzad Sunavala <farzad.sunavala.enovate.ai>farzad528
04e711cd55 Python: Feature/azure ai search agentic rag (search as separate package) (#2328)
* Python: Fix pyright errors and move search provider to core (#1546)

* address pablo coments

* update azure ai search pypi version to latest prev

* init update

* Fix MyPy type annotation errors in search provider

- Add type annotation to DEFAULT_CONTEXT_PROMPT
- Add type annotation to vectorizable_fields
- Add union type annotation to vector_queries

* Fix DEFAULT_CONTEXT_PROMPT MyPy error and update test

- Rename DEFAULT_CONTEXT_PROMPT to _DEFAULT_SEARCH_CONTEXT_PROMPT to avoid conflict with base class Final variable
- Update test to use new constant name
- All core package tests passing (1123 passed)

* Python: Move Azure AI Search to separate package per PR feedback

Addresses reviewer feedback from PR #1546 by isolating the beta dependency
(azure-search-documents==11.7.0b2) into a new agent-framework-aisearch package.

Changes:
- Created new agent-framework-aisearch package with complete structure
- Moved AzureAISearchContextProvider from core to aisearch package
- Added AzureAISearchSettings class for environment variable auto-loading
- Added support for direct API key string (auto-converts to AzureKeyCredential)
- Added azure_openai_api_key parameter for Knowledge Base authentication
- Updated embedding_function type to Callable[[str], Awaitable[list[float]]]
- Moved Role import to top-level imports
- Maintained lazy loading through agent_framework.azure module
- Removed beta dependency from core package
- Updated all tests to use new package location
- All quality checks pass: ruff format/lint, pyright, mypy (0 errors)
- All 21 unit tests pass with 59% coverage

Semantic search mode verified working with both API key and managed identity authentication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Python: Clarify top_k parameter only applies to semantic mode

Updated documentation to clarify that the top_k parameter only affects
semantic search mode. In agentic mode, the server-side Knowledge Base
determines retrieval based on query complexity and reasoning effort.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Python: Add Knowledge Base output mode and retrieval reasoning effort parameters

Added support for configurable Knowledge Base behavior in agentic mode:

- knowledge_base_output_mode: "extractive_data" (default) or "answer_synthesis"
  Some knowledge sources require answer_synthesis mode for proper functionality.

- retrieval_reasoning_effort: "minimal" (default), "medium", or "low"
  Controls query planning complexity and multi-hop reasoning depth.

These parameters give users fine-grained control over Knowledge Base behavior
and enable support for knowledge sources that require answer synthesis.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* effort and outputmode query params

* Address PR review feedback for Azure AI Search context provider

* comments eduward

* ed latest comments

---------

Co-authored-by: Farzad Sunavala <farzad.sunavala.enovate.ai>
Co-authored-by: farzad528 <farzad528@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 22:34:46 +00:00
Peter IbekweandGitHub ab3d898979 .NET: Add unit tests for RetrieveConversationMessageExecutor executor (#2232)
* Add unit tests for create conversation executor

* Update indentation and comment typo.

* Added unit tests for declarative executor SetMultipleVariablesExecutor

* Updated comments and syntactic sugar

* Add unit test for declarative executor  RetrieveConversationMessageExecutor

* Removed irrelevant code statements

* Updated based on copilot feedback.
2025-11-20 21:47:42 +00:00
Tao ChenandGitHub 02af2bc0ef Python: Remove duplicated workflow observability sample (#2357)
* Remove duplicated workflow observability sample

* Fix link
2025-11-20 21:06:12 +00:00
David WuandGitHub e5b63a1041 Python: Move evaluation folders to under evaluations (#2355)
* Move evaluation folders to under evaluations

* Change folder path
2025-11-20 20:50:23 +00:00
b575b631c8 Python: Fix for Azure AI client (#2358)
* Fix for Azure AI client

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-20 20:46:28 +00:00
ce738cc6bc .NET: Add sample to show how to do RAG using Foundry's built-in service (#2324)
* Add sample to show how to do RAG using Foundry's built-in service

* Update README.me

* Update dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs

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

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2025-11-20 18:55:21 +00:00
f99dca033f fix(observability): handle datetime serialization in tool results (#2248)
Fixes #2219

Adds default=str to json.dumps() calls to handle non-JSON-serializable
types like datetime objects in tool function results.

Co-authored-by: kishikawa-hayato <84244732+HerBest-max@users.noreply.github.com>
2025-11-20 17:50:45 +00:00
6ae32f007d [BREAKING] Python: Schema changes for azure functions package (#2151)
* 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

* Schema changes for azure functions

* Fixed serialization bug

* update to camel case

* Adding logs

* merge with main

* sync uv.lock

* Updated schema

* Fixed tests

* Addressed comments

* Fixed mypy errors

* Fixed bug in responsetype and authorName

* Addressed feedback

* Addressed more feedback

* Python: Addressing comments for #2151 (#2315)

* Initial fixes

* Address more comments

* Address remaining comments

* Fixed remaining snake_case properties

* Fixed remaining snake_case properties

* Fixed mypy errors

* Minor changes

* revert tool names

* Fixed mypy errors

---------

Co-authored-by: Laveesh Rohra <larohra@microsoft.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.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>
Co-authored-by: Victoria Hall <victoriahall@microsoft.com>
2025-11-20 16:24:34 +00:00
Eduard van ValkenburgandGitHub 039e49f353 Python: small fix for logging in declarative (#2341)
* small fix for logging in declarative

* fix spaces in string
2025-11-20 10:31:00 +00:00
Evan MattsonandGitHub 61dbacd6f8 Improve exception handling (#2337) 2025-11-20 09:25:09 +00:00
David WuandGitHub c7a8c12296 Python: Move red teaming files to its own folder (#2333)
* Move red teaming files to its own folder

* Update file path

* Updated folder names

* Updated reference names
2025-11-20 08:28:22 +00:00
Evan MattsonandGitHub d714b91a14 Python: Fix tool execution bleed-over in aiohttp/Bot Framework scenarios (#2314)
* Deep copy the agent chat options to avoid mutations

* avoiding _thread.RLock pickling errors
2025-11-20 08:06:14 +00:00
Evan MattsonGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
99689add09 Python: clean up exception (#2319)
* Potential fix for code scanning alert no. 18: Information exposure through an exception

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Fix test

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-20 00:54:07 +00:00
Evan MattsonandGitHub 4fcc5a4b7d Python: propagate as_tool() kwargs. Add sample for runtime context with as_tool kwargs and middleware. (#2311)
* as tool kwargs

* simplify
2025-11-20 00:53:44 +00:00
79bb87061b Python: Clean up imports (#2318)
* chore: tidy imports

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

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

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

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

* chore: revert stub file change

* chore: trigger pre-commit hook, re-add `annotations` import

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 23:41:01 +00:00
b3e96b80ae Python: Use AI Foundry evaluators for self-reflection (#2250)
* First working version

* Simplify the implementations

* Remove unused env var

* Update Python syntax

* Address feedbacks

* Fix a typo

* Update names as review suggestions

* Citation for self-reflection

* Move to independent folder

* Update python/samples/getting_started/evaluation/azure_ai_foundry/evaluation/README.md

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

* Updated from parquet to JSONL and hide the default environment variables

* As review feedback, remove the purpose of using `run_self_reflection_batch` as a library, only use it as sample code

* Update python/samples/getting_started/evaluation/azure_ai_foundry/evaluation/self_reflection.py

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

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2025-11-19 18:41:21 +00:00
Eduard van ValkenburgandGitHub 92df9e14bf Python: Introducing support for declarative yaml spec (#2002)
* first work on declarative

* initial version of the declarative support

* fix tests and mypy

* fix parameters of functiontool

* slight logic improvement

* remove path until merge

* updates from comments

* create dispatcher and spec type, json_schema method

* fix mypy, skipping model

* updated lock

* fixed declarative tests and renamed some other test files

* refined loader

* updated lock

* fix mypy

* added readme to samples folder

* fixes from review

* undid test file rename
2025-11-19 16:33:02 +00:00
Eduard van ValkenburgandGitHub d2d0f46e15 Python: fix all to include the latest and made that single source of truth (#2303)
* fix all to include the latest and made that single source of truth

* add lab
2025-11-19 16:09:28 +00:00
Dmytro StrukandGitHub 84e2c0cc22 Python: Added M365 Agent SDK Hosting sample (#2292)
* Added M365 Agent SDK Hosting sample

* Addressed PR feedback

* Added inline dependencies

* Addressed PR feedback
2025-11-19 15:54:47 +00:00
Eduard van ValkenburgandGitHub 4e339f841a added test to validate status set (#2265) 2025-11-19 15:54:12 +00:00
Eduard van ValkenburgandGitHub 34a00f1b8a Python: fix: @ai_function doesn't properly handle 'self' param (#2266)
* Fixes Python: @ai_function doesn't properly handle 'self' param
Fixes #1343

* fix for declaration only funcs

* fix mypy
2025-11-19 15:49:50 +00:00
Giles OdigweandGitHub d5165e2532 Python: Added Foundry Sample for A2A + SharePoint Samples (#2313)
* a2a + sharepoint samples

* small fixes
2025-11-19 11:13:47 +00:00
f83c39f924 Python: fix: resolve string annotations in FunctionExecutor (#2308)
* fix: resolve string annotations in FunctionExecutor

Enhance type hint validation in FunctionExecutor by importing `typing` and
using `get_type_hints` to correctly resolve annotations.

This fixes validation failures when `from __future__ import annotations`
is enabled, which stores annotations as strings.

Fixes #1808

* Update python/packages/core/tests/workflow/test_function_executor_future.py

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

* ran pre commit

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 07:26:44 +00:00
claude89757andGitHub 037349ff90 Python: fix Langfuse observability to capture ChatAgent system instructions (#2316)
Fix bug where ChatAgent system instructions were not captured in Langfuse
traces due to incorrect attribute access.

The observability code was attempting to retrieve instructions using
getattr(self, "instructions", None), but ChatAgent stores instructions
in self.chat_options.instructions. This caused system_instructions to
always be None in Langfuse traces.

Changed both _trace_agent_run and _trace_agent_run_stream functions
to correctly retrieve instructions from chat_options.instructions.

Fixes affect:
- Line 1123: _trace_agent_run (non-streaming)
- Line 1192: _trace_agent_run_stream (streaming)
2025-11-19 07:08:27 +00:00
Eduard van ValkenburgGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>eavanvalkenburgcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>CopilotVictor Dibia
293abf5b56 Python: fix for Incomplete URL substring sanitization (#2274)
* Potential fix for code scanning alert no. 29: Incomplete URL substring sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Python: Fix URL parsing to handle trailing punctuation in deployment progress detection (#2296)

* Initial plan

* Fix URL parsing to handle trailing punctuation correctly

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

---------

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

* updated lock

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
2025-11-18 22:16:58 +00:00
Evan MattsonandGitHub e2d2299a4f Python: Improve WorkflowBuilder doc strings with code samples (#1960)
* Improve WorkflowBuilder doc strings with code samples

* Cleanup
2025-11-18 22:13:17 +00:00
Eduard van ValkenburgandGitHub 8a7260140a Python: Anthropic foundry (#2302)
* added anthropic foundry sample

* updated readme

* typo
2025-11-18 16:03:40 +00:00
Korolev DmitryandGitHub 1da9107f4a .NET: Improve AIAgent and Workflow registrations for DevUI integration (#2227)
* wip

* resolve non-agent workflows as well!

* add tests for devui registrations and resolving

* fixes

* devui for net8 as well!

* simplify TFM

* update tfm...

* tfm rules....

* wip

* roll

* verify entities are registered with a devui call

* tests

* add a proper support for non-keyed workflows

* resolve default aiagent registration

* sort usings :)

* cleanup tests
2025-11-18 15:38:00 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
03b74bfad4 Bump js-yaml from 4.1.0 to 4.1.1 in /python/packages/devui/frontend (#2230)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-11-18 13:47:27 +00:00
Roger BarretoandGitHub f6cd329a32 .NET: Post bugbash updates (#2279)
* Add missing README.md

* Address bugbash comments

* Address bugbash issues and suggestions
2025-11-18 09:50:22 +00:00
Evan MattsonandGitHub 1f0ffc159c Python: Fix ag-ui state handling issues (#2289)
* Fix ag-ui state handling

* Bump package version and update changelog

* Update changelog
2025-11-18 11:47:26 +09:00
d50371729a Clarify exception handling in ConversationId property (#1457)
Update XML documentation to clarify exception behavior.

See `ChatClientAgentThreadTests.SetConversationIdThrowsWhenMessageStoreIsSet` which already verifies this is the actual behavior.

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-11-17 19:50:26 +00:00
Chris GillumandGitHub 77247a304e .NET: Change thread_id from entity ID to GUID (#2260) 2025-11-17 19:30:45 +00:00
e413c5a285 .NET: Add M365 Agent SDK Hosting sample (#2221)
* Add M365 Agent SDK interop sample

* Update dotnet/samples/M365Agent/README.md

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

* Address some comments.

* Update dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs

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

* Update dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponse.cs

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

* Update dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponse.cs

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

* Address PR comments

* Refactor code to simplify.

* Fix broken link.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 19:19:52 +00:00
225 changed files with 17193 additions and 5308 deletions
+2
View File
@@ -204,6 +204,8 @@ agents.md
# AI
.claude/
WARP.md
**/memory-bank/
**/projectBrief.md
# Azurite storage emulator files
*/__azurite_db_blob__.json
+3
View File
@@ -0,0 +1,3 @@
# Declarative Agents
This folder contains sample agent definitions than be ran using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
+25
View File
@@ -0,0 +1,25 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
model:
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
provider: AzureOpenAI
apiType: Chat
options:
temperature: 0.9
topP: 0.95
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
type:
kind: string
required: true
description: The type of the response.
@@ -0,0 +1,25 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
model:
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
provider: AzureOpenAI
apiType: Assistants
options:
temperature: 0.9
topP: 0.95
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
type:
kind: string
required: true
description: The type of the response.
@@ -0,0 +1,28 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
model:
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
provider: AzureOpenAI
apiType: Responses
options:
text:
verbosity: medium
connection:
kind: remote
endpoint: =Env.AZURE_OPENAI_ENDPOINT
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
type:
kind: string
required: true
description: The type of the response.
+18
View File
@@ -0,0 +1,18 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
model:
options:
temperature: 0.9
topP: 0.95
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
+27
View File
@@ -0,0 +1,27 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions using the tools provided.
model:
options:
allowMultipleToolCalls: true
chatToolMode: auto
tools:
- kind: function
name: GetWeather
description: Get the weather for a given location.
bindings:
get_weather: get_weather
parameters:
properties:
location:
kind: string
description: The city and state, e.g. San Francisco, CA
required: true
unit:
kind: string
description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
required: false
enum:
- celsius
- fahrenheit
@@ -0,0 +1,21 @@
kind: Prompt
name: MicrosoftLearnAgent
description: Microsoft Learn Agent
instructions: You answer questions by searching the Microsoft Learn content only.
model:
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
options:
temperature: 0.9
topP: 0.95
connection:
kind: remote
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
tools:
- kind: mcp
name: microsoft_learn
description: Get information from Microsoft Learn.
url: https://learn.microsoft.com/api/mcp
approvalMode:
kind: never
allowedTools:
- microsoft_docs_search
@@ -0,0 +1,22 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
model:
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
options:
temperature: 0.9
topP: 0.95
connection:
kind: remote
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
+28
View File
@@ -0,0 +1,28 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
model:
id: =Env.OPENAI_MODEL
provider: OpenAI
apiType: Chat
options:
temperature: 0.9
topP: 0.95
connection:
kind: key
key: =Env.OPENAI_API_KEY
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
type:
kind: string
required: true
description: The type of the response.
@@ -0,0 +1,30 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
model:
id: =Env.OPENAI_MODEL
provider: OpenAI
apiType: Assistants
options:
temperature: 0.9
topP: 0.95
connection:
kind: key
key: =Env.OPENAI_APIKEY
outputSchema:
name: AssistantResponse
description: The response from the assistant.
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
type:
kind: string
required: true
description: The type of the response.
+28
View File
@@ -0,0 +1,28 @@
kind: Prompt
name: Assistant
description: Helpful assistant
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
model:
id: =Env.OPENAI_MODEL
provider: OpenAI
apiType: Responses
options:
text:
verbosity: medium
connection:
kind: key
key: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
kind: string
required: true
description: The language of the answer.
answer:
kind: string
required: true
description: The answer text.
type:
kind: string
required: true
description: The type of the response.
+4
View File
@@ -81,6 +81,10 @@
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
<PackageVersion Include="Microsoft.Agents.Authentication.Msal" Version="1.2.41" />
<PackageVersion Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.2.41" />
<!-- A2A -->
<PackageVersion Include="A2A" Version="0.3.3-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.3-preview" />
+8 -3
View File
@@ -101,13 +101,14 @@
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.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" />
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/FoundryAgents/">
<File Path="samples/GettingStarted/FoundryAgents/README.md" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/FoundryAgents_Step01.1_Basics.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/FoundryAgents_Step01.2_Running.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/FoundryAgents_Step02_MultiturnConversation.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/FoundryAgents_Step03.1_UsingFunctionTools.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/FoundryAgents_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/FoundryAgents_Step05_StructuredOutput.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/FoundryAgents_Step06_PersistedConversations.csproj" />
@@ -191,7 +192,7 @@
<Project Path="samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Visualization/">
<Project Path="samples/GettingStarted/Workflows/Visualization/Visualization.csproj" Id="99bf0bc6-2440-428e-b3e7-d880e4b7a5fd" />
<Project Path="samples/GettingStarted/Workflows/Visualization/Visualization.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/_Foundational/">
<Project Path="samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
@@ -208,6 +209,9 @@
<Project Path="samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
</Folder>
<Folder Name="/Samples/M365Agent/">
<Project Path="samples/M365Agent/M365Agent.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
@@ -371,6 +375,7 @@
<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.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.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" />
@@ -2,7 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI;
namespace AgentWebChat.AgentHost;
@@ -10,24 +10,24 @@ internal static class ActorFrameworkWebApplicationExtensions
{
public static void MapAgentDiscovery(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string path)
{
var routeGroup = endpoints.MapGroup(path);
routeGroup.MapGet("/", async (
AgentCatalog agentCatalog,
CancellationToken cancellationToken) =>
{
var results = new List<AgentDiscoveryCard>();
await foreach (var result in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(new AgentDiscoveryCard
{
Name = result.Name!,
Description = result.Description,
});
}
var registeredAIAgents = endpoints.ServiceProvider.GetKeyedServices<AIAgent>(KeyedService.AnyKey);
return Results.Ok(results);
})
.WithName("GetAgents");
var routeGroup = endpoints.MapGroup(path);
routeGroup.MapGet("/", async (CancellationToken cancellationToken) =>
{
var results = new List<AgentDiscoveryCard>();
foreach (var result in registeredAIAgents)
{
results.Add(new AgentDiscoveryCard
{
Name = result.Name!,
Description = result.Description,
});
}
return Results.Ok(results);
})
.WithName("GetAgents");
}
internal sealed class AgentDiscoveryCard
@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
@@ -5,6 +5,7 @@ using AgentWebChat.AgentHost;
using AgentWebChat.AgentHost.Custom;
using AgentWebChat.AgentHost.Utilities;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DevUI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
@@ -21,6 +22,13 @@ builder.Services.AddProblemDetails();
// Configure the chat model and our agent.
builder.AddKeyedChatClient("chat-model");
// Add DevUI services
builder.AddDevUI();
// Add OpenAI services
builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
var pirateAgentBuilder = builder.AddAIAgent(
"pirate",
instructions: "You are a pirate. Speak like a pirate",
@@ -95,8 +103,48 @@ var scienceConcurrentWorkflow = builder.AddWorkflow("science-concurrent-workflow
return AgentWorkflowBuilder.BuildConcurrent(workflowName: key, agents: agents);
}).AddAsAIAgent();
builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
builder.AddWorkflow("nonAgentWorkflow", (sp, key) =>
{
List<IHostedAgentBuilder> usedAgents = [pirateAgentBuilder, chemistryAgent];
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents);
});
builder.Services.AddKeyedSingleton("NonAgentAndNonmatchingDINameWorkflow", (sp, key) =>
{
List<IHostedAgentBuilder> usedAgents = [pirateAgentBuilder, chemistryAgent];
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
return AgentWorkflowBuilder.BuildSequential(workflowName: "random-name", agents: agents);
});
builder.Services.AddSingleton<AIAgent>(sp =>
{
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
return new ChatClientAgent(chatClient, name: "default-agent", instructions: "you are a default agent.");
});
builder.Services.AddKeyedSingleton<AIAgent>("my-di-nonmatching-agent", (sp, name) =>
{
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
return new ChatClientAgent(
chatClient,
name: "some-random-name", // demonstrating registration can be different for DI and actual agent
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
});
builder.Services.AddKeyedSingleton<AIAgent>("my-di-matchingname-agent", (sp, name) =>
{
if (name is not string nameStr)
{
throw new NotSupportedException("Name should be passed as a key");
}
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
return new ChatClientAgent(
chatClient,
name: nameStr, // demonstrating registration with the same name
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
});
var app = builder.Build();
@@ -118,7 +166,10 @@ app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard
// Url = "http://localhost:5390/a2a/knights-and-knaves"
});
app.MapDevUI();
app.MapOpenAIResponses();
app.MapOpenAIConversations();
app.MapOpenAIChatCompletions(pirateAgentBuilder);
app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder);
@@ -9,7 +9,9 @@ var azOpenAiResourceGroup = builder.AddParameterFromConfiguration("AzureOpenAIRe
var chatModel = builder.AddAIModel("chat-model").AsAzureOpenAI("gpt-4o", o => o.AsExisting(azOpenAiResource, azOpenAiResourceGroup));
var agentHost = builder.AddProject<Projects.AgentWebChat_AgentHost>("agenthost")
.WithReference(chatModel);
.WithHttpEndpoint(name: "devui")
.WithUrlForEndpoint("devui", (url) => new() { Url = "/devui", DisplayText = "Dev UI" })
.WithReference(chatModel);
builder.AddProject<Projects.AgentWebChat_Web>("webfrontend")
.WithExternalHttpEndpoints()
@@ -47,7 +47,7 @@ curl -X POST http://localhost:7071/api/agents/Joker/run \
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" \
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"message": "Tell me another one."}'
@@ -64,7 +64,7 @@ The expected `application/json` output will look something like:
```json
{
"status": 200,
"thread_id": "@dafx-joker@your-thread-id",
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
"response": {
"Messages": [
{
@@ -52,7 +52,7 @@ The response will be a text string that looks something like the following, indi
```http
HTTP/1.1 200 OK
Content-Type: text/plain
x-ms-thread-id: @publisher@351ec855-7f4d-4527-a60d-498301ced36d
x-ms-thread-id: 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!
```
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -11,7 +11,6 @@
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" />
</ItemGroup>
<ItemGroup>
@@ -19,8 +18,8 @@
</ItemGroup>
<ItemGroup>
<None Update="OpenAPISpec.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<None Update="contoso-outdoors-knowledge-base.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use the built in RAG capabilities that the Foundry service provides when using AI Agents provided by Foundry.
using System.ClientModel;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Files;
using OpenAI.VectorStores;
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Create an AI Project client and get an OpenAI client that works with the foundry service.
AIProjectClient aiProjectClient = new(
new Uri(endpoint),
new AzureCliCredential());
OpenAIClient openAIClient = aiProjectClient.GetProjectOpenAIClient();
// Upload the file that contains the data to be used for RAG to the Foundry service.
OpenAIFileClient fileClient = openAIClient.GetOpenAIFileClient();
ClientResult<OpenAIFile> uploadResult = await fileClient.UploadFileAsync(
filePath: "contoso-outdoors-knowledge-base.md",
purpose: FileUploadPurpose.Assistants);
// Create a vector store in the Foundry service using the uploaded file.
VectorStoreClient vectorStoreClient = openAIClient.GetVectorStoreClient();
ClientResult<VectorStore> vectorStoreCreate = await vectorStoreClient.CreateVectorStoreAsync(options: new VectorStoreCreationOptions()
{
Name = "contoso-outdoors-knowledge-base",
FileIds = { uploadResult.Value.Id }
});
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreCreate.Value.Id)] };
AIAgent agent = await aiProjectClient
.CreateAIAgentAsync(
model: deploymentName,
name: "AskContoso",
instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
tools: [fileSearchTool]);
AgentThread thread = agent.GetNewThread();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
Console.WriteLine("\n>> Asking about shipping\n");
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
Console.WriteLine("\n>> Asking about product care\n");
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
// Cleanup
await fileClient.DeleteFileAsync(uploadResult.Value.Id);
await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreCreate.Value.Id);
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -0,0 +1,19 @@
# Contoso Outdoors Knowledge Base
## Contoso Outdoors Return Policy
Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection.
## Contoso Outdoors Shipping Guide
Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout.
## Product Information
### TrailRunner Tent
The TrailRunner Tent is a lightweight, 2-person tent designed for easy setup and durability. It features waterproof materials, ventilation windows, and a compact carry bag.
#### Care Instructions
Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating.
@@ -7,3 +7,4 @@ These samples show how to create an agent with the Agent Framework that uses Ret
|[Basic Text RAG](./AgentWithRAG_Step01_BasicTextRAG/)|This sample demonstrates how to create and run a basic agent with simple text Retrieval Augmented Generation (RAG).|
|[RAG with Vector Store and custom schema](./AgentWithRAG_Step02_CustomVectorStoreRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with a vector store. It also uses a custom schema for the documents stored in the vector store.|
|[RAG with custom RAG data source](./AgentWithRAG_Step03_CustomRAGDataSource/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with a custom RAG data source.|
|[RAG with Foundry VectorStore service](./AgentWithRAG_Step04_FoundryServiceRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with the Foundry VectorStore service.|
@@ -11,14 +11,15 @@ using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string JokerInstructions = "You are good at telling jokes.";
const string JokerInstructionsV1 = "You are good at telling jokes.";
const string JokerInstructionsV2 = "You are extremely hilarious at telling jokes.";
const string JokerName = "JokerAgent";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Define the agent you want to create. (Prompt Agent in this case)
AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructions });
AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructionsV1 });
// Azure.AI.Agents SDK creates and manages agent by name and versions.
// You can create a server side agent version with the Azure.AI.Agents SDK client below.
@@ -32,8 +33,8 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName:
// You can retrieve an AIAgent for an already created server side agent version.
AIAgent jokerAgentV1 = aiProjectClient.GetAIAgent(agentVersion);
// You can also create another AIAgent version (V2) by providing the same name with a different definition.
AIAgent jokerAgentV2 = aiProjectClient.CreateAIAgent(name: JokerName, model: deploymentName, instructions: JokerInstructions + "V2");
// You can also create another AIAgent version (V2) by providing the same name with a different definition/instruction.
AIAgent jokerAgentV2 = aiProjectClient.CreateAIAgent(name: JokerName, model: deploymentName, instructions: JokerInstructionsV2);
// You can also get the AIAgent latest version by just providing its name.
AIAgent jokerAgentLatest = aiProjectClient.GetAIAgent(name: JokerName);
@@ -43,11 +44,7 @@ AgentVersion latestVersion = jokerAgentLatest.GetService<AgentVersion>()!;
Console.WriteLine($"Latest agent version id: {latestVersion.Id}");
// Once you have the AIAgent, you can invoke it like any other AIAgent.
AgentThread thread = jokerAgentLatest.GetNewThread();
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", thread));
// This will use the same thread to continue the conversation.
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", thread));
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate."));
// Cleanup by agent name removes both agent versions created (jokerAgentV1 + jokerAgentV2).
await aiProjectClient.Agents.DeleteAgentAsync(jokerAgentV1.Name);
@@ -26,13 +26,8 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName:
// You can retrieve an AIAgent for a already created server side agent version.
AIAgent jokerAgent = aiProjectClient.GetAIAgent(agentVersion);
// Invoke the agent and output the text result.
AgentThread thread = jokerAgent.GetNewThread();
Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", thread));
// Invoke the agent with streaming support.
thread = jokerAgent.GetNewThread();
await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate."))
{
Console.WriteLine(update);
}
@@ -1,354 +0,0 @@
{
"openapi": "3.0.1",
"info": {
"title": "Github Versions API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.github.com"
}
],
"components": {
"schemas": {
"basic-error": {
"title": "Basic Error",
"description": "Basic Error",
"type": "object",
"properties": {
"message": {
"type": "string"
},
"documentation_url": {
"type": "string"
},
"url": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"label": {
"title": "Label",
"description": "Color-coded labels help you categorize and filter your issues (just like labels in Gmail).",
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for the label.",
"type": "integer",
"format": "int64",
"example": 208045946
},
"node_id": {
"type": "string",
"example": "MDU6TGFiZWwyMDgwNDU5NDY="
},
"url": {
"description": "URL for the label",
"example": "https://api.github.com/repositories/42/labels/bug",
"type": "string",
"format": "uri"
},
"name": {
"description": "The name of the label.",
"example": "bug",
"type": "string"
},
"description": {
"description": "Optional description of the label, such as its purpose.",
"type": "string",
"example": "Something isn't working",
"nullable": true
},
"color": {
"description": "6-character hex code, without the leading #, identifying the color",
"example": "FFFFFF",
"type": "string"
},
"default": {
"description": "Whether this label comes by default in a new repository.",
"type": "boolean",
"example": true
}
},
"required": [
"id",
"node_id",
"url",
"name",
"description",
"color",
"default"
]
},
"tag": {
"title": "Tag",
"description": "Tag",
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "v0.1"
},
"commit": {
"type": "object",
"properties": {
"sha": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"sha",
"url"
]
},
"zipball_url": {
"type": "string",
"format": "uri",
"example": "https://github.com/octocat/Hello-World/zipball/v0.1"
},
"tarball_url": {
"type": "string",
"format": "uri",
"example": "https://github.com/octocat/Hello-World/tarball/v0.1"
},
"node_id": {
"type": "string"
}
},
"required": [
"name",
"node_id",
"commit",
"zipball_url",
"tarball_url"
]
}
},
"examples": {
"label-items": {
"value": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "f29513",
"default": true
},
{
"id": 208045947,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement",
"name": "enhancement",
"description": "New feature or request",
"color": "a2eeef",
"default": false
}
]
},
"tag-items": {
"value": [
{
"name": "v0.1",
"commit": {
"sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc",
"url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
},
"zipball_url": "https://github.com/octocat/Hello-World/zipball/v0.1",
"tarball_url": "https://github.com/octocat/Hello-World/tarball/v0.1",
"node_id": "MDQ6VXNlcjE="
}
]
}
},
"parameters": {
"owner": {
"name": "owner",
"description": "The account owner of the repository. The name is not case sensitive.",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
"repo": {
"name": "repo",
"description": "The name of the repository without the `.git` extension. The name is not case sensitive.",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
"per-page": {
"name": "per_page",
"description": "The number of results per page (max 100). For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
"in": "query",
"schema": {
"type": "integer",
"default": 30
}
},
"page": {
"name": "page",
"description": "The page number of the results to fetch. For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
"in": "query",
"schema": {
"type": "integer",
"default": 1
}
}
},
"responses": {
"not_found": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/basic-error"
}
}
}
}
},
"headers": {
"link": {
"example": "<https://api.github.com/resource?page=2>; rel=\"next\", <https://api.github.com/resource?page=5>; rel=\"last\"",
"schema": {
"type": "string"
}
}
}
},
"paths": {
"/repos/{owner}/{repo}/tags": {
"get": {
"summary": "List repository tags",
"description": "",
"tags": [
"repos"
],
"operationId": "repos/list-tags",
"externalDocs": {
"description": "API method documentation",
"url": "https://docs.github.com/rest/repos/repos#list-repository-tags"
},
"parameters": [
{
"$ref": "#/components/parameters/owner"
},
{
"$ref": "#/components/parameters/repo"
},
{
"$ref": "#/components/parameters/per-page"
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/tag"
}
},
"examples": {
"default": {
"$ref": "#/components/examples/tag-items"
}
}
}
},
"headers": {
"Link": {
"$ref": "#/components/headers/link"
}
}
}
},
"x-github": {
"githubCloudOnly": false,
"enabledForGitHubApps": true,
"category": "repos",
"subcategory": "repos"
}
}
},
"/repos/{owner}/{repo}/labels": {
"get": {
"summary": "List labels for a repository",
"description": "Lists all labels for a repository.",
"tags": [
"issues"
],
"operationId": "issues/list-labels-for-repo",
"externalDocs": {
"description": "API method documentation",
"url": "https://docs.github.com/rest/issues/labels#list-labels-for-a-repository"
},
"parameters": [
{
"$ref": "#/components/parameters/owner"
},
{
"$ref": "#/components/parameters/repo"
},
{
"$ref": "#/components/parameters/per-page"
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/label"
}
},
"examples": {
"default": {
"$ref": "#/components/examples/label-items"
}
}
}
},
"headers": {
"Link": {
"$ref": "#/components/headers/link"
}
}
},
"404": {
"$ref": "#/components/responses/not_found"
}
},
"x-github": {
"githubCloudOnly": false,
"enabledForGitHubApps": true,
"category": "issues",
"subcategory": "labels"
}
}
}
}
}
@@ -1,38 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use an agent with function tools provided via an OpenAPI spec.
// It uses functionality from Semantic Kernel to parse the OpenAPI spec and create function tools to use with the Agent Framework Agent.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Load the OpenAPI Spec from a file.
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("github", "OpenAPISpec.json");
// Convert the Semantic Kernel plugin to Agent Framework function tools.
// This requires a dummy Kernel instance, since KernelFunctions cannot execute without one.
Kernel kernel = new();
List<AITool> tools = plugin.Select(x => x.WithKernel(kernel)).Cast<AITool>().ToList();
const string AssistantInstructions = "You are a helpful assistant that can query GitHub repositories.";
const string AssistantName = "GitHubAssistant";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: tools);
// Run the agent with the OpenAPI function tools.
AgentThread thread = agent.GetNewThread();
Console.WriteLine(await agent.RunAsync("Please list the names, colors and descriptions of all the labels available in the microsoft/agent-framework repository on github.", thread));
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -1,49 +0,0 @@
# Using Function Tools from OpenAPI Specifications
This sample demonstrates how to create function tools from an OpenAPI specification and use them with AI agents.
## What this sample demonstrates
- Loading OpenAPI specifications from files
- Converting OpenAPI specifications to Semantic Kernel plugins
- Converting Semantic Kernel plugins to AI function tools
- Using OpenAPI-based function tools with AI agents
- Managing agent lifecycle (creation and deletion)
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Azure Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Run the sample
Navigate to the FoundryAgents sample directory and run:
```powershell
cd dotnet/samples/GettingStarted/FoundryAgents
dotnet run --project .\FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI
```
## Expected behavior
The sample will:
1. Load the OpenAPI specification from OpenAPISpec.json (GitHub API)
2. Convert the OpenAPI spec to Semantic Kernel plugins
3. Create an agent named "GitHubAssistant" with the OpenAPI-based function tools
4. Run the agent with a prompt to query GitHub repositories
5. The agent will invoke the appropriate OpenAPI function tools to retrieve data
6. Clean up resources by deleting the agent
@@ -26,18 +26,26 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential(
AITool tool = AIFunctionFactory.Create(GetWeather);
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [tool]);
var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [tool]);
// Getting an already existing agent by name with tools.
/*
* IMPORTANT: Since agents that are stored in the server only know the definition of the function tools (JSON Schema),
* you need to provided all invocable function tools when retrieving the agent so it can invoke them automatically.
* If no invocable tools are provided, the function calling needs to handled manually.
*/
var existingAgent = await aiProjectClient.GetAIAgentAsync(name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentThread thread = agent.GetNewThread();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
AgentThread thread = existingAgent.GetNewThread();
Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
thread = agent.GetNewThread();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
thread = existingAgent.GetNewThread();
await foreach (AgentRunResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
}
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(existingAgent.Name);
@@ -25,7 +25,7 @@ const string AssistantName = "WeatherAssistant";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather));
ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather)));
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [approvalTool]);
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -16,5 +16,11 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\walkway.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -8,7 +8,7 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o";
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o";
const string VisionInstructions = "You are a helpful agent that can analyze images";
const string VisionName = "VisionAgent";
@@ -21,7 +21,7 @@ AIAgent agent = aiProjectClient.CreateAIAgent(name: VisionName, model: deploymen
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
new DataContent(File.ReadAllBytes("assets/walkway.jpg"), "image/jpeg")
]);
AgentThread thread = agent.GetNewThread();
@@ -55,7 +55,7 @@ AgentRunResponse response = await agentOption1.RunAsync("I need to solve the equ
// AgentRunResponse response = await agentOption2.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
// Get the CodeInterpreterToolCallContent
CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType<CodeInterpreterToolCallContent>().SingleOrDefault();
CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType<CodeInterpreterToolCallContent>().FirstOrDefault();
if (toolCallContent?.Inputs is not null)
{
DataContent? codeInput = toolCallContent.Inputs.OfType<DataContent>().FirstOrDefault();
@@ -15,8 +15,8 @@ internal sealed class Program
{
private static async Task Main(string[] args)
{
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "computer-use-preview";
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "computer-use-preview";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
@@ -24,8 +24,8 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="computer-use-preview" # Optional, defaults to computer-use-preview
$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="computer-use-preview" # Optional, defaults to computer-use-preview
```
## Run the sample
@@ -25,8 +25,7 @@ Before you begin, ensure you have the following prerequisites:
|[Basics](./FoundryAgents_Step01.1_Basics/)|This sample demonstrates how to create and manage AI agents with versioning|
|[Running a simple agent](./FoundryAgents_Step01.2_Running/)|This sample demonstrates how to create and run a basic Foundry agent|
|[Multi-turn conversation](./FoundryAgents_Step02_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a Foundry agent|
|[Using function tools](./FoundryAgents_Step03.1_UsingFunctionTools/)|This sample demonstrates how to use function tools with a Foundry agent|
|[Using OpenAPI function tools](./FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a Foundry agent|
|[Using function tools](./FoundryAgents_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with a Foundry agent|
|[Using function tools with approvals](./FoundryAgents_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution|
|[Structured output](./FoundryAgents_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a Foundry agent|
|[Persisted conversations](./FoundryAgents_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later|
@@ -0,0 +1,188 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using AdaptiveCards;
using M365Agent.Agents;
using Microsoft.Agents.AI;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Builder.State;
using Microsoft.Agents.Core.Models;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace M365Agent;
/// <summary>
/// An adapter class that exposes a Microsoft Agent Framework <see cref="AIAgent"/> as a M365 Agent SDK <see cref="AgentApplication"/>.
/// </summary>
internal sealed class AFAgentApplication : AgentApplication
{
private readonly AIAgent _agent;
private readonly string? _welcomeMessage;
public AFAgentApplication(AIAgent agent, AgentApplicationOptions options, [FromKeyedServices("AFAgentApplicationWelcomeMessage")] string? welcomeMessage = null) : base(options)
{
this._agent = agent;
this._welcomeMessage = welcomeMessage;
this.OnConversationUpdate(ConversationUpdateEvents.MembersAdded, this.WelcomeMessageAsync);
this.OnActivity(ActivityTypes.Message, this.MessageActivityAsync, rank: RouteRank.Last);
}
/// <summary>
/// The main agent invocation method, where each user message triggers a call to the underlying <see cref="AIAgent"/>.
/// </summary>
private async Task MessageActivityAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
// Start a Streaming Process
await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Working on a response for you", cancellationToken);
// Get the conversation history from turn state.
JsonElement threadElementStart = turnState.GetValue<JsonElement>("conversation.chatHistory");
// Deserialize the conversation history into an AgentThread, or create a new one if none exists.
AgentThread agentThread = threadElementStart.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null
? this._agent.DeserializeThread(threadElementStart, JsonUtilities.DefaultOptions)
: this._agent.GetNewThread();
ChatMessage chatMessage = HandleUserInput(turnContext);
// Invoke the WeatherForecastAgent to process the message
AgentRunResponse agentRunResponse = await this._agent.RunAsync(chatMessage, agentThread, cancellationToken: cancellationToken);
// Check for any user input requests in the response
// and turn them into adaptive cards in the streaming response.
List<Attachment>? attachments = null;
HandleUserInputRequests(agentRunResponse, ref attachments);
// Check for Adaptive Card content in the response messages
// and return them appropriately in the response.
var adaptiveCards = agentRunResponse.Messages.SelectMany(x => x.Contents).OfType<AdaptiveCardAIContent>().ToList();
if (adaptiveCards.Count > 0)
{
attachments ??= [];
attachments.Add(new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = adaptiveCards.First().AdaptiveCardJson,
});
}
else
{
turnContext.StreamingResponse.QueueTextChunk(agentRunResponse.Text);
}
// If created any adaptive cards, add them to the final message.
if (attachments is not null)
{
turnContext.StreamingResponse.FinalMessage = MessageFactory.Attachment(attachments);
}
// Serialize and save the updated conversation history back to turn state.
JsonElement threadElementEnd = agentThread.Serialize(JsonUtilities.DefaultOptions);
turnState.SetValue("conversation.chatHistory", threadElementEnd);
// End the streaming response
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken);
}
/// <summary>
/// A method to show a welcome message when a new user joins the conversation.
/// </summary>
private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(this._welcomeMessage))
{
return;
}
foreach (ChannelAccount member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text(this._welcomeMessage), cancellationToken);
}
}
}
/// <summary>
/// When a user responds to a function approval request by clicking on a card, this method converts the response
/// into the appropriate approval or rejection <see cref="ChatMessage"/>.
/// </summary>
/// <param name="turnContext">The <see cref="ITurnContext"/> for the current turn.</param>
/// <returns>The <see cref="ChatMessage"/> to pass to the <see cref="AIAgent"/>.</returns>
private static ChatMessage HandleUserInput(ITurnContext turnContext)
{
// Check if this contains the function approval Adaptive Card response.
if (turnContext.Activity.Value is JsonElement valueElement
&& valueElement.GetProperty("type").GetString() == "functionApproval"
&& valueElement.GetProperty("approved") is JsonElement approvedJsonElement
&& approvedJsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False
&& valueElement.GetProperty("requestJson") is JsonElement requestJsonElement
&& requestJsonElement.ValueKind == JsonValueKind.String)
{
var requestContent = JsonSerializer.Deserialize<FunctionApprovalRequestContent>(requestJsonElement.GetString()!, JsonUtilities.DefaultOptions);
return new ChatMessage(ChatRole.User, [requestContent!.CreateResponse(approvedJsonElement.ValueKind == JsonValueKind.True)]);
}
return new ChatMessage(ChatRole.User, turnContext.Activity.Text);
}
/// <summary>
/// When the agent returns any user input requests, this method converts them into adaptive cards that
/// asks the user to approve or deny the requests.
/// </summary>
/// <param name="response">The <see cref="AgentRunResponse"/> that may contain the user input requests.</param>
/// <param name="attachments">The list of <see cref="Attachment"/> to which the adaptive cards will be added.</param>
private static void HandleUserInputRequests(AgentRunResponse response, ref List<Attachment>? attachments)
{
var userInputRequests = response.UserInputRequests.ToList();
if (userInputRequests.Count > 0)
{
foreach (var functionApprovalRequest in userInputRequests.OfType<FunctionApprovalRequestContent>())
{
var functionApprovalRequestJson = JsonSerializer.Serialize(functionApprovalRequest, JsonUtilities.DefaultOptions);
var card = new AdaptiveCard("1.5");
card.Body.Add(new AdaptiveTextBlock
{
Text = "Function Call Approval Required",
Size = AdaptiveTextSize.Large,
Weight = AdaptiveTextWeight.Bolder,
HorizontalAlignment = AdaptiveHorizontalAlignment.Center
});
card.Body.Add(new AdaptiveTextBlock
{
Text = $"Function: {functionApprovalRequest.FunctionCall.Name}"
});
card.Body.Add(new AdaptiveActionSet()
{
Actions =
[
new AdaptiveSubmitAction
{
Id = "Approve",
Title = "Approve",
Data = new { type = "functionApproval", approved = true, requestJson = functionApprovalRequestJson }
},
new AdaptiveSubmitAction
{
Id = "Deny",
Title = "Deny",
Data = new { type = "functionApproval", approved = false, requestJson = functionApprovalRequestJson }
}
]
});
attachments ??= [];
attachments.Add(new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = card.ToJson(),
});
}
}
}
}
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using AdaptiveCards;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace M365Agent.Agents;
/// <summary>
/// An <see cref="AIContent"/> type allows an <see cref="AIAgent"/> to return adaptive cards as part of its response messages.
/// </summary>
internal sealed class AdaptiveCardAIContent : AIContent
{
public AdaptiveCardAIContent(AdaptiveCard adaptiveCard)
{
this.AdaptiveCard = adaptiveCard ?? throw new ArgumentNullException(nameof(adaptiveCard));
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
[JsonConstructor]
public AdaptiveCardAIContent(string adaptiveCardJson)
{
this.AdaptiveCardJson = adaptiveCardJson;
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
[JsonIgnore]
public AdaptiveCard AdaptiveCard { get; private set; }
public string AdaptiveCardJson
{
get => this.AdaptiveCard.ToJson();
set => this.AdaptiveCard = AdaptiveCard.FromJson(value).Card;
}
}
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json;
using AdaptiveCards;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace M365Agent.Agents;
/// <summary>
/// A weather forecasting agent. This agent wraps a <see cref="ChatClientAgent"/> and adds custom logic
/// to generate adaptive cards for weather forecasts and add these to the agent's response.
/// </summary>
public class WeatherForecastAgent : DelegatingAIAgent
{
private const string AgentName = "WeatherForecastAgent";
private const string AgentInstructions = """
You are a friendly assistant that helps people find a weather forecast for a given location.
You may ask follow up questions until you have enough information to answer the customers question.
When answering with a weather forecast, fill out the weatherCard property with an adaptive card containing the weather information and
add some emojis to indicate the type of weather.
When answering with just text, fill out the context property with a friendly response.
""";
/// <summary>
/// Initializes a new instance of the <see cref="WeatherForecastAgent"/> class.
/// </summary>
/// <param name="chatClient">An instance of <see cref="IChatClient"/> for interacting with an LLM.</param>
public WeatherForecastAgent(IChatClient chatClient)
: base(new ChatClientAgent(
chatClient: chatClient,
new ChatClientAgentOptions()
{
Name = AgentName,
Instructions = AgentInstructions,
ChatOptions = new ChatOptions()
{
Tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))],
// We want the agent to return structured output in a known format
// so that we can easily create adaptive cards from the response.
ResponseFormat = ChatResponseFormat.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(WeatherForecastAgentResponse)),
schemaName: "WeatherForecastAgentResponse",
schemaDescription: "Response to a query about the weather in a specified location"),
}
}))
{
}
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
var response = await base.RunAsync(messages, thread, options, cancellationToken);
// If the agent returned a valid structured output response
// we might be able to enhance the response with an adaptive card.
if (response.TryDeserialize<WeatherForecastAgentResponse>(JsonSerializerOptions.Web, out var structuredOutput))
{
var textContentMessage = response.Messages.FirstOrDefault(x => x.Contents.OfType<TextContent>().Any());
if (textContentMessage is not null)
{
// If the response contains weather information, create an adaptive card.
if (structuredOutput.ContentType == WeatherForecastAgentResponseContentType.WeatherForecastAgentResponse)
{
var card = CreateWeatherCard(structuredOutput.Location, structuredOutput.MeteorologicalCondition, structuredOutput.TemperatureInCelsius);
textContentMessage.Contents.Add(new AdaptiveCardAIContent(card));
}
// If the response is just text, replace the structured output with the text response.
if (structuredOutput.ContentType == WeatherForecastAgentResponseContentType.OtherAgentResponse)
{
var textContent = textContentMessage.Contents.OfType<TextContent>().First();
textContent.Text = structuredOutput.OtherResponse;
}
}
}
return response;
}
/// <summary>
/// A mock weather tool, to get weather information for a given location.
/// </summary>
[Description("Get the weather for a given location.")]
private static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
/// <summary>
/// Create an adaptive card to display weather information.
/// </summary>
private static AdaptiveCard CreateWeatherCard(string? location, string? condition, string? temperature)
{
var card = new AdaptiveCard("1.5");
card.Body.Add(new AdaptiveTextBlock
{
Text = "🌤️ Weather Forecast 🌤️",
Size = AdaptiveTextSize.Large,
Weight = AdaptiveTextWeight.Bolder,
HorizontalAlignment = AdaptiveHorizontalAlignment.Center
});
card.Body.Add(new AdaptiveTextBlock
{
Text = "Location: " + location,
});
card.Body.Add(new AdaptiveTextBlock
{
Text = "Condition: " + condition,
});
card.Body.Add(new AdaptiveTextBlock
{
Text = "Temperature: " + temperature,
});
return card;
}
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace M365Agent.Agents;
/// <summary>
/// The structured output type for the <see cref="WeatherForecastAgent"/>.
/// </summary>
internal sealed class WeatherForecastAgentResponse
{
/// <summary>
/// A value indicating whether the response contains a weather forecast or some other type of response.
/// </summary>
[JsonPropertyName("contentType")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public WeatherForecastAgentResponseContentType ContentType { get; set; }
/// <summary>
/// If the agent could not provide a weather forecast this should contain a textual response.
/// </summary>
[Description("If the answer is other agent response, contains the textual agent response.")]
[JsonPropertyName("otherResponse")]
public string? OtherResponse { get; set; }
/// <summary>
/// The location for which the weather forecast is given.
/// </summary>
[Description("If the answer is a weather forecast, contains the location for which the forecast is given.")]
[JsonPropertyName("location")]
public string? Location { get; set; }
/// <summary>
/// The temperature in Celsius for the given location.
/// </summary>
[Description("If the answer is a weather forecast, contains the temperature in Celsius.")]
[JsonPropertyName("temperatureInCelsius")]
public string? TemperatureInCelsius { get; set; }
/// <summary>
/// The meteorological condition for the given location.
/// </summary>
[Description("If the answer is a weather forecast, contains the meteorological condition (e.g., Sunny, Rainy).")]
[JsonPropertyName("meteorologicalCondition")]
public string? MeteorologicalCondition { get; set; }
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace M365Agent.Agents;
/// <summary>
/// The type of content contained in a <see cref="WeatherForecastAgentResponse"/>.
/// </summary>
internal enum WeatherForecastAgentResponseContentType
{
[JsonPropertyName("otherAgentResponse")]
OtherAgentResponse,
[JsonPropertyName("weatherForecastAgentResponse")]
WeatherForecastAgentResponse
}
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Concurrent;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.Agents.Authentication;
using Microsoft.Agents.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Validators;
namespace M365Agent;
internal static class AspNetExtensions
{
private static readonly CompositeFormat s_cachedValidTokenIssuerUrlTemplateV1Format = CompositeFormat.Parse(AuthenticationConstants.ValidTokenIssuerUrlTemplateV1);
private static readonly CompositeFormat s_cachedValidTokenIssuerUrlTemplateV2Format = CompositeFormat.Parse(AuthenticationConstants.ValidTokenIssuerUrlTemplateV2);
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> s_openIdMetadataCache = new();
/// <summary>
/// Adds AspNet token validation typical for ABS/SMBA and agent-to-agent using settings in configuration.
/// </summary>
/// <param name="services">The service collection to resolve dependencies.</param>
/// <param name="configuration">Used to read configuration settings.</param>
/// <param name="tokenValidationSectionName">Name of the config section to read.</param>
/// <remarks>
/// <para>This extension reads <see cref="TokenValidationOptions"/> settings from configuration. If configuration is missing JWT token
/// is not enabled.</para>
/// <p>The minimum, but typical, configuration is:</p>
/// <code>
/// "TokenValidation": {
/// "Enabled": boolean,
/// "Audiences": [
/// "{{ClientId}}" // this is the Client ID used for the Azure Bot
/// ],
/// "TenantId": "{{TenantId}}"
/// }
/// </code>
/// <para>The full options are:</para>
/// <code>
/// "TokenValidation": {
/// "Enabled": boolean,
/// "Audiences": [
/// "{required:agent-appid}"
/// ],
/// "TenantId": "{recommended:tenant-id}",
/// "ValidIssuers": [
/// "{default:Public-AzureBotService}"
/// ],
/// "IsGov": {optional:false},
/// "AzureBotServiceOpenIdMetadataUrl": optional,
/// "OpenIdMetadataUrl": optional,
/// "AzureBotServiceTokenHandling": "{optional:true}"
/// "OpenIdMetadataRefresh": "optional-12:00:00"
/// }
/// </code>
/// </remarks>
public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation")
{
IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName);
if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true))
{
// Noop if TokenValidation section missing or disabled.
System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled");
return;
}
services.AddAgentAspNetAuthentication(tokenValidationSection.Get<TokenValidationOptions>()!);
}
/// <summary>
/// Adds AspNet token validation typical for ABS/SMBA and agent-to-agent.
/// </summary>
public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions)
{
AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions));
// Must have at least one Audience.
if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0)
{
throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId");
}
// Audience values must be GUID's
foreach (var audience in validationOptions.Audiences)
{
if (!Guid.TryParse(audience, out _))
{
throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID");
}
}
// If ValidIssuers is empty, default for ABS Public Cloud
if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0)
{
validationOptions.ValidIssuers =
[
"https://api.botframework.com",
"https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
"https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
"https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
"https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
"https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/",
"https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0",
];
if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _))
{
validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, s_cachedValidTokenIssuerUrlTemplateV1Format, validationOptions.TenantId));
validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, s_cachedValidTokenIssuerUrlTemplateV2Format, validationOptions.TenantId));
}
}
// If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens.
if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl))
{
validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl;
}
// If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens.
if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl))
{
validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl;
}
var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval;
_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
ValidIssuers = validationOptions.ValidIssuers,
ValidAudiences = validationOptions.Audiences,
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
};
// Using Microsoft.IdentityModel.Validators
options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();
options.Events = new JwtBearerEvents
{
// Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens.
OnMessageReceived = async context =>
{
string authorizationHeader = context.Request.Headers.Authorization.ToString();
if (string.IsNullOrWhiteSpace(authorizationHeader))
{
// Default to AadTokenValidation handling
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
await Task.CompletedTask.ConfigureAwait(false);
return;
}
string[] parts = authorizationHeader.Split(' ')!;
if (parts.Length != 2 || parts[0] != "Bearer")
{
// Default to AadTokenValidation handling
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
await Task.CompletedTask.ConfigureAwait(false);
return;
}
JwtSecurityToken token = new(parts[1]);
string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!;
string openIdMetadataUrl = (validationOptions.AzureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.Ordinal))
? validationOptions.AzureBotServiceOpenIdMetadataUrl
: validationOptions.OpenIdMetadataUrl;
context.Options.TokenValidationParameters.ConfigurationManager = s_openIdMetadataCache.GetOrAdd(openIdMetadataUrl, key =>
{
return new ConfigurationManager<OpenIdConnectConfiguration>(openIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
{
AutomaticRefreshInterval = openIdMetadataRefresh
};
});
await Task.CompletedTask.ConfigureAwait(false);
},
OnTokenValidated = context => Task.CompletedTask,
OnForbidden = context => Task.CompletedTask,
OnAuthenticationFailed = context => Task.CompletedTask
};
});
}
}
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Authentication;
namespace M365Agent;
internal sealed class TokenValidationOptions
{
/// <summary>
/// The list of audiences to validate against.
/// </summary>
public IList<string>? Audiences { get; set; }
/// <summary>
/// TenantId of the Azure Bot. Optional but recommended.
/// </summary>
public string? TenantId { get; set; }
/// <summary>
/// Additional valid issuers. Optional, in which case the Public Azure Bot Service issuers are used.
/// </summary>
public IList<string>? ValidIssuers { get; set; }
/// <summary>
/// Can be omitted, in which case public Azure Bot Service and Azure Cloud metadata urls are used.
/// </summary>
public bool IsGov { get; set; }
/// <summary>
/// Azure Bot Service OpenIdMetadataUrl. Optional, in which case default value depends on IsGov.
/// </summary>
/// <see cref="AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl"/>
/// <see cref="AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl"/>
public string? AzureBotServiceOpenIdMetadataUrl { get; set; }
/// <summary>
/// Entra OpenIdMetadataUrl. Optional, in which case default value depends on IsGov.
/// </summary>
/// <see cref="AuthenticationConstants.PublicOpenIdMetadataUrl"/>
/// <see cref="AuthenticationConstants.GovOpenIdMetadataUrl"/>
public string? OpenIdMetadataUrl { get; set; }
/// <summary>
/// Determines if Azure Bot Service tokens are handled. Defaults to true and should always be true until Azure Bot Service sends Entra ID token.
/// </summary>
public bool AzureBotServiceTokenHandling { get; set; } = true;
/// <summary>
/// OpenIdMetadata refresh interval. Defaults to 12 hours.
/// </summary>
public TimeSpan? OpenIdMetadataRefresh { get; set; }
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using M365Agent.Agents;
using Microsoft.Extensions.AI;
namespace M365Agent;
/// <summary>Provides a collection of utility methods for working with JSON data in the context of the application.</summary>
internal static partial class JsonUtilities
{
/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.
/// </summary>
/// <remarks>
/// <para>
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// includes source generated contracts for all common exchange types contained in this library.
/// </para>
/// <para>
/// It additionally turns on the following settings:
/// <list type="number">
/// <item>Enables <see cref="JsonSerializerDefaults.Web"/> defaults.</item>
/// <item>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</item>
/// <item>Enables <see cref="JsonNumberHandling.AllowReadingFromString"/> as the default number handling for number types.</item>
/// </list>
/// </para>
/// </remarks>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
/// <summary>
/// Creates default options to use for agents-related serialization.
/// </summary>
/// <returns>The configured options.</returns>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
private static JsonSerializerOptions CreateDefaultOptions()
{
// Copy the configuration from the source generated context.
JsonSerializerOptions options = new(JsonContext.Default.Options)
{
// Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context.
// We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver.
TypeInfoResolver = JsonTypeInfoResolver.Combine(AIJsonUtilities.DefaultOptions.TypeInfoResolver, JsonContext.Default),
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as in AgentAbstractionsJsonUtilities and AIJsonUtilities
};
options.AddAIContentType<AdaptiveCardAIContent>(typeDiscriminatorId: "adaptiveCard");
if (JsonSerializer.IsReflectionEnabledByDefault)
{
options.Converters.Add(new JsonStringEnumConverter());
}
options.MakeReadOnly();
return options;
}
// Keep in sync with CreateDefaultOptions above.
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
// M365Agent specific types
[JsonSerializable(typeof(AdaptiveCardAIContent))]
[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
}
+30
View File
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b842df34-390f-490d-9dc0-73909363ad16</UserSecretsId>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="appsettings.json.template" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AdaptiveCards" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Microsoft.Agents.Authentication.Msal" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
+104
View File
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
// Sample that shows how to create an Agent Framework agent that is hosted using the M365 Agent SDK.
// The agent can then be consumed from various M365 channels.
// See the README.md for more information.
using Azure.AI.OpenAI;
using Azure.Identity;
using M365Agent;
using M365Agent.Agents;
using Microsoft.Agents.AI;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Hosting.AspNetCore;
using Microsoft.Agents.Storage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenAI;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
builder.Services.AddHttpClient();
// Register the inference service of your choice. AzureOpenAI and OpenAI are demonstrated...
IChatClient chatClient;
if (builder.Configuration.GetSection("AIServices").GetValue<bool>("UseAzureOpenAI"))
{
var deploymentName = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue<string>("DeploymentName")!;
var endpoint = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue<string>("Endpoint")!;
chatClient = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.AsIChatClient();
}
else
{
var modelId = builder.Configuration.GetSection("AIServices:OpenAI").GetValue<string>("ModelId")!;
var apiKey = builder.Configuration.GetSection("AIServices:OpenAI").GetValue<string>("ApiKey")!;
chatClient = new OpenAIClient(
apiKey)
.GetChatClient(modelId)
.AsIChatClient();
}
builder.Services.AddSingleton(chatClient);
// Add AgentApplicationOptions from appsettings section "AgentApplication".
builder.AddAgentApplicationOptions();
// Add the WeatherForecastAgent plus a welcome message.
// These will be consumed by the AFAgentApplication and exposed as an Agent SDK AgentApplication.
builder.Services.AddSingleton<AIAgent, WeatherForecastAgent>();
builder.Services.AddKeyedSingleton("AFAgentApplicationWelcomeMessage", "Hello and Welcome! I'm here to help with all your weather forecast needs!");
// Add the AgentApplication, which contains the logic for responding to
// user messages via the Agent SDK.
builder.AddAgent<AFAgentApplication>();
// Register IStorage. For development, MemoryStorage is suitable.
// For production Agents, persisted storage should be used so
// that state survives Agent restarts, and operates correctly
// in a cluster of Agent instances.
builder.Services.AddSingleton<IStorage, MemoryStorage>();
// Configure the HTTP request pipeline.
// Add AspNet token validation for Azure Bot Service and Entra. Authentication is
// configured in the appsettings.json "TokenValidation" section.
builder.Services.AddControllers();
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);
WebApplication app = builder.Build();
// Enable AspNet authentication and authorization
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", () => "Microsoft Agents SDK Sample");
// This receives incoming messages and routes them to the registered AgentApplication.
var incomingRoute = app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => await adapter.ProcessAsync(request, response, agent, cancellationToken));
if (!app.Environment.IsDevelopment())
{
incomingRoute.RequireAuthorization();
}
else
{
// Hardcoded for brevity and ease of testing.
// In production, this should be set in configuration.
app.Urls.Add("http://localhost:3978");
}
app.Run();
@@ -0,0 +1,12 @@
{
"profiles": {
"M365Agent": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:49692;http://localhost:49693"
}
}
}
+119
View File
@@ -0,0 +1,119 @@
# Microsoft Agent Framework agents with the M365 Agents SDK Weather Agent sample
This is a sample of a simple Weather Forecast Agent that is hosted on an Asp.Net core web service and is exposed via the M365 Agent SDK. This Agent is configured to accept a request asking for information about a weather forecast and respond to the caller with an Adaptive Card. This agent will handle multiple "turns" to get the required information from the user.
This Agent Sample is intended to introduce you the basics of integrating Agent Framework with the Microsoft 365 Agents SDK in order to use Agent Framework agents in various M365 services and applications. It can also be used as the base for a custom Agent that you choose to develop.
***Note:*** This sample requires JSON structured output from the model which works best from newer versions of the model such as gpt-4o-mini.
## Prerequisites
- [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download)
- [devtunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows)
- [Microsoft 365 Agents Toolkit](https://github.com/OfficeDev/microsoft-365-agents-toolkit)
- You will need an Azure OpenAI or OpenAI resource using `gpt-4o-mini`
- Configure OpenAI in appsettings
```json
"AIServices": {
"AzureOpenAI": {
"DeploymentName": "", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model
"Endpoint": "", // This is the Endpoint of the Azure OpenAI resource
"ApiKey": "" // This is the API Key of the Azure OpenAI resource. Optional, uses AzureCliCredential if not provided
},
"OpenAI": {
"ModelId": "", // This is the Model ID of the OpenAI model
"ApiKey": "" // This is your API Key for the OpenAI service
},
"UseAzureOpenAI": false // This is a flag to determine whether to use the Azure OpenAI or the OpenAI service
}
```
## QuickStart using Agent Toolkit
1. If you haven't done so already, install the Agents Playground
```
winget install agentsplayground
```
1. Start the sample application.
1. Start Agents Playground. At a command prompt: `agentsplayground`
- The tool will open a web browser showing the Microsoft 365 Agents Playground, ready to send messages to your agent.
1. Interact with the Agent via the browser
## QuickStart using WebChat or Teams
- Overview of running and testing an Agent
- Provision an Azure Bot in your Azure Subscription
- Configure your Agent settings to use to desired authentication type
- Running an instance of the Agent app (either locally or deployed to Azure)
- Test in a client
1. Create an Azure Bot with one of these authentication types
- [SingleTenant, Client Secret](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-single-secret)
- [SingleTenant, Federated Credentials](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-federated-credentials)
- [User Assigned Managed Identity](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-managed-identity)
> Be sure to follow the **Next Steps** at the end of these docs to configure your agent settings.
> **IMPORTANT:** If you want to run your agent locally via devtunnels, the only support auth type is ClientSecret and Certificates
1. Running the Agent
1. Running the Agent locally
- Requires a tunneling tool to allow for local development and debugging should you wish to do local development whilst connected to a external client such as Microsoft Teams.
- **For ClientSecret or Certificate authentication types only.** Federated Credentials and Managed Identity will not work via a tunnel to a local agent and must be deployed to an App Service or container.
1. Run `devtunnel`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below:
```bash
devtunnel host -p 3978 --allow-anonymous
```
1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages`
1. Start the Agent in Visual Studio
1. Deploy Agent code to Azure
1. VS Publish works well for this. But any tools used to deploy a web application will also work.
1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `https://{{appServiceDomain}}/api/messages`
## Testing this agent with WebChat
1. Select **Test in WebChat** under **Settings** on the Azure Bot in the Azure Portal
## Testing this Agent in Teams or M365
1. Update the manifest.json
- Edit the `manifest.json` contained in the `/appManifest` folder
- Replace with your AppId (that was created above) *everywhere* you see the place holder string `<<AAD_APP_CLIENT_ID>>`
- Replace `<<BOT_DOMAIN>>` with your Agent url. For example, the tunnel host name.
- Zip up the contents of the `/appManifest` folder to create a `manifest.zip`
- `manifest.json`
- `outline.png`
- `color.png`
1. Your Azure Bot should have the **Microsoft Teams** channel added under **Channels**.
1. Navigate to the Microsoft Admin Portal (MAC). Under **Settings** and **Integrated Apps,** select **Upload Custom App**.
1. Select the `manifest.zip` created in the previous step.
1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot.
## Enabling JWT token validation
1. By default, the AspNet token validation is disabled in order to support local debugging.
1. Enable by updating appsettings
```json
"TokenValidation": {
"Enabled": true,
"Audiences": [
"{{ClientId}}" // this is the Client ID used for the Azure Bot
],
"TenantId": "{{TenantId}}"
},
```
## Further reading
To learn more about using the M365 Agent SDK, see [Microsoft 365 Agents SDK](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/).
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,50 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.22/MicrosoftTeams.schema.json",
"manifestVersion": "1.22",
"version": "1.0.0",
"id": "<<AAD_APP_CLIENT_ID>>",
"developer": {
"name": "Microsoft, Inc.",
"websiteUrl": "https://example.azurewebsites.net",
"privacyUrl": "https://example.azurewebsites.net/privacy",
"termsOfUseUrl": "https://example.azurewebsites.net/termsofuse"
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"name": {
"short": "AF Sample Agent",
"full": "M365 AgentSDK and Microsoft Agent Framework Sample"
},
"description": {
"short": "Sample demonstrating M365 AgentSDK, Teams, and Microsoft Agent Framework",
"full": "Sample demonstrating M365 AgentSDK, Teams, and Microsoft Agent Framework"
},
"accentColor": "#FFFFFF",
"copilotAgents": {
"customEngineAgents": [
{
"id": "<<AAD_APP_CLIENT_ID>>",
"type": "bot"
}
]
},
"bots": [
{
"botId": "<<AAD_APP_CLIENT_ID>>",
"scopes": [
"personal"
],
"supportsFiles": false,
"isNotificationOnly": false
}
],
"permissions": [
"identity",
"messageTeamMembers"
],
"validDomains": [
"<<BOT_DOMAIN>>"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

@@ -0,0 +1,54 @@
{
"TokenValidation": {
"Enabled": false,
"Audiences": [
"{{ClientId}}" // this is the Client ID used for the Azure Bot
],
"TenantId": "{{TenantId}}"
},
"AgentApplication": {
"StartTypingTimer": true,
"RemoveRecipientMention": false,
"NormalizeMentions": false
},
"Connections": {
"ServiceConnection": {
"Settings": {
// this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret.
"AuthType": ""
// Other properties dependent on the authorization type the Azure Bot uses.
}
}
},
"ConnectionsMap": [
{
"ServiceUrl": "*",
"Connection": "ServiceConnection"
}
],
// This is the configuration for the AI services, use environment variables or user secrets to store sensitive information.
// Do not store sensitive information in this file
"AIServices": {
"AzureOpenAI": {
"DeploymentName": "", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model
"Endpoint": "", // This is the Endpoint of the Azure OpenAI resource
"ApiKey": "" // This is the API Key of the Azure OpenAI resource. Optional, uses AzureCliCredential if not provided
},
"OpenAI": {
"ModelId": "", // This is the Model ID of the OpenAI model
"ApiKey": "" // This is your API Key for the OpenAI service
},
"UseAzureOpenAI": false // This is a flag to determine whether to use the Azure OpenAI or the OpenAI service
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -1,10 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
@@ -27,21 +24,26 @@ internal static class EntitiesApiExtensions
/// <item><description>GET /v1/entities/{entityId}/info - Get detailed information about a specific entity</description></item>
/// </list>
/// The endpoints are compatible with the Python DevUI frontend and automatically discover entities
/// from the registered <see cref="AgentCatalog"/> and <see cref="WorkflowCatalog"/> services.
/// from the registered <see cref="AIAgent">agents</see> and <see cref="Workflow">workflows</see> in the dependency injection container.
/// </remarks>
public static IEndpointConventionBuilder MapEntities(this IEndpointRouteBuilder endpoints)
{
var registeredAIAgents = GetRegisteredEntities<AIAgent>(endpoints.ServiceProvider);
var registeredWorkflows = GetRegisteredEntities<Workflow>(endpoints.ServiceProvider);
var group = endpoints.MapGroup("/v1/entities")
.WithTags("Entities");
// List all entities
group.MapGet("", ListEntitiesAsync)
group.MapGet("", (CancellationToken cancellationToken)
=> ListEntitiesAsync(registeredAIAgents, registeredWorkflows, cancellationToken))
.WithName("ListEntities")
.WithSummary("List all registered entities (agents and workflows)")
.Produces<DiscoveryResponse>(StatusCodes.Status200OK, contentType: "application/json");
// Get detailed entity information
group.MapGet("{entityId}/info", GetEntityInfoAsync)
group.MapGet("{entityId}/info", (string entityId, string? type, CancellationToken cancellationToken)
=> GetEntityInfoAsync(entityId, type, registeredAIAgents, registeredWorkflows, cancellationToken))
.WithName("GetEntityInfo")
.WithSummary("Get detailed information about a specific entity")
.Produces<EntityInfo>(StatusCodes.Status200OK, contentType: "application/json")
@@ -51,8 +53,8 @@ internal static class EntitiesApiExtensions
}
private static async Task<IResult> ListEntitiesAsync(
AgentCatalog? agentCatalog,
WorkflowCatalog? workflowCatalog,
IEnumerable<AIAgent> agents,
IEnumerable<Workflow> workflows,
CancellationToken cancellationToken)
{
try
@@ -60,13 +62,13 @@ internal static class EntitiesApiExtensions
var entities = new Dictionary<string, EntityInfo>();
// Discover agents
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
foreach (var agentInfo in DiscoverAgents(agents, entityIdFilter: null))
{
entities[agentInfo.Id] = agentInfo;
}
// Discover workflows
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
foreach (var workflowInfo in DiscoverWorkflows(workflows, entityIdFilter: null))
{
entities[workflowInfo.Id] = workflowInfo;
}
@@ -85,15 +87,15 @@ internal static class EntitiesApiExtensions
private static async Task<IResult> GetEntityInfoAsync(
string entityId,
string? type,
AgentCatalog? agentCatalog,
WorkflowCatalog? workflowCatalog,
IEnumerable<AIAgent> agents,
IEnumerable<Workflow> workflows,
CancellationToken cancellationToken)
{
try
{
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
{
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
foreach (var workflowInfo in DiscoverWorkflows(workflows, entityId))
{
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
}
@@ -101,7 +103,7 @@ internal static class EntitiesApiExtensions
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
{
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
foreach (var agentInfo in DiscoverAgents(agents, entityId))
{
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
}
@@ -118,17 +120,9 @@ internal static class EntitiesApiExtensions
}
}
private static async IAsyncEnumerable<EntityInfo> DiscoverAgentsAsync(
AgentCatalog? agentCatalog,
string? entityIdFilter,
[EnumeratorCancellation] CancellationToken cancellationToken)
private static IEnumerable<EntityInfo> DiscoverAgents(IEnumerable<AIAgent> agents, string? entityIdFilter)
{
if (agentCatalog is null)
{
yield break;
}
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
foreach (var agent in agents)
{
// If filtering by entity ID, skip non-matching agents
if (entityIdFilter is not null &&
@@ -148,17 +142,9 @@ internal static class EntitiesApiExtensions
}
}
private static async IAsyncEnumerable<EntityInfo> DiscoverWorkflowsAsync(
WorkflowCatalog? workflowCatalog,
string? entityIdFilter,
[EnumeratorCancellation] CancellationToken cancellationToken)
private static IEnumerable<EntityInfo> DiscoverWorkflows(IEnumerable<Workflow> workflows, string? entityIdFilter)
{
if (workflowCatalog is null)
{
yield break;
}
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
foreach (var workflow in workflows)
{
var workflowId = workflow.Name ?? workflow.StartExecutorId;
@@ -304,4 +290,14 @@ internal static class EntitiesApiExtensions
StartExecutorId = workflow.StartExecutorId
};
}
private static IEnumerable<T> GetRegisteredEntities<T>(IServiceProvider serviceProvider)
{
var keyedEntities = serviceProvider.GetKeyedServices<T>(KeyedService.AnyKey);
var defaultEntities = serviceProvider.GetServices<T>() ?? [];
return keyedEntities
.Concat(defaultEntities)
.Where(entity => entity is not null);
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Extensions.Hosting;
/// <summary>
/// Extension methods for <see cref="IHostApplicationBuilder"/> to configure DevUI.
/// </summary>
public static class MicrosoftAgentAIDevUIHostApplicationBuilderExtensions
{
/// <summary>
/// Adds DevUI services to the host application builder.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
/// <returns>The <see cref="IHostApplicationBuilder"/> for method chaining.</returns>
public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddDevUI();
return builder;
}
}
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugCoreTargetFrameworks)</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.Agents.AI.DevUI</RootNamespace>
@@ -12,6 +13,10 @@
<NoWarn>$(NoWarn);CS1591;CA1852;CA1050;RCS1037;RCS1036;RCS1124;RCS1021;RCS1146;RCS1211;CA2007;CA1308;IL2026;IL3050;CA1812</NoWarn>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<!-- Import nuget packaging properties -->
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
@@ -33,4 +38,7 @@
<Description>Provides Microsoft Agent Framework support for developer UI.</Description>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.DevUI.UnitTests"/>
</ItemGroup>
</Project>
@@ -24,9 +24,15 @@ var builder = WebApplication.CreateBuilder(args);
// Register your agents
builder.AddAIAgent("assistant", "You are a helpful assistant.");
// Register DevUI services
if (builder.Environment.IsDevelopment())
{
builder.AddDevUI();
}
// Register services for OpenAI responses and conversations (also required for DevUI)
builder.Services.AddOpenAIResponses();
builder.Services.AddOpenAIConversations();
builder.AddOpenAIResponses();
builder.AddOpenAIConversations();
var app = builder.Build();
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/> to configure DevUI.
/// </summary>
public static class MicrosoftAgentAIDevUIServiceCollectionsExtensions
{
/// <summary>
/// Adds services required for DevUI integration.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <returns>The <see cref="IServiceCollection"/> for method chaining.</returns>
public static IServiceCollection AddDevUI(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// a factory that tries to construct an AIAgent from Workflow,
// even if workflow was not explicitly registered as an AIAgent.
#pragma warning disable IDE0001 // Simplify Names
services.AddKeyedSingleton<AIAgent>(KeyedService.AnyKey, (sp, key) =>
{
var keyAsStr = key as string;
Throw.IfNullOrEmpty(keyAsStr);
var workflow = sp.GetKeyedService<Workflow>(keyAsStr);
if (workflow is not null)
{
return workflow.AsAgent(name: workflow.Name);
}
// another thing we can do is resolve a non-keyed workflow.
// however, we can't rely on anything than key to be equal to the workflow.Name.
// so we try: if we fail, we return null.
workflow = sp.GetService<Workflow>();
if (workflow is not null && workflow.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true)
{
return workflow.AsAgent(name: workflow.Name);
}
// and it's possible to lookup at the default-registered AIAgent
// with the condition of same name as the key.
var agent = sp.GetService<AIAgent>();
if (agent is not null && agent.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true)
{
return agent;
}
return null!;
});
#pragma warning restore IDE0001 // Simplify Names
return services;
}
}
@@ -83,12 +83,13 @@ internal static class BuiltInFunctions
string? threadIdValue = threadIdFromBody ?? threadIdFromQuery;
// If no session ID is provided, use a new one based on the function name and invocation ID.
// This may be better than a random one because it can be correlated with the function invocation.
// Specifying a session ID is how the caller correlates multiple calls to the same agent session.
// The thread_id is treated as a session key (not a full session ID).
// If no session key is provided, use the function invocation ID as the session key
// to help correlate the session with the function invocation.
string agentName = GetAgentName(context);
AgentSessionId sessionId = string.IsNullOrEmpty(threadIdValue)
? new AgentSessionId(GetAgentName(context), context.InvocationId)
: AgentSessionId.Parse(threadIdValue);
? new AgentSessionId(agentName, context.InvocationId)
: new AgentSessionId(agentName, threadIdValue);
if (string.IsNullOrWhiteSpace(message))
{
@@ -110,7 +111,7 @@ internal static class BuiltInFunctions
}
}
AIAgent agentProxy = client.AsDurableAgentProxy(context, GetAgentName(context));
AIAgent agentProxy = client.AsDurableAgentProxy(context, agentName);
DurableAgentRunOptions options = new() { IsFireAndForget = !waitForResponse };
@@ -126,7 +127,7 @@ internal static class BuiltInFunctions
req,
context,
HttpStatusCode.OK,
sessionId.ToString(),
sessionId.Key,
agentResponse);
}
@@ -140,7 +141,7 @@ internal static class BuiltInFunctions
return await CreateAcceptedResponseAsync(
req,
context,
sessionId.ToString());
sessionId.Key);
}
public static async Task<string?> RunMcpToolAsync(
@@ -63,7 +63,11 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
return ValueTask.FromResult<ResponseError?>(new ResponseError
{
Code = "agent_not_found",
Message = $"Agent '{agentName}' not found. Ensure the agent is registered with AddAIAgent()."
Message = $"""
Agent '{agentName}' not found.
Ensure the agent is registered with '{agentName}' name in the dependency injection container.
We recommend using 'builder.AddAIAgent()' for simplicity.
"""
});
}
@@ -1,38 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
namespace Microsoft.Agents.AI.Hosting;
/// <summary>
/// Provides a catalog of registered AI agents within the hosting environment.
/// </summary>
/// <remarks>
/// The agent catalog allows enumeration of all registered agents in the dependency injection container.
/// This is useful for scenarios where you need to discover and interact with multiple agents programmatically.
/// </remarks>
public abstract class AgentCatalog
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentCatalog"/> class.
/// </summary>
protected AgentCatalog()
{
}
/// <summary>
/// Asynchronously retrieves all registered AI agents from the catalog.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// An asynchronous enumerable of <see cref="AIAgent"/> instances representing all registered agents.
/// The enumeration will only include agents that are successfully resolved from the service provider.
/// </returns>
/// <remarks>
/// This method enumerates through all registered agent names and attempts to resolve each agent
/// from the dependency injection container. Only successfully resolved agents are yielded.
/// The enumeration is lazy and agents are resolved on-demand during iteration.
/// </remarks>
public abstract IAsyncEnumerable<AIAgent> GetAgentsAsync(CancellationToken cancellationToken = default);
}
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Hosting.Local;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -126,31 +125,9 @@ public static class AgentHostingServiceCollectionExtensions
return agent;
});
// Register the agent by name for discovery.
var agentHostBuilder = GetAgentRegistry(services);
agentHostBuilder.AgentNames.Add(name);
return new HostedAgentBuilder(name, services);
}
private static LocalAgentRegistry GetAgentRegistry(IServiceCollection services)
{
var descriptor = services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalAgentRegistry)));
if (descriptor?.ImplementationInstance is not LocalAgentRegistry instance)
{
instance = new LocalAgentRegistry();
ConfigureHostBuilder(services, instance);
}
return instance;
}
private static void ConfigureHostBuilder(IServiceCollection services, LocalAgentRegistry agentHostBuilderContext)
{
services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
services.AddSingleton<AgentCatalog, LocalAgentCatalog>();
}
private static IList<AITool> GetRegisteredToolsForAgent(IServiceProvider serviceProvider, string agentName)
{
var registry = serviceProvider.GetService<LocalAgentToolRegistry>();
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using Microsoft.Agents.AI.Hosting.Local;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -47,28 +45,6 @@ public static class HostApplicationBuilderWorkflowExtensions
return workflow;
});
// Register the workflow by name for discovery.
var workflowRegistry = GetWorkflowRegistry(builder);
workflowRegistry.WorkflowNames.Add(name);
return new HostedWorkflowBuilder(name, builder);
}
private static LocalWorkflowRegistry GetWorkflowRegistry(IHostApplicationBuilder builder)
{
var descriptor = builder.Services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalWorkflowRegistry)));
if (descriptor?.ImplementationInstance is not LocalWorkflowRegistry instance)
{
instance = new LocalWorkflowRegistry();
ConfigureHostBuilder(builder, instance);
}
return instance;
}
private static void ConfigureHostBuilder(IHostApplicationBuilder builder, LocalWorkflowRegistry agentHostBuilderContext)
{
builder.Services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
builder.Services.AddSingleton<WorkflowCatalog, LocalWorkflowCatalog>();
}
}
@@ -1,37 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.Local;
// Implementation of an AgentCatalog which enumerates agents registered in the local service provider.
internal sealed class LocalAgentCatalog : AgentCatalog
{
public readonly HashSet<string> _registeredAgents;
private readonly IServiceProvider _serviceProvider;
public LocalAgentCatalog(LocalAgentRegistry agentHostBuilder, IServiceProvider serviceProvider)
{
this._registeredAgents = [.. agentHostBuilder.AgentNames];
this._serviceProvider = serviceProvider;
}
public override async IAsyncEnumerable<AIAgent> GetAgentsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask.ConfigureAwait(false);
foreach (var name in this._registeredAgents)
{
var agent = this._serviceProvider.GetKeyedService<AIAgent>(name);
if (agent is not null)
{
yield return agent;
}
}
}
}
@@ -1,10 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Agents.AI.Hosting.Local;
internal sealed class LocalAgentRegistry
{
public HashSet<string> AgentNames { get; } = [];
}
@@ -1,37 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.Local;
internal sealed class LocalWorkflowCatalog : WorkflowCatalog
{
public readonly HashSet<string> _registeredWorkflows;
private readonly IServiceProvider _serviceProvider;
public LocalWorkflowCatalog(LocalWorkflowRegistry workflowRegistry, IServiceProvider serviceProvider)
{
this._registeredWorkflows = [.. workflowRegistry.WorkflowNames];
this._serviceProvider = serviceProvider;
}
public override async IAsyncEnumerable<Workflow> GetWorkflowsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask.ConfigureAwait(false);
foreach (var name in this._registeredWorkflows)
{
var workflow = this._serviceProvider.GetKeyedService<Workflow>(name);
if (workflow is not null)
{
yield return workflow;
}
}
}
}
@@ -1,10 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Agents.AI.Hosting.Local;
internal sealed class LocalWorkflowRegistry
{
public HashSet<string> WorkflowNames { get; } = [];
}
@@ -75,8 +75,8 @@ public class ChatClientAgentThread : AgentThread
/// <remarks>
/// <para>
/// Note that either <see cref="ConversationId"/> or <see cref="MessageStore "/> may be set, but not both.
/// If <see cref="MessageStore "/> is not null, and <see cref="ConversationId"/> is set, <see cref="MessageStore "/>
/// will be reverted to null, and vice versa.
/// If <see cref="MessageStore "/> is not null, setting <see cref="ConversationId"/> will throw an
/// <see cref="InvalidOperationException "/> exception.
/// </para>
/// <para>
/// This property may be null in the following cases:
@@ -91,6 +91,7 @@ public class ChatClientAgentThread : AgentThread
/// to fork the thread with each iteration.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">Attempted to set a conversation ID but a <see cref="MessageStore"/> is already set.</exception>
public string? ConversationId
{
get => this._conversationId;
@@ -0,0 +1,222 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
/// <summary>
/// Unit tests for DevUI service collection extensions.
/// Tests verify that workflows and agents can be resolved even when registered non-conventionally.
/// </summary>
public class DevUIExtensionsTests
{
/// <summary>
/// Verifies that AddDevUI throws ArgumentNullException when services collection is null.
/// </summary>
[Fact]
public void AddDevUI_NullServices_ThrowsArgumentNullException()
{
IServiceCollection services = null!;
Assert.Throws<ArgumentNullException>(() => services.AddDevUI());
}
/// <summary>
/// Verifies that GetRequiredKeyedService throws for non-existent keys.
/// </summary>
[Fact]
public void AddDevUI_GetRequiredKeyedServiceNonExistent_ThrowsInvalidOperationException()
{
// Arrange
var services = new ServiceCollection();
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act & Assert
Assert.Throws<InvalidOperationException>(() => serviceProvider.GetRequiredKeyedService<AIAgent>("non-existent"));
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_WorkflowWithName_CanBeResolved_AsAIAgent()
{
// Arrange
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow", workflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act
var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow");
// Assert
Assert.NotNull(resolvedWorkflowAsAgent);
Assert.Null(resolvedWorkflowAsAgent.Name);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_MultipleWorkflowsWithName_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow1 = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
var workflow2 = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow1", workflow1);
services.AddKeyedSingleton("workflow2", workflow2);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedWorkflow1AsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow1");
Assert.NotNull(resolvedWorkflow1AsAgent);
Assert.Null(resolvedWorkflow1AsAgent.Name);
var resolvedWorkflow2AsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow2");
Assert.NotNull(resolvedWorkflow2AsAgent);
Assert.Null(resolvedWorkflow2AsAgent.Name);
Assert.False(resolvedWorkflow1AsAgent == resolvedWorkflow2AsAgent);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_NonKeyedWorkflow_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow", workflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow");
Assert.NotNull(resolvedWorkflowAsAgent);
Assert.Null(resolvedWorkflowAsAgent.Name);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_NonKeyedWorkflow_PlusKeyedWorkflow_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential("standardname", agent1, agent2);
var keyedWorkflow = AgentWorkflowBuilder.BuildSequential("keyedname", agent1, agent2);
services.AddSingleton(workflow);
services.AddKeyedSingleton("keyed", keyedWorkflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// resolve a workflow with the same name as workflow's name (which is registered without a key)
var standardAgent = serviceProvider.GetKeyedService<AIAgent>("standardname");
Assert.NotNull(standardAgent);
Assert.Equal("standardname", standardAgent.Name);
var keyedAgent = serviceProvider.GetKeyedService<AIAgent>("keyed");
Assert.NotNull(keyedAgent);
Assert.Equal("keyedname", keyedAgent.Name);
var nonExisting = serviceProvider.GetKeyedService<AIAgent>("random-non-existing!!!");
Assert.Null(nonExisting);
}
/// <summary>
/// Verifies that an agent registered with a different key than its name can be resolved by key.
/// </summary>
[Fact]
public void AddDevUI_AgentRegisteredWithDifferentKey_CanBeResolvedByKey()
{
// Arrange
var services = new ServiceCollection();
const string AgentName = "actual-agent-name";
const string RegistrationKey = "different-key";
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", AgentName);
services.AddKeyedSingleton<AIAgent>(RegistrationKey, agent);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act
var resolvedAgent = serviceProvider.GetKeyedService<AIAgent>(RegistrationKey);
// Assert
Assert.NotNull(resolvedAgent);
// The resolved agent should have the agent's name, not the registration key
Assert.Equal(AgentName, resolvedAgent.Name);
}
/// <summary>
/// Verifies that an agent registered with a different key than its name can be resolved by key.
/// </summary>
[Fact]
public void AddDevUI_Keyed_AndStandard_BothCanBeResolved()
{
// Arrange
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var defaultAgent = new ChatClientAgent(mockChatClient.Object, "default", "default");
var keyedAgent = new ChatClientAgent(mockChatClient.Object, "keyed", "keyed");
services.AddSingleton<AIAgent>(defaultAgent);
services.AddKeyedSingleton<AIAgent>("keyed-registration", keyedAgent);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedKeyedAgent = serviceProvider.GetKeyedService<AIAgent>("keyed-registration");
Assert.NotNull(resolvedKeyedAgent);
Assert.Equal("keyed", resolvedKeyedAgent.Name);
// resolving default agent based on its name, not on the registration-key
var resolvedDefaultAgent = serviceProvider.GetKeyedService<AIAgent>("default");
Assert.NotNull(resolvedDefaultAgent);
Assert.Equal("default", resolvedDefaultAgent.Name);
}
/// <summary>
/// Verifies that the DevUI fallback handler error message includes helpful information.
/// </summary>
[Fact]
public void AddDevUI_InvalidResolution_ErrorMessageIsInformative()
{
// Arrange
var services = new ServiceCollection();
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
const string InvalidKey = "invalid-key-name";
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => serviceProvider.GetRequiredKeyedService<AIAgent>(InvalidKey));
}
}
@@ -0,0 +1,285 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Workflows;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
public class DevUIIntegrationTests
{
private sealed class NoOpExecutor(string id) : Executor(id)
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<object>(
(msg, ctx) => ctx.SendMessageAsync(msg));
}
[Fact]
public async Task TestServerWithDevUI_ResolvesRequestToWorkflow_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", "agent-name");
builder.Services.AddKeyedSingleton<AIAgent>("registration-key", agent);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var resolvedAgent = app.Services.GetKeyedService<AIAgent>("registration-key");
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
Assert.NotNull(discoveryResponse);
Assert.Single(discoveryResponse.Entities);
Assert.Equal("agent-name", discoveryResponse.Entities[0].Name);
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMultipleAIAgents_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-one");
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-two");
var agent3 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-three");
builder.Services.AddKeyedSingleton<AIAgent>("key-1", agent1);
builder.Services.AddKeyedSingleton<AIAgent>("key-2", agent2);
builder.Services.AddKeyedSingleton<AIAgent>("key-3", agent3);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-three" && e.Type == "agent");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesAIAgents_WithKeyedAndDefaultRegistrationAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agentKeyed1 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-one");
var agentKeyed2 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-two");
var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-agent");
builder.Services.AddKeyedSingleton<AIAgent>("key-1", agentKeyed1);
builder.Services.AddKeyedSingleton<AIAgent>("key-2", agentKeyed2);
builder.Services.AddSingleton<AIAgent>(agentDefault);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-agent" && e.Type == "agent");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMultipleWorkflows_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var workflow1 = new WorkflowBuilder("executor-1")
.WithName("workflow-one")
.WithDescription("First workflow")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflow2 = new WorkflowBuilder("executor-2")
.WithName("workflow-two")
.WithDescription("Second workflow")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflow3 = new WorkflowBuilder("executor-3")
.WithName("workflow-three")
.WithDescription("Third workflow")
.BindExecutor(new NoOpExecutor("executor-3"))
.Build();
builder.Services.AddKeyedSingleton("key-1", workflow1);
builder.Services.AddKeyedSingleton("key-2", workflow2);
builder.Services.AddKeyedSingleton("key-3", workflow3);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-three" && e.Type == "workflow");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesWorkflows_WithKeyedAndDefaultRegistrationAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var workflowKeyed1 = new WorkflowBuilder("executor-1")
.WithName("keyed-workflow-one")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflowKeyed2 = new WorkflowBuilder("executor-2")
.WithName("keyed-workflow-two")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflowDefault = new WorkflowBuilder("executor-default")
.WithName("default-workflow")
.BindExecutor(new NoOpExecutor("executor-default"))
.Build();
builder.Services.AddKeyedSingleton("key-1", workflowKeyed1);
builder.Services.AddKeyedSingleton("key-2", workflowKeyed2);
builder.Services.AddSingleton(workflowDefault);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-workflow" && e.Type == "workflow");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMixedAgentsAndWorkflows_AllRegistrationsAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
// Create AIAgents
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-one");
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-two");
var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-mixed-agent");
// Create Workflows
var workflow1 = new WorkflowBuilder("executor-1")
.WithName("mixed-workflow-one")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflow2 = new WorkflowBuilder("executor-2")
.WithName("mixed-workflow-two")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflowDefault = new WorkflowBuilder("executor-default")
.WithName("default-mixed-workflow")
.BindExecutor(new NoOpExecutor("executor-default"))
.Build();
// Register all
builder.Services.AddKeyedSingleton<AIAgent>("agent-key-1", agent1);
builder.Services.AddKeyedSingleton<AIAgent>("agent-key-2", agent2);
builder.Services.AddSingleton<AIAgent>(agentDefault);
builder.Services.AddKeyedSingleton("workflow-key-1", workflow1);
builder.Services.AddKeyedSingleton("workflow-key-2", workflow2);
builder.Services.AddSingleton(workflowDefault);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(6, discoveryResponse.Entities.Count);
// Verify agents
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-agent" && e.Type == "agent");
// Verify workflows
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-workflow" && e.Type == "workflow");
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugCoreTargetFrameworks)</TargetFrameworks>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" VersionOverride="8.0.21" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' != 'net8.0'" />
<PackageReference Include="OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
{
"profiles": {
"Microsoft.Agents.AI.DevUI.UnitTests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:63009;http://localhost:63010"
}
}
}
@@ -0,0 +1,12 @@
{
"profiles": {
"Microsoft.Agents.AI.Hosting.A2A.UnitTests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52186;http://localhost:52187"
}
}
}
@@ -75,9 +75,9 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi
// The response headers should include the agent thread ID, which can be used to continue the conversation.
string? threadId = response.Headers.GetValues("x-ms-thread-id")?.FirstOrDefault();
Assert.NotNull(threadId);
Assert.NotEmpty(threadId);
this._outputHelper.WriteLine($"Agent thread ID: {threadId}");
Assert.StartsWith("@dafx-joker@", threadId);
// Wait for up to 30 seconds to see if the agent response is available in the logs
await this.WaitForConditionAsync(
@@ -289,7 +289,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi
startResponse.Headers.TryGetValues("x-ms-thread-id", out IEnumerable<string>? agentIdValues);
string? threadId = agentIdValues?.FirstOrDefault();
Assert.NotNull(threadId);
Assert.StartsWith("@dafx-publisher@", threadId);
Assert.NotEmpty(threadId);
// Wait for the orchestration to report that it's waiting for human approval
await this.WaitForConditionAsync(
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
@@ -15,10 +16,18 @@ internal sealed class MockAgentProvider : Mock<WorkflowAgentProvider>
{
public IList<string> ExistingConversationIds { get; } = [];
public ChatMessage? TestChatMessage { get; set; }
public MockAgentProvider()
{
this.Setup(provider => provider.CreateConversationAsync(It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult(this.CreateConversationId()));
this.Setup(provider => provider.GetMessageAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(this.CreateChatMessage()));
}
private string CreateConversationId()
@@ -28,4 +37,13 @@ internal sealed class MockAgentProvider : Mock<WorkflowAgentProvider>
return newConversationId;
}
private ChatMessage CreateChatMessage()
{
this.TestChatMessage = new ChatMessage(ChatRole.User, Guid.NewGuid().ToString("N"))
{
MessageId = Guid.NewGuid().ToString("N"),
};
return this.TestChatMessage;
}
}
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="RetrieveConversationMessageExecutor"/>.
/// </summary>
public sealed class RetrieveConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task RetrieveMessageSuccessfullyAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(RetrieveMessageSuccessfullyAsync),
"TestMessage");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
RetrieveConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
"DefaultMessageId");
RetrieveConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage testMessage = mockAgentProvider.TestChatMessage ?? new ChatMessage();
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private RetrieveConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
string messageId)
{
RetrieveConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
MessageId = StringExpression.Literal(messageId)
};
return AssignParent<RetrieveConversationMessage>(actionBuilder);
}
}
+1
View File
@@ -59,6 +59,7 @@
"OPENAI",
"opentelemetry",
"OTEL",
"powerfx",
"protos",
"pydantic",
"pytestmark",
+8
View File
@@ -3,6 +3,14 @@ AZURE_AI_PROJECT_ENDPOINT=""
AZURE_AI_MODEL_DEPLOYMENT_NAME=""
# Bing connection for web search (optional, used by samples with web search)
BING_CONNECTION_ID=""
# Azure AI Search (optional, used by AzureAISearchContextProvider samples)
AZURE_SEARCH_ENDPOINT=""
AZURE_SEARCH_API_KEY=""
AZURE_SEARCH_INDEX_NAME=""
AZURE_SEARCH_SEMANTIC_CONFIG=""
AZURE_SEARCH_KNOWLEDGE_BASE_NAME=""
# Note: For agentic mode Knowledge Bases, also set AZURE_OPENAI_ENDPOINT below
# (different from AZURE_AI_PROJECT_ENDPOINT - Knowledge Base needs OpenAI endpoint for model calls)
# OpenAI
OPENAI_API_KEY=""
OPENAI_CHAT_MODEL_ID=""
+39 -1
View File
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0b251120] - 2025-11-20
### Added
- **agent-framework-core**: Introducing support for declarative YAML spec ([#2002](https://github.com/microsoft/agent-framework/pull/2002))
- **agent-framework-core**: Use AI Foundry evaluators for self-reflection ([#2250](https://github.com/microsoft/agent-framework/pull/2250))
- **agent-framework-core**: Propagate `as_tool()` kwargs and add runtime context + middleware sample ([#2311](https://github.com/microsoft/agent-framework/pull/2311))
- **agent-framework-anthropic**: Anthropic Foundry integration ([#2302](https://github.com/microsoft/agent-framework/pull/2302))
- **samples**: M365 Agent SDK Hosting sample ([#2292](https://github.com/microsoft/agent-framework/pull/2292))
- **samples**: Foundry Sample for A2A + SharePoint Samples ([#2313](https://github.com/microsoft/agent-framework/pull/2313))
### Changed
- **agent-framework-azurefunctions**: [BREAKING] Schema changes for Azure Functions package ([#2151](https://github.com/microsoft/agent-framework/pull/2151))
- **agent-framework-core**: Move evaluation folders under `evaluations` ([#2355](https://github.com/microsoft/agent-framework/pull/2355))
- **agent-framework-core**: Move red teaming files to their own folder ([#2333](https://github.com/microsoft/agent-framework/pull/2333))
- **agent-framework-core**: "fix all" task now single source of truth ([#2303](https://github.com/microsoft/agent-framework/pull/2303))
- **agent-framework-core**: Improve and clean up exception handling ([#2337](https://github.com/microsoft/agent-framework/pull/2337), [#2319](https://github.com/microsoft/agent-framework/pull/2319))
- **agent-framework-core**: Clean up imports ([#2318](https://github.com/microsoft/agent-framework/pull/2318))
### Fixed
- **agent-framework-azure-ai**: Fix for Azure AI client ([#2358](https://github.com/microsoft/agent-framework/pull/2358))
- **agent-framework-core**: Fix tool execution bleed-over in aiohttp/Bot Framework scenarios ([#2314](https://github.com/microsoft/agent-framework/pull/2314))
- **agent-framework-core**: `@ai_function` now correctly handles `self` parameter ([#2266](https://github.com/microsoft/agent-framework/pull/2266))
- **agent-framework-core**: Resolve string annotations in `FunctionExecutor` ([#2308](https://github.com/microsoft/agent-framework/pull/2308))
- **agent-framework-core**: Langfuse observability captures ChatAgent system instructions ([#2316](https://github.com/microsoft/agent-framework/pull/2316))
- **agent-framework-core**: Incomplete URL substring sanitization fix ([#2274](https://github.com/microsoft/agent-framework/pull/2274))
- **observability**: Handle datetime serialization in tool results ([#2248](https://github.com/microsoft/agent-framework/pull/2248))
## [1.0.0b251117] - 2025-11-17
### Fixed
- **agent-framework-ag-ui**: Fix ag-ui state handling issues ([#2289](https://github.com/microsoft/agent-framework/pull/2289))
## [1.0.0b251114] - 2025-11-14
### Added
@@ -254,7 +290,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251114...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251120...HEAD
[1.0.0b251120]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251117...python-1.0.0b251120
[1.0.0b251117]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251114...python-1.0.0b251117
[1.0.0b251114]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251112.post1...python-1.0.0b251114
[1.0.0b251112.post1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251112...python-1.0.0b251112.post1
[1.0.0b251112]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251111...python-1.0.0b251112
+1 -1
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251114"
version = "1.0.0b251120"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+2 -2
View File
@@ -16,7 +16,7 @@ pip install agent-framework-ag-ui
from fastapi import FastAPI
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
# Create your agent
agent = ChatAgent(
@@ -41,7 +41,7 @@ add_agent_framework_fastapi_endpoint(app, agent, "/")
```python
import asyncio
from agent_framework import TextContent
from agent_framework_ag_ui import AGUIChatClient
from agent_framework.ag_ui import AGUIChatClient
async def main():
async with AGUIChatClient(endpoint="http://localhost:8000/") as client:
@@ -91,4 +91,4 @@ def add_agent_framework_fastapi_endpoint(
)
except Exception as e:
logger.error(f"Error in agent endpoint: {e}", exc_info=True)
return {"error": str(e)}
return {"error": "An internal error has occurred."}
@@ -86,6 +86,7 @@ class AgentFrameworkEventBridge:
self.pending_tool_calls: list[dict[str, Any]] = [] # Track tool calls for assistant message
self.tool_results: list[dict[str, Any]] = [] # Track tool results
self.tool_calls_ended: set[str] = set() # Track which tool calls have had ToolCallEndEvent emitted
self.accumulated_text_content: str = "" # Track accumulated text for final MessagesSnapshotEvent
async def from_agent_run_update(self, update: AgentRunResponseUpdate) -> list[BaseEvent]:
"""
@@ -99,18 +100,29 @@ class AgentFrameworkEventBridge:
"""
events: list[BaseEvent] = []
for content in update.contents:
logger.info(f"Processing AgentRunUpdate with {len(update.contents)} content items")
for idx, content in enumerate(update.contents):
logger.info(f" Content {idx}: type={type(content).__name__}")
if isinstance(content, TextContent):
logger.info(
f" TextContent found: text_length={len(content.text)}, text_preview='{content.text[:100]}'"
)
logger.info(
f" Flags: skip_text_content={self.skip_text_content}, should_stop_after_confirm={self.should_stop_after_confirm}"
)
# Skip text content if using structured outputs (it's just the JSON)
if self.skip_text_content:
logger.info(" SKIPPING TextContent: skip_text_content is True")
continue
# Skip text content if we're about to emit confirm_changes
# The summary should only appear after user confirms
if self.should_stop_after_confirm:
logger.debug("Skipping text content - waiting for confirm_changes response")
logger.info(" SKIPPING TextContent: waiting for confirm_changes response")
# Save the summary text to show after confirmation
self.suppressed_summary += content.text
logger.info(f" Suppressed summary now has {len(self.suppressed_summary)} chars")
continue
if not self.current_message_id:
@@ -119,14 +131,16 @@ class AgentFrameworkEventBridge:
message_id=self.current_message_id,
role="assistant",
)
logger.debug(f"Emitting TextMessageStartEvent with message_id={self.current_message_id}")
logger.info(f" EMITTING TextMessageStartEvent with message_id={self.current_message_id}")
events.append(start_event)
event = TextMessageContentEvent(
message_id=self.current_message_id,
delta=content.text,
)
logger.debug(f"Emitting TextMessageContentEvent with delta: {content.text}")
# Accumulate text content for final MessagesSnapshotEvent
self.accumulated_text_content += content.text
logger.info(f" EMITTING TextMessageContentEvent with delta: '{content.text}'")
events.append(event)
elif isinstance(content, FunctionCallContent):
@@ -427,7 +441,24 @@ class AgentFrameworkEventBridge:
# Emit MessagesSnapshotEvent with the complete conversation including tool calls and results
# This is required for CopilotKit's useCopilotAction to detect tool result
if self.pending_tool_calls and self.tool_results:
# HOWEVER: Skip this for predictive tools when require_confirmation=False, because
# the agent will generate a follow-up text message and we'll emit a complete snapshot at the end.
# Emitting here would create an incomplete snapshot that gets replaced, causing UI flicker.
should_emit_snapshot = self.pending_tool_calls and self.tool_results
# Check if this is a predictive tool that will have a follow-up message
is_predictive_without_confirmation = False
if should_emit_snapshot and self.current_tool_call_name and self.predict_state_config:
for state_key, config in self.predict_state_config.items():
if config["tool"] == self.current_tool_call_name and not self.require_confirmation:
is_predictive_without_confirmation = True
logger.info(
f"Skipping intermediate MessagesSnapshotEvent for predictive tool '{self.current_tool_call_name}' "
"- will emit complete snapshot after follow-up message"
)
break
if should_emit_snapshot and not is_predictive_without_confirmation:
# Import message adapter
from ._message_adapters import agent_framework_messages_to_agui
@@ -283,8 +283,62 @@ def extract_text_from_contents(contents: list[Any]) -> str:
return "".join(text_parts)
def agui_messages_to_snapshot_format(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Normalize AG-UI messages for MessagesSnapshotEvent.
Converts AG-UI input format (with 'input_text' type) to snapshot format (with 'text' type).
Args:
messages: List of AG-UI messages in input format
Returns:
List of normalized messages suitable for MessagesSnapshotEvent
"""
from ._utils import generate_event_id
result: list[dict[str, Any]] = []
for msg in messages:
normalized_msg = msg.copy()
# Ensure ID exists
if "id" not in normalized_msg:
normalized_msg["id"] = generate_event_id()
# Normalize content field
content = normalized_msg.get("content")
if isinstance(content, list):
# Convert content array format to simple string
text_parts = []
for item in content:
if isinstance(item, dict):
# Convert 'input_text' to 'text' type
if item.get("type") == "input_text":
text_parts.append(item.get("text", ""))
elif item.get("type") == "text":
text_parts.append(item.get("text", ""))
else:
# Other types - just extract text field if present
text_parts.append(item.get("text", ""))
normalized_msg["content"] = "".join(text_parts)
elif content is None:
normalized_msg["content"] = ""
# Normalize tool_call_id to toolCallId for tool messages
if normalized_msg.get("role") == "tool":
if "tool_call_id" in normalized_msg:
normalized_msg["toolCallId"] = normalized_msg["tool_call_id"]
del normalized_msg["tool_call_id"]
elif "toolCallId" not in normalized_msg:
normalized_msg["toolCallId"] = ""
result.append(normalized_msg)
return result
__all__ = [
"agui_messages_to_agent_framework",
"agent_framework_messages_to_agui",
"agui_messages_to_snapshot_format",
"extract_text_from_contents",
]
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any
from ag_ui.core import (
BaseEvent,
MessagesSnapshotEvent,
RunErrorEvent,
TextMessageContentEvent,
TextMessageEndEvent,
@@ -588,32 +589,37 @@ class DefaultOrchestrator(Orchestrator):
# We should NOT add to thread.on_new_messages() as that would cause duplication.
# Instead, we pass messages directly to the agent via messages_to_run.
# Inject current state as system message context if we have state
# Inject current state as system message context if we have state and this is a new user turn
messages_to_run: list[Any] = []
# Check if the last message is from the user (new turn) vs assistant/tool (mid-execution)
is_new_user_turn = False
if provider_messages:
last_msg = provider_messages[-1]
is_new_user_turn = last_msg.role.value == "user"
# Check if conversation has tool calls (indicates mid-execution)
conversation_has_tool_calls = False
logger.debug(f"Checking {len(provider_messages)} provider messages for tool calls")
for i, msg in enumerate(provider_messages):
logger.debug(
f" Message {i}: role={msg.role.value}, contents={len(msg.contents) if hasattr(msg, 'contents') and msg.contents else 0}"
)
for msg in provider_messages:
if msg.role.value == "assistant" and hasattr(msg, "contents") and msg.contents:
if any(isinstance(content, FunctionCallContent) for content in msg.contents):
conversation_has_tool_calls = True
break
if current_state and context.config.state_schema and not conversation_has_tool_calls:
# Only inject state context on new user turns AND when conversation doesn't have tool calls
# (tool calls indicate we're mid-execution, so state context was already injected)
if current_state and context.config.state_schema and is_new_user_turn and not conversation_has_tool_calls:
state_json = json.dumps(current_state, indent=2)
state_context_msg = ChatMessage(
role="system",
contents=[
TextContent(
text=f"""Current state of the application:
{state_json}
{state_json}
When modifying state, you MUST include ALL existing data plus your changes.
For example, if adding a new ingredient, include all existing ingredients PLUS the new one.
Never replace existing data - always append or merge."""
When modifying state, you MUST include ALL existing data plus your changes.
For example, if adding one new item to a list, include ALL existing items PLUS the one new item.
Never replace existing data - always preserve and append or merge."""
)
],
)
@@ -714,12 +720,19 @@ Never replace existing data - always append or merge."""
# Collect all updates to get the final structured output
all_updates: list[Any] = []
update_count = 0
async for update in context.agent.run_stream(messages_to_run, thread=thread, tools=tools_param):
update_count += 1
logger.info(f"[STREAM] Received update #{update_count} from agent")
all_updates.append(update)
events = await event_bridge.from_agent_run_update(update)
logger.info(f"[STREAM] Update #{update_count} produced {len(events)} events")
for event in events:
logger.info(f"[STREAM] Yielding event: {type(event).__name__}")
yield event
logger.info(f"[STREAM] Agent stream completed. Total updates: {update_count}")
# After agent completes, check if we should stop (waiting for user to confirm changes)
if event_bridge.should_stop_after_confirm:
logger.info("Stopping run after confirm_changes - waiting for user response")
@@ -793,9 +806,56 @@ Never replace existing data - always append or merge."""
yield TextMessageEndEvent(message_id=message_id)
logger.info(f"Emitted conversational message: {response_dict['message'][:100]}...")
logger.info(f"[FINALIZE] Checking for unclosed message. current_message_id={event_bridge.current_message_id}")
if event_bridge.current_message_id:
logger.info(f"[FINALIZE] Emitting TextMessageEndEvent for message_id={event_bridge.current_message_id}")
yield event_bridge.create_message_end_event(event_bridge.current_message_id)
# Emit MessagesSnapshotEvent to persist the final assistant text message
from ._message_adapters import agui_messages_to_snapshot_format
# Build the final assistant message with accumulated text content
assistant_text_message = {
"id": event_bridge.current_message_id,
"role": "assistant",
"content": event_bridge.accumulated_text_content,
}
# Convert input messages to snapshot format (normalize content structure)
# event_bridge.input_messages are already in AG-UI format, just need normalization
converted_input_messages = agui_messages_to_snapshot_format(event_bridge.input_messages)
# Build complete messages array
# Include: input messages + any pending tool calls/results + final text message
all_messages = converted_input_messages.copy()
# Add assistant message with tool calls if any
if event_bridge.pending_tool_calls:
tool_call_message = {
"id": generate_event_id(),
"role": "assistant",
"tool_calls": event_bridge.pending_tool_calls.copy(),
}
all_messages.append(tool_call_message)
# Add tool results if any
all_messages.extend(event_bridge.tool_results.copy())
# Add final text message
all_messages.append(assistant_text_message)
messages_snapshot = MessagesSnapshotEvent(
messages=all_messages, # type: ignore[arg-type]
)
logger.info(
f"[FINALIZE] Emitting MessagesSnapshotEvent with {len(all_messages)} messages "
f"(text content length: {len(event_bridge.accumulated_text_content)})"
)
yield messages_snapshot
else:
logger.info("[FINALIZE] No current_message_id - skipping TextMessageEndEvent")
logger.info("[FINALIZE] Emitting RUN_FINISHED event")
yield event_bridge.create_run_finished_event()
logger.info(f"Completed agent run for thread_id={context.thread_id}, run_id={context.run_id}")
@@ -18,7 +18,7 @@ All example agents are factory functions that accept any `ChatClientProtocol`-co
from fastapi import FastAPI
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.openai import OpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework_ag_ui_examples.agents import simple_agent, weather_agent
app = FastAPI()
@@ -40,7 +40,7 @@ add_agent_framework_fastapi_endpoint(app, weather_agent(openai_client), "/weathe
from fastapi import FastAPI
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
# Create your agent
agent = ChatAgent(
@@ -136,7 +136,7 @@ The server exposes endpoints at:
```python
from fastapi import FastAPI
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework_ag_ui_examples.agents import (
simple_agent,
weather_agent,
@@ -188,8 +188,8 @@ You can create your own agent factories following the same pattern as the exampl
```python
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent
from agent_framework import ChatClientProtocol
from agent_framework.ag_ui import AgentFrameworkAgent
@ai_function
def my_tool(param: str) -> str:
@@ -2,10 +2,8 @@
"""Example agent demonstrating predictive state updates with document writing."""
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent, DocumentWriterConfirmationStrategy
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
from agent_framework.ag_ui import AgentFrameworkAgent, DocumentWriterConfirmationStrategy
@ai_function
@@ -4,8 +4,7 @@
from enum import Enum
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
from pydantic import BaseModel, Field
@@ -4,12 +4,10 @@
from enum import Enum
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
from agent_framework.ag_ui import AgentFrameworkAgent, RecipeConfirmationStrategy
from pydantic import BaseModel, Field
from agent_framework_ag_ui import AgentFrameworkAgent, RecipeConfirmationStrategy
class SkillLevel(str, Enum):
"""The skill level required for the recipe."""
@@ -130,4 +128,5 @@ def recipe_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"recipe": {"tool": "update_recipe", "tool_argument": "recipe"},
},
confirmation_strategy=RecipeConfirmationStrategy(),
require_confirmation=False,
)
@@ -4,10 +4,8 @@
import asyncio
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
from agent_framework.ag_ui import AgentFrameworkAgent
@ai_function
@@ -2,8 +2,7 @@
"""Simple agentic chat example (Feature 1: Agentic Chat)."""
from agent_framework import ChatAgent
from agent_framework._clients import ChatClientProtocol
from agent_framework import ChatAgent, ChatClientProtocol
def simple_agent(chat_client: ChatClientProtocol) -> ChatAgent:
@@ -2,10 +2,8 @@
"""Example agent demonstrating human-in-the-loop with function approvals."""
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent, TaskPlannerConfirmationStrategy
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
from agent_framework.ag_ui import AgentFrameworkAgent, TaskPlannerConfirmationStrategy
@ai_function(approval_mode="always_require")
@@ -18,12 +18,10 @@ from ag_ui.core import (
TextMessageStartEvent,
ToolCallStartEvent,
)
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
from agent_framework.ag_ui import AgentFrameworkAgent
from pydantic import BaseModel, Field
from agent_framework_ag_ui import AgentFrameworkAgent
class StepStatus(str, Enum):
"""Status of a task step."""
@@ -4,10 +4,8 @@
from typing import Any
from agent_framework import AIFunction, ChatAgent
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent
from agent_framework import AIFunction, ChatAgent, ChatClientProtocol
from agent_framework.ag_ui import AgentFrameworkAgent
# Declaration-only tools (func=None) - actual rendering happens on the client side
generate_haiku = AIFunction[Any, str](
@@ -4,8 +4,7 @@
from typing import Any
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
@ai_function
@@ -2,11 +2,10 @@
"""Backend tool rendering endpoint."""
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.azure import AzureOpenAIChatClient
from fastapi import FastAPI
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from ...agents.weather_agent import weather_agent
@@ -6,12 +6,11 @@ import logging
import os
import uvicorn
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.azure import AzureOpenAIChatClient
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from ..agents.document_writer_agent import document_writer_agent
from ..agents.human_in_the_loop_agent import human_in_the_loop_agent
from ..agents.recipe_agent import recipe_agent

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