Compare commits

...
Author SHA1 Message Date
SergeyMenshykhandGitHub 394e9c1692 .NET: Improve skill name validation: reject consecutive hyphens and enforce directory name match (#4526)
* improve skill validation

* address pr review comments
2026-03-06 17:29:53 +00:00
CopilotGitHubCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>rogerbarreto
7e98b0cd29 .NET: Update HostedAgents samples to Azure.AI.AgentServer.AgentFramework 1.0.0-beta.9 and MEAI 10.3.0 (#4477)
* Initial plan

* Update HostedAgents samples to Azure.AI.AgentServer.AgentFramework 1.0.0-beta.9 and MEAI 10.3.0

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

* Fix HostedAgents samples for Microsoft.Agents.AI 1.0.0-rc2 API changes

- Rename CreateAIAgent -> AsAIAgent (AgentThreadAndHITL, AgentWithHostedMCP, AgentWithTextSearchRag)
- Rename AsAgent -> AsAIAgent (AgentsInWorkflows)
- Replace AIContextProviderFactory with AIContextProviders and simplified TextSearchProvider ctor (AgentWithTextSearchRag)
- Update Microsoft.Agents.AI.OpenAI to 1.0.0-rc2 (AgentThreadAndHITL, AgentWithTextSearchRag, AgentWithTools)
- Update Microsoft.Agents.AI.Workflows to 1.0.0-rc2 (AgentsInWorkflows)
- Add Microsoft.Agents.AI 1.0.0-rc2 reference (AgentWithHostedMCP)

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

* Update HostedAgents samples for beta.9 API changes and add missing projects to slnx

- Use DefaultAzureCredential consistently across all samples
- Add AgentThreadAndHITL, AgentWithLocalTools, AgentWithTools to slnx
- Apply dotnet format

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

* Remove unnecessary Microsoft.Agents.AI.* package references (transitive from AgentFramework)

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

* Add DefaultAzureCredential production warning comments to all HostedAgents samples

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

* Update HostedAgents READMEs to reflect DefaultAzureCredential usage

Replace AzureCliCredential references with DefaultAzureCredential in all
HostedAgents README files to match the actual sample code.

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

* Replace Microsoft.Extensions.AI.OpenAI with Microsoft.Agents.AI.OpenAI and remove AsIChatClient()

Swap package references from Microsoft.Extensions.AI.OpenAI to
Microsoft.Agents.AI.OpenAI across all 6 HostedAgents samples. This enables
using the AsAIAgent() extension directly on ChatClient/ResponsesClient
(from OpenAI.Chat/OpenAI.Responses namespaces), removing the intermediate
AsIChatClient() call in 3 samples where it was unnecessary.

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

* Use explicit types and AsAIAgent() extensions across all HostedAgents samples

Replace var with explicit types for clarity in all 6 samples. Replace
new ChatClientAgent() constructor calls with chatClient.AsAIAgent()
extension method in AgentWithLocalTools and AgentsInWorkflows.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 12:15:10 +00:00
f7e4143c61 .NET: Fix filter combine logic for ChatHistoryMemoryProvider (#4501)
* Fix filter combine logic for ChatHistoryMemoryProvider

* Replace var with explicit types in filter building code and test

Address PR review nit: use explicit types instead of var for better
readability in the filter-building logic and the new combined filter
compilation test.

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

* Fix style issues

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 11:59:50 +00:00
westeyandGitHub d8d6ac1c59 Add ServiceLifetime support for Hosting DI registration (#4476) 2026-03-06 09:39:33 +00:00
4bd5469798 Python: Improve ag-ui tests and coverage (#4442)
* Improve ag-ui tests and coverage

* fix tests paths

* Fixes

* Improve AG-UI test robustness and correctness

- Map toolName → tool_call_name in SSE helpers for TOOL_CALL_START events
- Fail loudly on malformed SSE JSON in parse_sse_response() instead of silently dropping
- Detect duplicate TOOL_CALL_START/TOOL_CALL_END in assert_tool_calls_balanced()
- Remove fragile source line reference from test docstring
- Add found guard in test_client_tool_sets_additional_properties to prevent vacuous pass

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 07:06:56 +00:00
CopilotGitHubCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>markwallace-microsofteavanvalkenburgTaoChenOSUBen Thomas
1ac68f65bf Python: Fix RedisContextProvider for redisvl 0.14.0 by using AggregateHybridQuery (#3954)
* Initial plan

* Fix: Replace alpha with linear_alpha in HybridQuery for redisvl 0.14.0 compatibility

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

* Address code review: Improve test readability and add explanatory comment

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

* Add CHANGELOG entry for redisvl 0.14.0 compatibility fix

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

* Use AggregateHybridQuery instead of HybridQuery for backward compatibility

Replace HybridQuery with AggregateHybridQuery to preserve existing functionality that works with older Redis versions. The new HybridQuery in redisvl 0.14.0 requires Redis 8.4.0+ and uses a different API, while AggregateHybridQuery maintains compatibility with the original implementation.

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Fix test to use linear_alpha parameter matching _redis_search implementation

The test was passing alpha as a keyword argument to _redis_search(), but the
method uses linear_alpha to match the redisvl 0.14.0 AggregateHybridQuery API.

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

* Fix pyright error: use alpha parameter matching AggregateHybridQuery API

AggregateHybridQuery expects 'alpha', not 'linear_alpha'. Updated the
_redis_search method parameter and the test accordingly.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 01:34:35 +00:00
8664d19285 Python: Propagated MCP isError flag through function middleware pipeline (#4511)
* Propagated MCP isError flag through function middleware pipeline

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

* Small update

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

* Fix CI

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-05 23:16:19 +00:00
ce7b5b17c1 Python: Fix as_agent() not defaulting name/description from client properties (#4484)
* Fix as_agent() not defaulting name/description from client properties

AzureAIClient.as_agent() and AzureAIAgentClient.as_agent() now fall back
to self.agent_name and self.agent_description when name/description are
not explicitly passed. This ensures Agent.name is populated for
telemetry spans without requiring callers to repeat the name.

Fixes #4471

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

* Address review: use is None checks instead of truthiness

Switch from name or self.agent_name to explicit is None checks so
that callers can intentionally pass empty strings without them being
replaced by client defaults. Added edge-case tests for empty strings.

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

* Update docstrings to document name/description defaulting behavior

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-05 20:12:11 +00:00
SergeyMenshykhandGitHub 8bf4235f4e Python: Forward runtime kwargs to skill resource functions (#4417)
* support code skills

* address pr review comments

* address package and syntax checks

* address pr review comments

* address pr review comment

* address failed check

* rename agentskill and agetnskillprovider

* move agent skills related assets to _skills.py

* address pr review comments

* address review comments

* support kwargs

* address pr review feedback
2026-03-05 18:01:25 +00:00
55ddd841b7 Python: Fix Python pyright package scoping and typing remediation (#4426)
* Fix Python pyright package scoping and typing remediation

Implements issue #4407 by removing the root pyright include, adding package-level pyright includes, and resolving pyright/mypy typing issues across Python packages. Also cleans unnecessary casts and applies line-level, rule-specific ignores where external libraries are too dynamic.

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

* Reduce pyright cost in handoff cloning

Simplify cloned_options construction in HandoffAgentExecutor to avoid expensive TypedDict narrowing/inference in _handoff.py, which was causing pyright to spend a long time in orchestrations.

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

* fix types

* Fix lint and type-check regressions

Resolve current Python package check failures across lint, pyright, and mypy after recent code changes, including purview/declarative pyright issues and multiple ruff simplification findings.

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

* fixed hooks

* Stabilize package tests and test tasks

Resolve cross-package non-integration test failures, simplify streaming type flow, harden locale/culture handling, and standardize package test poe tasks to exclude integration tests where applicable.

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

* lots of small fixes

* Fix current Python test regressions

Address current failing unit tests in azure-ai, bedrock, and azure-cosmos while keeping Bedrock parsing logic inline (no new static helper methods).

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

* small fixes

* small fixes

* removed pydantic from json

* final updates

* fix core

* fix tests

* fix obser

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-05 15:32:24 +00:00
westeyandGitHub 4a043c6c66 .NET: Switch auth sample to use Singletons (#4454)
* Switch auth sample to use Singletons

* Address PR comments

* Add comment to warn users to choose the appropriate lifetime for their service
2026-03-05 14:42:46 +00:00
+6
westeyGitHuballiscodeCopilotcrickmancopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>CopilotRoger BarretoEduard van ValkenburgRishabh ChawlaPeter IbekweDmytro StrukBen ThomasEvan Mattson
3fb90a501a .NET: CI Build time end to end improvement (#4208)
* .NET: Upgrade to XUnit 3 and Microsoft Testing Platform (#4176)

* Fix copilot studio integration tests failure (#4209)

* Fix anthropic integration tests and skip reason (#4211)

* Remove accidental add of code coverage for integration tests (#4219)

* Add solution filtered parallel test run (#4226)

* Fix build paths (#4228)

* Fix coverage settings path and trait filter (#4229)

* Add project name filter to solution (#4231)

* Increase Integration Test Parallelism (#4241)

* Increase integration tests threads to 4x (#4242)

* Separate build and test into parallel jobs (#4243)

* Filter src by framework for tests build (#4244)

* Separate build and test into parallel jobs

* Filter source projects by framework for tests build

* Pre-build samples via tests to avoid timeouts (#4245)

* Separate build from run for console sample validation (#4251)

* Address PR comments (#4255)

* Merge and move scripts (#4308)

* .NET: Add Microsoft Fabric sample #3674 (#4230)

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Phase 2: Embedding clients for Ollama, Bedrock, and Azure AI Inference (#4207)

* Phase 2: Embedding clients for Ollama, Bedrock, and Azure AI Inference

Add embedding client implementations to existing provider packages:

- OllamaEmbeddingClient: Text embeddings via Ollama's embed API
- BedrockEmbeddingClient: Text embeddings via Amazon Titan on Bedrock
- AzureAIInferenceEmbeddingClient: Text and image embeddings via Azure AI
  Inference, supporting Content | str input with separate model IDs for
  text (AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID) and image
  (AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID) endpoints

Additional changes:
- Rename EmbeddingCoT -> EmbeddingT, EmbeddingOptionsCoT -> EmbeddingOptionsT
- Add otel_provider_name passthrough to all embedding clients
- Register integration pytest marker in all packages
- Add lazy-loading namespace exports for Ollama and Bedrock embeddings
- Add image embedding sample using Cohere-embed-v3-english
- Add azure-ai-inference dependency to azure-ai package

Part of #1188

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

* Fix mypy duplicate name and ruff lint issues

- Rename second 'vector' variable to 'img_vector' in image embedding loop
- Combine nested with statements in tests
- Remove unused result assignments in tests

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

* updates from feedback

* Fix CI failures in embedding usage handling

- Fix Azure AI embedding mypy issues by normalizing vectors to list[float],
  safely accumulating optional usage token fields, and filtering None entries
  before constructing GeneratedEmbeddings
- Avoid Bandit false positive by initializing usage details as an empty dict
- Update OpenAI embedding tests to assert canonical usage keys
  (input_token_count/total_token_count)

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

---------

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

* [Purview] Mark responses as responses and fix epoch bug for python long overflow (#4225)

* .NET: Support InvokeMcpTool for declarative workflows (#4204)

* Initial implementation of InvokeMcpTool in declarative workflow

* Cleaned up sample implementation

* Updated sample comments.

* Added missing executor routing attribute

* Fix PR comments.

* Updated based on PR comments.

* Updated based on PR comments.

* Removed unnecessary using statement.

* Update Python package versions to rc2 (#4258)

- Bump core and azure-ai to 1.0.0rc2
- Bump preview packages to 1.0.0b260225
- Update dependencies to >=1.0.0rc2
- Add CHANGELOG entries for changes since rc1
- Update uv.lock

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

* .NET: Fixing issue where OpenTelemetry span is never exported in .NET in-process workflow execution (#4196)

* 1. Add reproduction test for issue #4155: workflow.run Activity never stopped in streaming OffThread path

The WorkflowRunActivity_IsStopped_Streaming_OffThread test demonstrates that
the workflow.run OpenTelemetry Activity created in StreamingRunEventStream.RunLoopAsync
is started but never stopped when using the OffThread/Default streaming execution.
The background run loop keeps running after event consumption completes, so the
using Activity? declaration never disposes until explicit StopAsync() is called.

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

2. Fix workflow.run Activity never stopped in streaming OffThread execution (#4155)

The workflow.run OpenTelemetry Activity in StreamingRunEventStream.RunLoopAsync
was scoped to the method lifetime via 'using'. Since the run loop only exits on
cancellation, the Activity was never stopped/exported until explicit disposal.

Fix: Remove 'using' and explicitly dispose the Activity when the workflow reaches
Idle status (all supersteps complete). A safety-net disposal in the finally block
handles cancellation and error paths.

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

* Add root-level workflow.session activity spanning run loop lifetime\n\nImplements two-level telemetry hierarchy per PR feedback from lokitoth:\n- workflow.session: spans the entire run loop / stream lifetime\n- workflow_invoke: per input-to-halt cycle, nested within the session\n\nThis ensures the session activity stays open across multiple turns,\nwhile individual run activities are created and disposed per cycle.\n\nAlso fixes linkedSource CancellationTokenSource disposal leak in\nStreamingRunEventStream (added using declaration)."

* Address Copilot review: fix Activity/CTS disposal, rename activity, add error tag\n\n1. LockstepRunEventStream: Remove 'using' from Activity in async iterator\n   and manually dispose in finally block (fixes #4155 pattern). Also dispose\n   linkedSource CTS in finally to prevent leak.\n2. Tags.cs: Add ErrorMessage (\"error.message\") tag for runtime errors,\n   distinct from BuildErrorMessage (\"build.error.message\").\n3. ActivityNames: Rename WorkflowRun from \"workflow_invoke\" to \"workflow.run\"\n   for cross-language consistency.\n4. WorkflowTelemetryContext: Fix XML doc to say \"outer/parent span\" instead\n   of \"root-level span\".\n5. ObservabilityTests: Assert WorkflowSession absence when DisableWorkflowRun\n   is true.\n6. WorkflowRunActivityStopTests: Fix streaming test race by disposing\n   StreamingRun before asserting activities are stopped.\n7. StreamingRunEventStream/LockstepRunEventStream: Use Tags.ErrorMessage\n   instead of Tags.BuildErrorMessage for runtime error events."

* Review fixes: revert workflow_invoke rename, use 'using' for linkedSource, move SessionStarted earlier\n\n- Revert ActivityNames.WorkflowRun back to \"workflow_invoke\" (OTEL semantic convention contract)\n- Use 'using' declaration for linkedSource CTS in LockstepRunEventStream (no timing sensitivity)\n- Move SessionStarted event before WaitForInputAsync in StreamingRunEventStream to match Lockstep behavior"

* Improve naming and comments in WorkflowRunActivityStopTests"

* Prevent session Activity.Current leak in lockstep mode, add nesting test

Save and restore Activity.Current in LockstepRunEventStream.Start() so the
session activity doesn't leak into caller code via AsyncLocal. Re-establish
Activity.Current = sessionActivity before creating the run activity in
TakeEventStreamAsync to preserve parent-child nesting.

Add test verifying app activities after RunAsync are not parented under the
session, and that the workflow_invoke activity nests under the session."

* Fix stale XML doc: WorkflowRun -> WorkflowInvoke in ObservabilityTests

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python / .NET Samples - Restructure and Improve Samples (Feature Branc… (#4092)

* Python: .NET Samples - Restructure and Improve Samples (Feature Branch) (#4091)

* Moved by agent (#4094)

* Fix readme links

* .NET Samples - Create `04-hosting` learning path step (#4098)

* Agent move

* Agent reorderd

* Remove A2A section from README 

Removed A2A section from the Getting Started README.

* Agent fixed links

* Fix broken sample links in durable-agents README (#4101)

* Initial plan

* Fix broken internal links in documentation

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Revert template link changes; keep only durable-agents README fix

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

---------

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

* .NET Samples - Create `03-workflows` learning path step (#4102)

* Fix solution project path

* Python: Fix broken markdown links to repo resources (outside /docs) (#4105)

* Initial plan

* Fix broken markdown links to repo resources

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Update README to rename .NET Workflows Samples section

---------

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

* .NET Samples - Create `02-agents` learning path step (#4107)

* .NET: Fix broken relative link in GroupChatToolApproval README (#4108)

* Initial plan

* Fix broken link in GroupChatToolApproval README

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

---------

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

* Update labeler configuration for workflow samples

* .NET - Reorder Agents samples to start from Step01 instead of Step04 (#4110)

* Fix solution

* Resolve new sample paths

* Move new AgentSkills and AgentWithMemory_Step04 samples

* Fix link

* Fix readme path

* fix: update stale dotnet/samples/Durable path reference in AGENTS.md

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Moved new sample

* Update solution

* Resolve merge (new sample)

* Sync to new sample - FoundryAgents_Step21_BingCustomSearch

* Updated README

* .NET Samples - Configuration Naming Update (#4149)

* .NET: Restore AzureFunctions index parity with ConsoleApps under DurableAgents samples (#4221)

* Clean-up `05_host_your_agent`

* Config setting consistency

* Refine samples

* AGENTS.md

* Move new samples

* Re-order samples

* Move new project and fixup solution

* Fixup model config

* Fix up new UT project

---------

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

* Python: Fix Bedrock embedding test stub missing meta attribute (#4287)

* Fix Bedrock embedding test stub missing meta attribute

* Increase test coverage so gate passes

* Python: (ag-ui): fix approval payloads being re-processed on subsequent conversation turns (#4232)

* Fix ag-ui tool call issue

* Safe json fix

* Python: Update workflow orchestration samples to use AzureOpenAIResponsesClient (#4285)

* Update workflow orchestration samples to use AzureOpenAIResponsesClient

* Fix broken link

* Move scripts to scripts folder

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rishabh Chawla <rishabhchawla1995@gmail.com>
Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* Fix encoding (#4309)

* Disable Parallelization for WorkflowRunActivityStopTests (#4313)

* Revert parallel disable (#4324)

* .NET: Disable flakey Workflow Observability tests (#4416)

* Disable flakey OffThread test

* Disable additional OffThread test

* Disable a further test

* Disable all observability tests

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rishabh Chawla <rishabhchawla1995@gmail.com>
Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-03-05 14:14:33 +00:00
56bba795cb .NET: Add foundry extension samples for python and dotnet (#4359)
* Add foundry extension samples for python and dotnet

* Align foundry extension samples with existing hosted agent patterns

- Fix Python multiagent indentation bug (from_agent_framework ran in both modes)
- Remove hardcoded personal endpoint from appsettings.Development.json
- Rename .NET folders/projects to PascalCase (FoundryMultiAgent, FoundrySingleAgent)
- Upgrade .NET multiagent from net9.0 to net10.0
- Add ManagePackageVersionsCentrally=false and analyzer blocks to .csproj files
- Replace wildcard package versions with fixed versions
- Use alpine Docker images and standard build pattern
- Align agent.yaml structure (template nesting, displayName, resources, authors)
- Convert .NET multiagent from namespace/class to top-level statements
- Add run-requests.http for multiagent sample
- Fix Python requirements.txt (remove dev deps, add agent-framework)
- Add proper copyright headers

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

* Align foundry samples: fix builds, upgrade AgentServer to beta.8

- Fix TargetFrameworks (plural) to override inherited net472 from Directory.Build.props
- Upgrade Azure.AI.AgentServer.AgentFramework to 1.0.0-beta.8 (latest)
- Bump OpenTelemetry packages to 1.12.0 (required by beta.8)
- Fix Roslynator/format errors (imports ordering, BOM, sealed record, target-typed new)
- Verified with docker dotnet format (matching CI pipeline)

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

* Refactor hosted samples to use AIProjectClient.CreateAIAgentAsync

Replace PersistentAgentsClient and manual AzureOpenAIClient setup with
AIProjectClient.CreateAIAgentAsync() from Microsoft.Agents.AI.AzureAI.

- FoundryMultiAgent: Remove Azure.AI.Agents.Persistent, use CreateAIAgentAsync
  for Writer and Reviewer agents with cleanup in finally block
- FoundrySingleAgent: Remove manual GetConnection/AzureOpenAIClient chain,
  use CreateAIAgentAsync with hotel search tool
- Update csproj: add Microsoft.Agents.AI.AzureAI, remove unused packages

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

* Update READMEs to reflect AIProjectClient.CreateAIAgentAsync usage

- Reference Microsoft.Agents.AI.AzureAI and Microsoft.Agents.AI.Workflows packages
- Add Azure AI Developer role requirement for agents/write data action
- Replace PersistentAgentsClient references

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

* Add HostedAgents READMEs and Foundry samples to solution

- Create dotnet/samples/05-end-to-end/HostedAgents/README.md with sample index
- Create python/samples/05-end-to-end/hosted_agents/README.md with sample index
- Add FoundryMultiAgent and FoundrySingleAgent to agent-framework-dotnet.slnx

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

* Fix Python linting: reorder imports before load_dotenv, remove trailing whitespace

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

* Update uv.lock to match latest package versions

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

* Fix trailing whitespace in foundry_single_agent agent.yaml

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

* Exclude dotnet.microsoft.com from link checker

This domain intermittently times out in CI, causing flaky markdown
link check failures unrelated to PR changes.

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

* Align env vars to AZURE_AI_PROJECT_ENDPOINT and default model to gpt-4o-mini

Addresses PR review feedback:
- Rename PROJECT_ENDPOINT to AZURE_AI_PROJECT_ENDPOINT across all
  Foundry samples (dotnet + python) to match existing samples
- Change default model from gpt-4.1-mini to gpt-4o-mini consistently

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

* Skip flaky test CreatesWorkflowEndToEndActivities_WithCorrectName_DefaultAsync

Tracked in #4398

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

* Remove Python foundry samples from PR scope

Python hosted agent samples need further alignment with the azure-ai
package conventions. Removing from this PR to ship .NET samples first.

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

* Narrow linkspector exclusion to dotnet.microsoft.com/download only

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

---------

Co-authored-by: Leo Yao <leoyao@Leos-MacBook-Pro.local>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-05 11:43:24 +00:00
westeyandGitHub 6dc65dbaa1 .NET: Increase credential timeout for Integration Tests (#4472)
* Increase credential timeout for Integration Tests

* Fix format error.

* Update further tests

* Fix comment

* Rename credentials file and class.

* Fix broken reference.
2026-03-05 10:32:45 +00:00
d02051dbb6 Python: Add propagate_session to as_tool() for session sharing in agent-as-tool scenarios (#4439)
* Python: Add propagate_session parameter to as_tool() for session sharing

Add opt-in session propagation in agent-as-tool scenarios. When
propagate_session=True, the parent agent's AgentSession is forwarded
to the sub-agent's run() call, allowing both agents to share session
state (history, metadata, session_id).

- Add propagate_session parameter to BaseAgent.as_tool() (default False)
- Include session in additional_function_arguments so it flows to tools
- Add 3 tests for propagation on/off and shared state verification
- Add sample showing session propagation with observability middleware

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

* Clarify propagate_session docstring per review feedback

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 23:14:17 +00:00
23644ac6a7 .NET: bug fix for duplicate output on GitHubCopilotAgent (#3981)
* bug fix for duplicate output on GitHubCopilotAgent

* Add Test code for bug fix of duplicate output on GitHubCopilotAgenttT

* update Test code for bug fix of duplicate output on GitHubCopilotAgenttT

* update Test for duplicate output of GitHubCopilotAgent

---------

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2026-03-04 21:40:34 +00:00
Dmytro StrukandGitHub fd981da0f8 Fixed CA1873 warning (#4479) 2026-03-04 13:24:53 -08:00
Dmytro StrukandGitHub b2ad1c3424 Update package versions (#4468) 2026-03-04 19:11:30 +00:00
SergeyMenshykhandGitHub afdd1e539d .NET: Discover skill resources from directory instead of markdown links (#4401)
* discover resources in skills folder

* address pr review comments

* change type of AllowedResourceExtensions

* address pr review comment
2026-03-04 18:37:11 +00:00
SergeyMenshykhandGitHub 4dad26fcae Python: [BREAKING] Support code-defined agent skills (#4387)
* support code skills

* address pr review comments

* address package and syntax checks

* address pr review comments

* address pr review comment

* address failed check

* rename agentskill and agetnskillprovider

* move agent skills related assets to _skills.py

* address pr review comments

* address review comments
2026-03-04 18:36:02 +00:00
Dineshsuriya DandGitHub 5fb0cc106a Python: feat(claude): add plugins, setting_sources, thinking, and effort options to ClaudeAgentOptions (#4425)
* feat(claude): add plugins, setting_sources, thinking, and effort options

Add four Claude Agent SDK options to ClaudeAgentOptions that are clean
passthroughs with no abstraction conflicts:

- plugins: load Claude Code plugins programmatically via SdkPluginConfig
- setting_sources: control which .claude settings files are loaded
- thinking: modern extended thinking config (adaptive/enabled/disabled)
- effort: control thinking depth (low/medium/high/max)

* feat(claude): remove max_thinking_tokens, add plugins/setting_sources/thinking/effort

Remove the deprecated max_thinking_tokens field from ClaudeAgentOptions
in favor of the new thinking field (ThinkingConfig).

Add four Claude Agent SDK options as clean passthroughs:
- plugins: load Claude Code plugins via SdkPluginConfig
- setting_sources: control which .claude settings files are loaded
- thinking: extended thinking config (adaptive/enabled/disabled)
- effort: thinking depth control (low/medium/high/max)
2026-03-04 17:05:15 +00:00
Dmytro StrukandGitHub 965a1ec103 Updated package versions (#4470) 2026-03-04 16:49:48 +00:00
e8a7ffbc14 .NET: Skip flacky UT + (Attempt) Merge Gatekeeper fix (#4456)
* Skip flacky UT

* Ignore org-level GitHub App checks in merge-gatekeeper

Add Cleanup artifacts, Agent, Prepare, and Upload results to the
ignored list. These are check runs created by an org-level GitHub App
(MSDO), not by any workflow in this repo, and their transient failures
should not block merges.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 14:39:54 +00:00
e7961571a8 .NET: Update Azure.AI.Projects 2.0.0-beta.1 (#4270)
* Update Microsoft.Agents.AI.AzureAI for Azure.AI.Projects SDK 2.0.0

- Bump Azure.AI.Projects to 2.0.0-alpha.20260213.1
- Bump Azure.AI.Projects.OpenAI to 2.0.0-alpha.20260213.1
- Bump System.ClientModel to 1.9.0 (transitive dependency)
- Switch both GetAgent and CreateAgentVersion to protocol methods
  with MEAI user-agent policy injection via RequestOptions
- Migrate 29 CREATE-path tests from FakeAgentClient to HttpHandlerAssert
  pattern for real HTTP pipeline testing
- Fix StructuredOutputDefinition constructor (BinaryData -> IDictionary)
- Fix responses endpoint path (openai/responses -> /responses)
- Add local-packages NuGet source for pre-release nupkgs

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

* Update Azure.AI.Projects to 2.0.0-beta.1 from NuGet.org

- Update Azure.AI.Projects and Azure.AI.Projects.OpenAI to 2.0.0-beta.1
- Remove local-packages NuGet source (packages now on nuget.org)
- Fix MemorySearchTool -> MemorySearchPreviewTool rename
- Fix RedTeams.CreateAsync ambiguous call
- Fix CreateAgentVersion/Async signature change (BinaryData -> string)
- Suppress AAIP001 experimental warning for WorkflowAgentDefinition

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

* Move s_modelWriterOptionsWire field before methods that use it

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

* Fix flaky test: prevent spurious workflow_invoke Activity on timeout wake-up

The StreamingRunEventStream run loop uses a 1-second timeout on
WaitForInputAsync. When the timeout fires before the consumer calls
StopAsync, the loop would create a spurious workflow_invoke Activity
even though no actual input was provided. This caused the
WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync test
to intermittently fail (expecting 2 activities but finding 3).

Fix: guard the loop body with a HasUnprocessedMessages check. On
timeout wake-ups with no work, the loop waits again without creating
an activity or changing the run status.

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

* Fix epoch race condition causing unit tests to hang on net10.0 and net472

The HasUnprocessedMessages guard (previous commit) correctly prevents
spurious workflow_invoke Activity creation on timeout wake-ups, but
exposed a latent race in the epoch-based signal filtering.

The race: when the run loop processes messages quickly and calls
Interlocked.Increment(ref _completionEpoch) before the consumer calls
TakeEventStreamAsync, the consumer reads the already-incremented epoch
and sets myEpoch = epoch + 1. This causes the consumer to skip the
valid InternalHaltSignal (its epoch < myEpoch) and block forever
waiting for a signal that will never arrive (since the guard prevents
spurious signal generation).

Fix: read _completionEpoch without +1. The +1 was originally needed to
filter stale signals from timeout-driven spurious loop iterations, but
those no longer exist thanks to the HasUnprocessedMessages guard.

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

* Revert "Fix epoch race condition causing unit tests to hang on net10.0 and net472"

This reverts commit 6ce7f01be8.

* Revert "Fix flaky test: prevent spurious workflow_invoke Activity on timeout wake-up"

This reverts commit 98963e17f2.

* Skip hanging multi-turn declarative integration tests

The ValidateMultiTurnAsync tests (ConfirmInput.yaml, RequestExternalInput.yaml)
hang indefinitely in CI, blocking the merge queue. The hang is SDK-independent
(reproduces with both Azure.AI.Projects 1.2.0-beta.5 and 2.0.0-beta.1) and
is a pre-existing issue in the declarative workflow multi-turn test logic.

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

* Remove unused using directive in IntegrationTest.cs

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

* Restore Azure.AI.Projects 2.0.0-beta.1 version bump

The merge from main accidentally reverted the package versions back to
1.2.0-beta.5. This is the primary change of this PR.

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

* Address merge conflict

* Skip flaky WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync test

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

* Skip CheckSystem test cases temporarily

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 11:36:39 +00:00
f788fdc72b Disable OpenAIAssistant structured output integration tests (#4451)
Skip all three structured output run tests in
OpenAIAssistantStructuredOutputRunTests as they fail intermittently
on the build agent/CI, matching the pattern already used in
AzureAIAgentsPersistentStructuredOutputRunTests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 11:21:05 +00:00
4dc20c6be4 Python: Fix PowerFx eval crash on non-English system locales by setting CurrentUICulture to en-US (#4408)
* Fix #4321: Set CurrentUICulture to en-US in PowerFx eval()

On non-English systems, CultureInfo.CurrentUICulture causes PowerFx to
emit localized error messages. The existing ValueError guard only matches
English strings ("isn't recognized", "Name isn't valid"), so undefined
variable errors crash instead of returning None gracefully.

Fix: save and restore CurrentUICulture alongside CurrentCulture before
calling engine.eval(), ensuring error messages are always in English.

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

* Reuse single CultureInfo instance to avoid redundant allocations

Cache CultureInfo("en-US") in a local variable instead of instantiating
it twice per eval() call, as suggested in PR review.

Fixes #4321

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

* Add assertion for CurrentUICulture restoration after eval

Assert that the production code's finally-block correctly restores
CurrentUICulture to it-IT after eval returns, covering future
regressions where the culture could leak.

The CultureInfo caching suggestion (comment #2) was already
implemented in the production code.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 05:46:16 +00:00
e3ea71ec2e fix(anthropic): set role='assistant' on message_start streaming update (#4329)
Co-authored-by: Leela Karthik U <wqtk@novonordisk.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
2026-03-04 00:23:43 +00:00
b0ac3939c1 Python: Fix MCP tools duplicated on second turn when runtime tools are present (#4432)
* Fix MCP tools duplicated on second turn when runtime tools are present

When AG-UI's collect_server_tools pre-expands MCP functions on turn 2
(after the MCP server is connected), _prepare_run_context unconditionally
appends them again from self.mcp_tools, duplicating every MCP tool.

Skip MCP functions whose names already exist in the final tool list,
following the same name-based dedup pattern used in _merge_options.

Fixes #4381

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

* mypy fix

* Remove issue-specific references from test docstring

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 00:21:01 +00:00
Dmytro StrukandGitHub b5edb529b7 Python: Upgraded azure-ai-projects to 2.0.0b4 (#4438)
* Upgraded azure-ai-projects to 2.0.0b4

* Fixed tests
2026-03-04 00:11:41 +00:00
Dmytro StrukandGitHub 5ba1c6f0cc .NET: Updated Copilot SDK to the latest version (#4406)
* Updated Copilot SDK to the latest version

* Added retry
2026-03-03 23:28:24 +00:00
2b3c401848 Python: Fix: Parse oauth_consent_request events in Azure AI client (#4197)
* Fix: Parse oauth_consent_request events in Azure AI client (#3950)

When Azure AI Agent Service returns an oauth_consent_request output item
for OAuth-protected MCP tools, the base OpenAI responses parser drops it
(hits case _ default branch). This causes agent runs to complete silently
with zero content.

Changes:
- Add oauth_consent_request ContentType and Content.from_oauth_consent_request()
  factory with consent_link field and user_input_request=True
- Override _parse_response_from_openai and _parse_chunk_from_openai in
  RawAzureAIClient to intercept Azure-specific oauth_consent_request items
- Add _emit_oauth_consent helper in AG-UI to emit CustomEvent for frontends
- Add tests proving base parser drops the event and Azure AI override catches it

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

* addressed comment

* addressed comments

* addressed comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-03 23:02:03 +00:00
7135ed13eb Python: Add file_ids and data_sources support to get_code_interpreter_tool() (#4201)
* Python: Add file_ids and data_sources support to AzureAIAgentClient.get_code_interpreter_tool()

Update the factory method to accept file_ids and data_sources keyword
arguments, matching the underlying azure.ai.agents SDK CodeInterpreterTool
constructor. This enables users to attach uploaded files for code
interpreter analysis.

Fixes #4050

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

* addressed comments

* addressed comments

* Add per-message file attachment support for AzureAIAgentClient

Add hosted_file handling in _prepare_messages() to convert
Content.from_hosted_file() into MessageAttachment on ThreadMessageOptions.
This enables per-message file scoping for code interpreter, matching the
underlying Azure AI Agents SDK MessageAttachment pattern.

- Add hosted_file case in _prepare_messages() match statement
- Import MessageAttachment from azure.ai.agents.models
- Add sample for per-message CSV file attachment with code interpreter
- Add employees.csv test data file
- Add 3 unit tests for hosted_file attachment conversion

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

* Address PR review: validation, fix assertions, remove MessageAttachment

- Add empty string validation in resolve_file_ids()
- Add test for Content with file_id=None
- Add test for empty string file_ids
- Revert MessageAttachment/hosted_file handling from _prepare_messages()
  (moved to separate issue #4352 for proper design)
- Remove per-message file upload sample and employees.csv
- Keep data_sources assertion as-is (dict keyed by asset_identifier)

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-03 23:01:17 +00:00
Tao ChenandGitHub 1a8729d5a7 Python: Fix workflow tests pyright warnings (#4362)
* Fix workflow tests pyright warnings

* Update uv.lock

* Fix pyright

* Comments

* Update root pyproject pyright setting

* Update core pyproject pyright setting

* Update core pyproject pyright setting
2026-03-03 21:52:05 +00:00
2a20750110 ADR: Python context compaction strategy (#3802)
* Add ADR for Python context compaction strategy

* Remove async vs sync open question - compact() is async

* updated adr

* docs: refine context compaction ADR

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

* updated adr

* further refinement

* renamed and numbered

* remove XX version

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-03 20:24:02 +00:00
fae36b36f2 Python: Add OpenTelemetry instrumentation to ClaudeAgent (#4278) (#4326)
* Python: Add OpenTelemetry instrumentation to ClaudeAgent (#4278)

Add inline telemetry to ClaudeAgent.run() so that enable_instrumentation()
emits invoke_agent spans and metrics. Covers both streaming and
non-streaming paths using the same observability helpers as
AgentTelemetryLayer. Adds 5 unit tests for telemetry behavior.

Co-Authored-By: amitmukh <amimukherjee@microsoft.com>

* Address PR review feedback for ClaudeAgent telemetry

- Add justification comment for private observability API imports
- Pass system_instructions to capture_messages for system prompt capture
- Use monkeypatch instead of try/finally for test global state isolation

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

* Adopt AgentTelemetryLayer instead of inline telemetry

Restructure ClaudeAgent to inherit from AgentTelemetryLayer via a
_ClaudeAgentRunImpl mixin, eliminating duplicated telemetry code and
private API imports.

MRO: ClaudeAgent → AgentTelemetryLayer → _ClaudeAgentRunImpl → BaseAgent

- Remove inline _run_with_telemetry / _run_with_telemetry_stream methods
- Remove private observability helper imports (_capture_messages, etc.)
- Add default_options property mapping system_prompt → instructions
- Net -105 lines by reusing core telemetry layer

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

* Fix mypy: align _ClaudeAgentRunImpl.run() signature with AgentTelemetryLayer.run()

Remove explicit `options` parameter from mixin's run() signature and
extract it from **kwargs to match AgentTelemetryLayer's signature.
Also align overload return types (ResponseStream, Awaitable) to match.

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

* Introduce RawClaudeAgent following framework's RawAgent/Agent pattern

Replace private _ClaudeAgentRunImpl mixin with public RawClaudeAgent
class that contains all core logic (init, run, lifecycle, tools).
ClaudeAgent becomes a thin wrapper that adds AgentTelemetryLayer.

- RawClaudeAgent(BaseAgent): full implementation without telemetry
- ClaudeAgent(AgentTelemetryLayer, RawClaudeAgent): adds OTel tracing
- Export RawClaudeAgent from package __init__.py

Users who want to skip telemetry or provide their own can use
RawClaudeAgent directly.

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

* Address review nits: trim RawClaudeAgent docstring, fix import paths

- Simplify RawClaudeAgent docstring to a single basic example (not the
  primary entry point for most users)
- Use agent_framework.anthropic import path in docstrings instead of
  direct agent_framework_claude path
- Add RawClaudeAgent to agent_framework.anthropic lazy re-exports

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

---------

Co-authored-by: Amit Mukherjee <amimukherjee@microsoft.com>
Co-authored-by: amitmukh <amitmukh@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2026-03-03 20:12:21 +00:00
381 changed files with 15842 additions and 4341 deletions
+1
View File
@@ -20,6 +20,7 @@ ignorePatterns:
- pattern: "https://your-resource.openai.azure.com/"
- pattern: "http://host.docker.internal"
- pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/"
- pattern: "https:\/\/dotnet.microsoft.com\/download"
# excludedDirs:
# Folders which include links to localhost, since it's not ignored with regular expressions
baseUrl: https://github.com/microsoft/agent-framework/
+109 -48
View File
@@ -59,20 +59,20 @@ jobs:
if: steps.filter.outputs.dotnet != 'true'
run: echo "NOT dotnet file"
dotnet-build-and-test:
# Build the full solution (including samples) on all TFMs. No tests.
dotnet-build:
needs: paths-filter
if: needs.paths-filter.outputs.dotnetChanges == 'true'
strategy:
fail-fast: false
matrix:
include:
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release }
- { targetFramework: "net9.0", os: "windows-latest", configuration: Debug }
- { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release }
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" }
- { targetFramework: "net472", os: "windows-latest", configuration: Release }
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v6
with:
@@ -84,16 +84,6 @@ jobs:
python
workflow-samples
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Setup dotnet
uses: actions/setup-dotnet@v5.1.0
with:
@@ -140,25 +130,98 @@ jobs:
popd
rm -rf "$TEMP_DIR"
- name: Run Unit Tests
shell: bash
run: |
export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | tr '\n' ' ')
for project in $UT_PROJECTS; do
# Query the project's target frameworks using MSBuild with the current configuration
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
# Build src+tests only (no samples) for a single TFM and run tests.
dotnet-test:
needs: paths-filter
if: needs.paths-filter.outputs.dotnetChanges == 'true'
strategy:
fail-fast: false
matrix:
include:
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" }
# Check if the project supports the target framework
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
else
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
fi
else
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
fi
done
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
python
workflow-samples
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Setup dotnet
uses: actions/setup-dotnet@v5.1.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Generate test solution (no samples)
shell: pwsh
run: |
./dotnet/eng/scripts/New-FilteredSolution.ps1 `
-Solution dotnet/agent-framework-dotnet.slnx `
-TargetFramework ${{ matrix.targetFramework }} `
-Configuration ${{ matrix.configuration }} `
-ExcludeSamples `
-OutputPath dotnet/filtered.slnx `
-Verbose
- name: Build src and tests
shell: bash
run: dotnet build dotnet/filtered.slnx -c ${{ matrix.configuration }} -f ${{ matrix.targetFramework }} --warnaserror
- name: Generate test-type filtered solutions
shell: pwsh
run: |
$commonArgs = @{
Solution = "dotnet/filtered.slnx"
TargetFramework = "${{ matrix.targetFramework }}"
Configuration = "${{ matrix.configuration }}"
Verbose = $true
}
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
-TestProjectNameFilter "*UnitTests*" `
-OutputPath dotnet/filtered-unit.slnx
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
-TestProjectNameFilter "*IntegrationTests*" `
-OutputPath dotnet/filtered-integration.slnx
- name: Run Unit Tests
shell: pwsh
working-directory: dotnet
run: |
$coverageSettings = Join-Path $PWD "tests/coverage.runsettings"
$coverageArgs = @()
if ("${{ matrix.targetFramework }}" -eq "${{ env.COVERAGE_FRAMEWORK }}") {
$coverageArgs = @(
"--coverage",
"--coverage-output-format", "cobertura",
"--coverage-settings", $coverageSettings,
"--results-directory", "../TestResults/Coverage/"
)
}
dotnet test --solution ./filtered-unit.slnx `
-f ${{ matrix.targetFramework }} `
-c ${{ matrix.configuration }} `
--no-build -v Normal `
--report-xunit-trx `
--ignore-exit-code 8 `
@coverageArgs
env:
# Cosmos DB Emulator connection settings
COSMOSDB_ENDPOINT: https://localhost:8081
@@ -185,21 +248,19 @@ jobs:
id: azure-functions-setup
- name: Run Integration Tests
shell: bash
shell: pwsh
working-directory: dotnet
if: github.event_name != 'pull_request' && matrix.integration-tests
run: |
export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ')
for project in $INTEGRATION_TEST_PROJECTS; do
# Query the project's target frameworks using MSBuild with the current configuration
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
# Check if the project supports the target framework
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled"
else
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
fi
done
dotnet test --solution ./filtered-integration.slnx `
-f ${{ matrix.targetFramework }} `
-c ${{ matrix.configuration }} `
--no-build -v Normal `
--report-xunit-trx `
--ignore-exit-code 8 `
--filter-not-trait "Category=IntegrationDisabled" `
--parallel-algorithm aggressive `
--max-threads 2.0x
env:
# Cosmos DB Emulator connection settings
COSMOSDB_ENDPOINT: https://localhost:8081
@@ -222,7 +283,7 @@ jobs:
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
with:
reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
reports: "./TestResults/Coverage/**/*.cobertura.xml"
targetdir: "./TestResults/Reports"
reporttypes: "HtmlInline;JsonSummary"
@@ -236,13 +297,13 @@ jobs:
- name: Check coverage
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
shell: pwsh
run: .github/workflows/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
run: ./dotnet/eng/scripts/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
# This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
dotnet-build-and-test-check:
if: always()
runs-on: ubuntu-latest
needs: [dotnet-build-and-test]
needs: [dotnet-build, dotnet-test]
steps:
- name: Get Date
shell: bash
+4 -1
View File
@@ -29,4 +29,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
timeout: 3600
interval: 30
ignored: CodeQL,CodeQL analysis (csharp)
# "Cleanup artifacts", "Agent", "Prepare", and "Upload results" are check runs
# created by an org-level GitHub App (MSDO), not by any workflow in this repo.
# They are outside our control and their transient failures should not block merges.
ignored: CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results
File diff suppressed because it is too large Load Diff
+50 -5
View File
@@ -17,14 +17,17 @@ dotnet format # Auto-fix formatting for all projects
# Build/test/format a specific project (preferred for isolated/internal changes)
dotnet build src/Microsoft.Agents.AI.<Package> --tl:off
dotnet test tests/Microsoft.Agents.AI.<Package>.UnitTests
dotnet test --project tests/Microsoft.Agents.AI.<Package>.UnitTests
dotnet format src/Microsoft.Agents.AI.<Package>
# Run a single test
dotnet test --filter "FullyQualifiedName~Namespace.TestClassName.TestMethodName"
# Replace the filter values with the appropriate assembly, namespace, class, and method names for the test you want to run and use * as a wildcard elsewhere, e.g. "/*/*/HttpClientTests/GetAsync_ReturnsSuccessStatusCode"
# Use `--ignore-exit-code 8` to avoid failing the build when no tests are found for some projects
dotnet test --filter-query "/<assemblyFilter>/<namespaceFilter>/<classFilter>/<methodFilter>" --ignore-exit-code 8
# Run unit tests only
dotnet test --filter FullyQualifiedName\~UnitTests
# Use `--ignore-exit-code 8` to avoid failing the build when no tests are found for integration test projects
dotnet test --filter-query "/*UnitTests*/*/*/*" --ignore-exit-code 8
```
Use `--tl:off` when building to avoid flickering when running commands in the agent.
@@ -56,7 +59,7 @@ Example: Running tests for a single project using .NET 10.
```bash
# From dotnet/ directory
dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0
dotnet test --project ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0
```
Example: Running a single test in a specific project using .NET 10.
@@ -64,7 +67,7 @@ Provide the full namespace, class name, and method name for the test you want to
```bash
# From dotnet/ directory
dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 --filter "FullyQualifiedName~Microsoft.Agents.AI.Abstractions.UnitTests.AgentRunOptionsTests.CloningConstructorCopiesProperties"
dotnet test --project ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 --filter-query "/*/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests/CloningConstructorCopiesProperties"
```
### Multi-target framework tip
@@ -83,3 +86,45 @@ Just remember to run `dotnet restore` after pulling changes, making changes to p
Unit tests target both .NET Framework as well as .NET Core. When running on Linux, only the .NET Core tests can be run, as .NET Framework is not supported on Linux.
To run only the .NET Core tests, use the `-f net10.0` option with `dotnet test`.
### Microsoft Testing Platform (MTP)
Tests use the [Microsoft Testing Platform](https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-intro) via xUnit v3. Key differences from the legacy VSTest runner:
- **`dotnet test` requires `--project`** to specify a test project directly (positional arguments are no longer supported).
- **Test output** uses the MTP format (e.g., `[✓112/x0/↓0]` progress and `Test run summary: Passed!`).
- **TRX reports** use `--report-xunit-trx` instead of `--logger trx`.
- **Code coverage** uses `Microsoft.Testing.Extensions.CodeCoverage` with `--coverage --coverage-output-format cobertura`.
- **Running a test project directly** is supported via `dotnet run --project <test-project>`. This bypasses the `dotnet test` infrastructure and runs the test executable directly with the MTP command line.
- **Running tests across the solution** with a filter may cause some projects to match zero tests, which MTP treats as a failure (exit code 8). Use `--ignore-exit-code 8` to suppress this:
```bash
# Run all unit tests across the solution, ignoring projects with no matching tests
dotnet test --solution ./agent-framework-dotnet.slnx --no-build -f net10.0 --ignore-exit-code 8
```
- **Running tests with `--solution` for a specific TFM** requires all projects in the solution to support that TFM. Not all projects target every framework (e.g., some are `net10.0`-only). Use `./dotnet/eng/scripts/New-FilteredSolution.ps1` to generate a filtered solution:
```powershell
# Generate a filtered solution for net472 and run tests
$filtered = ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472
dotnet test --solution $filtered --no-build -f net472 --ignore-exit-code 8
# Exclude samples and keep only unit test projects
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -ExcludeSamples -TestProjectNameFilter "*UnitTests*" -OutputPath dotnet/filtered-unit.slnx
```
```bash
# Run tests via dotnet test (uses MTP under the hood)
dotnet test --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0
# Run tests with code coverage (Cobertura format)
dotnet test --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 --coverage --coverage-output-format cobertura --coverage-settings ./tests/coverage.runsettings
# Run tests directly via dotnet run (MTP native command line)
dotnet run --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0
# Show MTP command line help
dotnet run --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 -- -?
```
+9 -11
View File
@@ -19,8 +19,8 @@
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
@@ -35,7 +35,7 @@
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
@@ -94,7 +94,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.23" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.29" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -140,12 +140,10 @@
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net10.0'" Version="10.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="Moq" Version="[4.18.4]" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xretry" Version="1.9.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xRetry.v3" Version="1.0.0-rc3" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.4.1" />
<!-- Symbols -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<!-- Toolset -->
@@ -187,4 +185,4 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
</Project>
+13 -1
View File
@@ -284,8 +284,13 @@
</Folder>
<Folder Name="/Samples/05-end-to-end/HostedAgents/">
<Project Path="samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml" />
@@ -311,7 +316,6 @@
</Folder>
<Folder Name="/Solution Items/.github/workflows/">
<File Path="../.github/workflows/dotnet-build-and-test.yml" />
<File Path="../.github/workflows/dotnet-check-coverage.ps1" />
<File Path="../.github/workflows/dotnet-format.yml" />
</Folder>
<Folder Name="/Solution Items/demos/">
@@ -348,6 +352,10 @@
<File Path="eng/MSBuild/Shared.props" />
<File Path="eng/MSBuild/Shared.targets" />
</Folder>
<Folder Name="/Solution Items/eng/scripts/">
<File Path="eng/scripts/dotnet-check-coverage.ps1" />
<File Path="eng/scripts/New-FilteredSolution.ps1" />
</Folder>
<Folder Name="/Solution Items/nuget/">
<File Path="nuget/icon.png" />
<File Path="nuget/nuget-package.props" />
@@ -413,6 +421,10 @@
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/README.md" />
</Folder>
<Folder Name="/Solution Items/src/Shared/IntegrationTestsAzureCredentials/">
<File Path="src/Shared/IntegrationTestsAzureCredentials/README.md" />
<File Path="src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Samples/">
<File Path="src/Shared/Samples/BaseSample.cs" />
<File Path="src/Shared/Samples/README.md" />
+3
View File
@@ -8,6 +8,9 @@
<ItemGroup Condition="'$(InjectSharedIntegrationTestCode)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTests\*.cs" LinkBase="Shared\IntegrationTests" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
</ItemGroup>
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env pwsh
# Copyright (c) Microsoft. All rights reserved.
<#
.SYNOPSIS
Generates a filtered .slnx solution file by removing projects that don't match the specified criteria.
.DESCRIPTION
Parses a .slnx solution file and applies one or more filters:
- Removes projects that don't support the specified target framework (via MSBuild query).
- Optionally removes all sample projects (under samples/).
- Optionally filters test projects by name pattern (e.g., only *UnitTests*).
Writes the filtered solution to the specified output path and prints the path.
.PARAMETER Solution
Path to the source .slnx solution file.
.PARAMETER TargetFramework
The target framework to filter by (e.g., net10.0, net472).
.PARAMETER Configuration
Optional MSBuild configuration used when querying TargetFrameworks. Defaults to Debug.
.PARAMETER TestProjectNameFilter
Optional wildcard pattern to filter test project names (e.g., *UnitTests*, *IntegrationTests*).
When specified, only test projects whose filename matches this pattern are kept.
.PARAMETER ExcludeSamples
When specified, removes all projects under the samples/ directory from the solution.
.PARAMETER OutputPath
Optional output path for the filtered .slnx file. If not specified, a temp file is created.
.EXAMPLE
# Generate a filtered solution and run tests
$filtered = ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472
dotnet test --solution $filtered --no-build -f net472
.EXAMPLE
# Generate a solution with only unit test projects
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameFilter "*UnitTests*" -OutputPath filtered-unit.slnx
.EXAMPLE
# Inline usage with dotnet test (PowerShell)
dotnet test --solution (./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472) --no-build -f net472
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Solution,
[Parameter(Mandatory)]
[string]$TargetFramework,
[string]$Configuration = "Debug",
[string]$TestProjectNameFilter,
[switch]$ExcludeSamples,
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
# Resolve the solution path
$solutionPath = Resolve-Path $Solution
$solutionDir = Split-Path $solutionPath -Parent
if (-not $OutputPath) {
$OutputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "filtered-$(Split-Path $solutionPath -Leaf)")
}
# Parse the .slnx XML
[xml]$slnx = Get-Content $solutionPath -Raw
$removed = @()
$kept = @()
# Remove sample projects if requested
if ($ExcludeSamples) {
$sampleProjects = $slnx.SelectNodes("//Project[contains(@Path, 'samples/')]")
foreach ($proj in $sampleProjects) {
$projRelPath = $proj.GetAttribute("Path")
Write-Verbose "Removing (sample): $projRelPath"
$removed += $projRelPath
$proj.ParentNode.RemoveChild($proj) | Out-Null
}
Write-Host "Removed $($sampleProjects.Count) sample project(s)." -ForegroundColor Yellow
}
# Filter all remaining projects by target framework
$allProjects = $slnx.SelectNodes("//Project")
foreach ($proj in $allProjects) {
$projRelPath = $proj.GetAttribute("Path")
$projFullPath = Join-Path $solutionDir $projRelPath
$projFileName = Split-Path $projRelPath -Leaf
$isTestProject = $projRelPath -like "*tests/*"
# Filter test projects by name pattern if specified
if ($isTestProject -and $TestProjectNameFilter -and ($projFileName -notlike $TestProjectNameFilter)) {
Write-Verbose "Removing (name filter): $projRelPath"
$removed += $projRelPath
$proj.ParentNode.RemoveChild($proj) | Out-Null
continue
}
if (-not (Test-Path $projFullPath)) {
Write-Verbose "Project not found, keeping in solution: $projRelPath"
$kept += $projRelPath
continue
}
# Query the project's target frameworks using MSBuild
$targetFrameworks = & dotnet msbuild $projFullPath -getProperty:TargetFrameworks -p:Configuration=$Configuration -nologo 2>$null
$targetFrameworks = $targetFrameworks.Trim()
if ($targetFrameworks -like "*$TargetFramework*") {
Write-Verbose "Keeping: $projRelPath (targets: $targetFrameworks)"
$kept += $projRelPath
}
else {
Write-Verbose "Removing: $projRelPath (targets: $targetFrameworks, missing: $TargetFramework)"
$removed += $projRelPath
$proj.ParentNode.RemoveChild($proj) | Out-Null
}
}
# Write the filtered solution
$slnx.Save($OutputPath)
# Report results to stderr so stdout is clean for piping
Write-Host "Filtered solution written to: $OutputPath" -ForegroundColor Green
if ($removed.Count -gt 0) {
Write-Host "Removed $($removed.Count) project(s):" -ForegroundColor Yellow
foreach ($r in $removed) {
Write-Host " - $r" -ForegroundColor Yellow
}
}
Write-Host "Kept $($kept.Count) project(s)." -ForegroundColor Green
# Output the path for piping
Write-Output $OutputPath
+3
View File
@@ -3,5 +3,8 @@
"version": "10.0.100",
"rollForward": "minor",
"allowPrerelease": false
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
+4 -4
View File
@@ -2,11 +2,11 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<RCNumber>2</RCNumber>
<RCNumber>3</RCNumber>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260225.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260225.1</PackageVersion>
<GitTag>1.0.0-rc2</GitTag>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260304.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260304.1</PackageVersion>
<GitTag>1.0.0-rc3</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -60,7 +60,7 @@ Console.WriteLine();
// Submit the red team run to the service
Console.WriteLine("Submitting red team run...");
RedTeam redTeamRun = await aiProjectClient.RedTeams.CreateAsync(redTeamConfig);
RedTeam redTeamRun = await aiProjectClient.RedTeams.CreateAsync(redTeamConfig, options: null);
Console.WriteLine($"Red team run created: {redTeamRun.Name}");
Console.WriteLine($"Status: {redTeamRun.Status}");
@@ -35,7 +35,7 @@ string userScope = $"user_{Environment.MachineName}";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create the Memory Search tool configuration
MemorySearchTool memorySearchTool = new(memoryStoreName, userScope)
MemorySearchPreviewTool memorySearchTool = new(memoryStoreName, userScope)
{
// Optional: Configure how quickly new memories are indexed (in seconds)
UpdateDelay = 1,
@@ -88,7 +88,9 @@ internal sealed class Program
{
string workflowYaml = File.ReadAllText("MathChat.yaml");
#pragma warning disable AAIP001 // WorkflowAgentDefinition is experimental
WorkflowAgentDefinition workflowAgentDefinition = WorkflowAgentDefinition.FromYaml(workflowYaml);
#pragma warning restore AAIP001
return
await agentClient.CreateAgentAsync(
@@ -75,10 +75,15 @@ string apiKey = builder.Configuration["OPENAI_API_KEY"]
?? throw new InvalidOperationException("Set the OPENAI_API_KEY environment variable.");
string model = builder.Configuration["OPENAI_MODEL"] ?? "gpt-4.1-mini";
// Here we are using Singleton lifetime, since none of the services, function tools and user context classes in the sample have state that are per request.
// You should evaluate the appropriate lifetime for your own services and tools based on their behavior and dependencies.
// E.g. if any of the service instances or tools maintain state that is specific to a user, and each request may be from a different user,
// you should use Scoped lifetime instead, so that a new instance is created for each request.
// Note that if you use Scoped lifetime for any dependencies, you must also use Scoped lifetime for any class that uses it, including the agent itself.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IUserContext, KeycloakUserContext>();
builder.Services.AddScoped<ExpenseService>();
builder.Services.AddScoped<AIAgent>(sp =>
builder.Services.AddSingleton<IUserContext, KeycloakUserContext>();
builder.Services.AddSingleton<ExpenseService>();
builder.Services.AddSingleton<AIAgent>(sp =>
{
var expenseService = sp.GetRequiredService<ExpenseService>();
@@ -27,43 +27,73 @@ public interface IUserContext
/// Keycloak uses <c>sub</c> for the user ID, <c>preferred_username</c>
/// for the login name, <c>given_name</c>/<c>family_name</c> for the
/// display name, and <c>scope</c> (space-delimited) for granted scopes.
/// Registered as a scoped service so it is resolved once per request.
/// Registered as a singleton — claims are parsed once per request and
/// cached in <see cref="HttpContext.Items"/>.
/// </summary>
public sealed class KeycloakUserContext : IUserContext
{
public string UserId { get; }
private static readonly object s_cacheKey = new();
public string UserName { get; }
public string DisplayName { get; }
public IReadOnlySet<string> Scopes { get; }
private readonly IHttpContextAccessor _httpContextAccessor;
public KeycloakUserContext(IHttpContextAccessor httpContextAccessor)
{
ClaimsPrincipal? user = httpContextAccessor.HttpContext?.User;
this._httpContextAccessor = httpContextAccessor;
}
this.UserId = user?.FindFirstValue(ClaimTypes.NameIdentifier)
?? user?.FindFirstValue("sub")
?? "anonymous";
public string UserId => this.GetOrCreateCachedInfo().UserId;
this.UserName = user?.FindFirstValue("preferred_username")
?? user?.FindFirstValue(ClaimTypes.Name)
?? "unknown";
public string UserName => this.GetOrCreateCachedInfo().UserName;
public string DisplayName => this.GetOrCreateCachedInfo().DisplayName;
public IReadOnlySet<string> Scopes => this.GetOrCreateCachedInfo().Scopes;
private CachedUserInfo GetOrCreateCachedInfo()
{
HttpContext? httpContext = this._httpContextAccessor.HttpContext;
if (httpContext is not null && httpContext.Items.TryGetValue(s_cacheKey, out object? cached) && cached is CachedUserInfo info)
{
return info;
}
info = ParseClaims(httpContext?.User);
if (httpContext is not null)
{
httpContext.Items[s_cacheKey] = info;
}
return info;
}
private static CachedUserInfo ParseClaims(ClaimsPrincipal? user)
{
string userId = user?.FindFirstValue(ClaimTypes.NameIdentifier)
?? user?.FindFirstValue("sub")
?? "anonymous";
string userName = user?.FindFirstValue("preferred_username")
?? user?.FindFirstValue(ClaimTypes.Name)
?? "unknown";
string? givenName = user?.FindFirstValue("given_name") ?? user?.FindFirstValue(ClaimTypes.GivenName);
string? familyName = user?.FindFirstValue("family_name") ?? user?.FindFirstValue(ClaimTypes.Surname);
this.DisplayName = (givenName, familyName) switch
string displayName = (givenName, familyName) switch
{
(not null, not null) => $"{givenName} {familyName}",
(not null, null) => givenName,
(null, not null) => familyName,
_ => this.UserName,
_ => userName,
};
string? scopeClaim = user?.FindFirstValue("scope");
this.Scopes = scopeClaim is not null
IReadOnlySet<string> scopes = scopeClaim is not null
? new HashSet<string>(scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
return new CachedUserInfo(userId, userName, displayName, scopes);
}
private sealed record CachedUserInfo(string UserId, string UserName, string DisplayName, IReadOnlySet<string> Scopes);
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -36,11 +36,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -11,9 +11,10 @@ using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
@@ -22,17 +23,19 @@ static string GetWeather([Description("The location to get the weather for.")] s
// Create the chat client and agent.
// Note: ApprovalRequiredAIFunction wraps the tool to require user approval before invocation.
// User should reply with 'approve' or 'reject' when prompted.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
#pragma warning disable MEAI001 // Type is for evaluation purposes only
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient()
.CreateAIAgent(
.AsAIAgent(
instructions: "You are a helpful assistant",
tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]
);
#pragma warning restore MEAI001
var threadRepository = new InMemoryAgentThreadRepository(agent);
InMemoryAgentThreadRepository threadRepository = new(agent);
await agent.RunAIAgentAsync(telemetrySourceName: "Agents", threadRepository: threadRepository);
@@ -35,10 +35,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.6" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -9,9 +9,10 @@ using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Create an MCP tool that can be called without approval.
AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAddress: "https://learn.microsoft.com/api/mcp")
@@ -28,8 +29,7 @@ AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsIChatClient()
.CreateAIAgent(
.AsAIAgent(
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgent",
tools: [mcpTool]);
@@ -18,7 +18,7 @@ Before running this sample, ensure you have:
2. A deployment of a chat model (e.g., gpt-4o-mini)
3. Azure CLI installed and authenticated
**Note**: This sample uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource.
**Note**: This sample uses `DefaultAzureCredential` for authentication, which probes multiple sources automatically. For local development, make sure you're logged in with `az login` and have access to the Azure OpenAI resource.
## Environment Variables
@@ -36,11 +36,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -15,21 +15,21 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
Console.WriteLine($"Project Endpoint: {endpoint}");
Console.WriteLine($"Model Deployment: {deploymentName}");
var seattleHotels = new[]
{
Hotel[] seattleHotels =
[
new Hotel("Contoso Suites", 189, 4.5, "Downtown"),
new Hotel("Fabrikam Residences", 159, 4.2, "Pike Place Market"),
new Hotel("Alpine Ski House", 249, 4.7, "Seattle Center"),
new Hotel("Margie's Travel Lodge", 219, 4.4, "Waterfront"),
new Hotel("Northwind Inn", 139, 4.0, "Capitol Hill"),
new Hotel("Relecloud Hotel", 99, 3.8, "University District"),
};
];
[Description("Get available hotels in Seattle for the specified dates. This simulates a call to a hotel availability API.")]
string GetAvailableHotels(
@@ -54,21 +54,21 @@ string GetAvailableHotels(
return "Error: Check-out date must be after check-in date.";
}
var nights = (checkOut - checkIn).Days;
var availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList();
int nights = (checkOut - checkIn).Days;
List<Hotel> availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList();
if (availableHotels.Count == 0)
{
return $"No hotels found in Seattle within your budget of ${maxPrice}/night.";
}
var result = new StringBuilder();
StringBuilder result = new();
result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):");
result.AppendLine();
foreach (var hotel in availableHotels)
foreach (Hotel hotel in availableHotels)
{
var totalCost = hotel.PricePerNight * nights;
int totalCost = hotel.PricePerNight * nights;
result.AppendLine($"**{hotel.Name}**");
result.AppendLine($" Location: {hotel.Location}");
result.AppendLine($" Rating: {hotel.Rating}/5");
@@ -84,7 +84,10 @@ string GetAvailableHotels(
}
}
var credential = new AzureCliCredential();
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
DefaultAzureCredential credential = new();
AIProjectClient projectClient = new(new Uri(endpoint), credential);
ClientConnection connection = projectClient.GetConnection(typeof(AzureOpenAIClient).FullName!);
@@ -96,14 +99,14 @@ if (!connection.TryGetLocatorAsUri(out Uri? openAiEndpoint) || openAiEndpoint is
openAiEndpoint = new Uri($"https://{openAiEndpoint.Host}");
Console.WriteLine($"OpenAI Endpoint: {openAiEndpoint}");
var chatClient = new AzureOpenAIClient(openAiEndpoint, credential)
IChatClient chatClient = new AzureOpenAIClient(openAiEndpoint, credential)
.GetChatClient(deploymentName)
.AsIChatClient()
.AsBuilder()
.UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false)
.Build();
var agent = new ChatClientAgent(chatClient,
AIAgent agent = chatClient.AsAIAgent(
name: "SeattleHotelAgent",
instructions: """
You are a helpful travel assistant specializing in finding hotels in Seattle, Washington.
@@ -35,11 +35,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -11,8 +11,8 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
TextSearchProviderOptions textSearchOptions = new()
{
@@ -28,13 +28,13 @@ AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new ChatOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
},
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)]
});
await agent.RunAIAgentAsync();
@@ -35,11 +35,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -9,13 +9,16 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var toolConnectionId = Environment.GetEnvironmentVariable("MCP_TOOL_CONNECTION_ID") ?? throw new InvalidOperationException("MCP_TOOL_CONNECTION_ID is not set.");
string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string toolConnectionId = Environment.GetEnvironmentVariable("MCP_TOOL_CONNECTION_ID") ?? throw new InvalidOperationException("MCP_TOOL_CONNECTION_ID is not set.");
var credential = new AzureCliCredential();
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
DefaultAzureCredential credential = new();
var chatClient = new AzureOpenAIClient(new Uri(openAiEndpoint), credential)
IChatClient chatClient = new AzureOpenAIClient(new Uri(openAiEndpoint), credential)
.GetChatClient(deploymentName)
.AsIChatClient()
.AsBuilder()
@@ -23,7 +26,7 @@ var chatClient = new AzureOpenAIClient(new Uri(openAiEndpoint), credential)
.UseOpenTelemetry(sourceName: "Agents", configure: (cfg) => cfg.EnableSensitiveData = true)
.Build();
var agent = new ChatClientAgent(chatClient,
AIAgent agent = chatClient.AsAIAgent(
name: "AgentWithTools",
instructions: @"You are a helpful assistant with access to tools for fetching Microsoft documentation.
@@ -6,7 +6,7 @@ Key features:
- Configuring Foundry tools using `UseFoundryTools` with MCP and code interpreter
- Connecting to an external MCP tool via a Foundry project connection
- Using `AzureCliCredential` for Azure authentication
- Using `DefaultAzureCredential` for Azure authentication
- OpenTelemetry instrumentation for both the chat client and agent
> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md).
@@ -36,7 +36,7 @@ $env:MCP_TOOL_CONNECTION_ID="SampleMCPTool"
## How It Works
1. An `AzureOpenAIClient` is created with `AzureCliCredential` and used to get a chat client
1. An `AzureOpenAIClient` is created with `DefaultAzureCredential` and used to get a chat client
2. The chat client is wrapped with `UseFoundryTools` which registers two Foundry tool types:
- **MCP connection**: Connects to an external MCP server (Microsoft Learn) via the project connection name, providing documentation fetch and search capabilities
- **Code interpreter**: Allows the agent to execute code snippets when needed
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -35,11 +35,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -12,8 +12,8 @@ using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
// Set up the Azure OpenAI client
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
@@ -32,9 +32,9 @@ AIAgent agent = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
.Build()
.AsAgent();
.AsAIAgent();
await agent.RunAIAgentAsync();
static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) =>
new(chatClient, $"You are a translation assistant that translates the provided text to {targetLanguage}.");
static AIAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) =>
chatClient.AsAIAgent($"You are a translation assistant that translates the provided text to {targetLanguage}.");
@@ -19,7 +19,7 @@ Before you begin, ensure you have the following prerequisites:
- Azure OpenAI 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 OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
**Note**: This demo uses `DefaultAzureCredential` for authentication, which probes multiple sources automatically. For local development, make sure you're logged in with `az login` and have access to the Azure OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
@@ -0,0 +1,20 @@
# Build the application
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
# Copy files from the current directory on the host to the working directory in the container
COPY . .
RUN dotnet restore
RUN dotnet build -c Release --no-restore
RUN dotnet publish -c Release --no-build -o /app -f net10.0
# Run the application
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
# Copy everything needed to run the app from the "build" stage.
COPY --from=build /app .
EXPOSE 8088
ENTRYPOINT ["dotnet", "FoundryMultiAgent.dll"]
@@ -0,0 +1,76 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!--
Disable central package management for this project.
This project requires explicit package references with versions specified inline rather than
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
be created from this project, and the Docker build process only has access to this folder
and cannot access parent folders where Directory.Packages.props resides.
-->
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<!--
Remove analyzer PackageReference items inherited from Directory.Packages.props.
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
and the PackageReference items are always inherited through MSBuild imports regardless of the
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
-->
<ItemGroup>
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
<PackageReference Remove="xunit.analyzers" />
<PackageReference Remove="Moq.Analyzers" />
<PackageReference Remove="Roslynator.Analyzers" />
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251219.1" />
<PackageReference Include="OpenTelemetry" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates a multi-agent workflow with Writer and Reviewer agents
// using Azure AI Foundry AIProjectClient and the Agent Framework WorkflowBuilder.
using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
Console.WriteLine($"Using Azure AI endpoint: {endpoint}");
Console.WriteLine($"Using model deployment: {deploymentName}");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create Foundry agents
AIAgent writerAgent = await aiProjectClient.CreateAIAgentAsync(
name: "Writer",
model: deploymentName,
instructions: "You are an excellent content writer. You create new content and edit contents based on the feedback.");
AIAgent reviewerAgent = await aiProjectClient.CreateAIAgentAsync(
name: "Reviewer",
model: deploymentName,
instructions: "You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content. Provide the feedback in the most concise manner possible.");
try
{
var workflow = new WorkflowBuilder(writerAgent)
.AddEdge(writerAgent, reviewerAgent)
.Build();
Console.WriteLine("Starting Writer-Reviewer Workflow Agent Server on http://localhost:8088");
await workflow.AsAgent().RunAIAgentAsync();
}
finally
{
// Cleanup server-side agents
await aiProjectClient.Agents.DeleteAgentAsync(writerAgent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(reviewerAgent.Name);
}
@@ -0,0 +1,168 @@
**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md).
Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct.
Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates.
Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output.
# What this sample demonstrates
This sample demonstrates a **key advantage of code-based hosted agents**:
- **Multi-agent workflows** - Orchestrate multiple agents working together
Code-based agents can execute **any C# code** you write. This sample includes a Writer-Reviewer workflow where two agents collaborate: a Writer creates content and a Reviewer provides feedback.
The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/) and can be deployed to Microsoft Foundry.
## How It Works
### Multi-Agent Workflow
In [Program.cs](Program.cs), the sample creates two agents using `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package:
- **Writer** - An agent that creates and edits content based on feedback
- **Reviewer** - An agent that provides actionable feedback on the content
The `WorkflowBuilder` from the [Microsoft.Agents.AI.Workflows](https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/) package connects these agents in a sequential flow:
1. The Writer receives the initial request and generates content
2. The Reviewer evaluates the content and provides feedback
3. Both agent responses are output to the user
### Agent Hosting
The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/),
which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Running the Agent Locally
### Prerequisites
Before running this sample, ensure you have:
1. **Azure AI Foundry Project**
- Project created.
- Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`)
- Note your project endpoint URL and model deployment name
> **Note**: You can right-click the project in the Microsoft Foundry VS Code extension and select `Copy Project Endpoint URL` to get the endpoint.
2. **Azure CLI**
- Installed and authenticated
- Run `az login` and verify with `az account show`
- Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`)
3. **.NET 10.0 SDK or later**
- Verify your version: `dotnet --version`
- Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download)
### Environment Variables
Set the following environment variables:
**PowerShell:**
```powershell
# Replace with your actual values
$env:AZURE_AI_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
$env:MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
**Bash:**
```bash
export AZURE_AI_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
export MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Running the Sample
To run the agent, execute the following command in your terminal:
```bash
dotnet restore
dotnet build
dotnet run
```
This will start the hosted agent locally on `http://localhost:8088/`.
### Interacting with the Agent
**VS Code:**
1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command.
2. Execute the following commands to start the containerized hosted agent.
```bash
dotnet restore
dotnet build
dotnet run
```
3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "Create a slogan for a new electric SUV that is affordable and fun to drive."
4. Review the agent's response in the playground interface.
> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly.
**PowerShell (Windows):**
```powershell
$body = @{
input = "Create a slogan for a new electric SUV that is affordable and fun to drive"
stream = $false
} | ConvertTo-Json
Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json"
```
**Bash/curl (Linux/macOS):**
```bash
curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \
-d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive","stream":false}'
```
You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension.
The Writer agent will generate content based on your prompt, and the Reviewer agent will provide feedback on the output.
## Deploying the Agent to Microsoft Foundry
**Preparation (required)**
Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project.
To deploy the hosted agent:
1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command.
2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs.
3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground.
**What the deploy flow does for you:**
- Creates or obtains an Azure Container Registry for the target project.
- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`).
- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime).
- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it.
## MSI Configuration in the Azure Portal
This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role.
To configure the Managed Identity:
1. In the Azure Portal, open the Foundry Project.
2. Select "Access control (IAM)" from the left-hand menu.
3. Click "Add" and choose "Add role assignment".
4. In the role selection, search for and select "Azure AI User", then click "Next".
5. For "Assign access to", choose "Managed identity".
6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select".
7. Click "Review + assign" to complete the assignment.
8. Allow a few minutes for the role assignment to propagate before running the application.
## Additional Resources
- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/)
@@ -0,0 +1,31 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
name: FoundryMultiAgent
displayName: "Foundry Multi-Agent Workflow"
description: >
A multi-agent workflow featuring a Writer and Reviewer that collaborate
to create and refine content using Azure AI Foundry PersistentAgentsClient.
metadata:
authors:
- Microsoft Agent Framework Team
tags:
- Azure AI AgentServer
- Microsoft Agent Framework
- Multi-Agent Workflow
- Writer-Reviewer
- Content Creation
template:
kind: hosted
name: FoundryMultiAgent
protocols:
- protocol: responses
version: v1
environment_variables:
- name: AZURE_AI_PROJECT_ENDPOINT
value: ${AZURE_AI_PROJECT_ENDPOINT}
- name: MODEL_DEPLOYMENT_NAME
value: gpt-4o-mini
resources:
- name: "gpt-4o-mini"
kind: model
id: gpt-4o-mini
@@ -0,0 +1,4 @@
{
"AZURE_AI_PROJECT_ENDPOINT": "https://<your-resource>.services.ai.azure.com/api/projects/<your-project>",
"MODEL_DEPLOYMENT_NAME": "gpt-4o-mini"
}
@@ -0,0 +1,34 @@
@host = http://localhost:8088
@endpoint = {{host}}/responses
### Health Check
GET {{host}}/readiness
### Simple string input - Content creation request
POST {{endpoint}}
Content-Type: application/json
{
"input": "Create a slogan for a new electric SUV that is affordable and fun to drive",
"stream": false
}
### Explicit input format
POST {{endpoint}}
Content-Type: application/json
{
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Write a short product description for a smart water bottle that tracks hydration"
}
]
}
],
"stream": false
}
@@ -0,0 +1,20 @@
# Build the application
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
# Copy files from the current directory on the host to the working directory in the container
COPY . .
RUN dotnet restore
RUN dotnet build -c Release --no-restore
RUN dotnet publish -c Release --no-build -o /app -f net10.0
# Run the application
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
# Copy everything needed to run the app from the "build" stage.
COPY --from=build /app .
EXPOSE 8088
ENTRYPOINT ["dotnet", "FoundrySingleAgent.dll"]
@@ -0,0 +1,67 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!--
Disable central package management for this project.
This project requires explicit package references with versions specified inline rather than
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
be created from this project, and the Docker build process only has access to this folder
and cannot access parent folders where Directory.Packages.props resides.
-->
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<!--
Remove analyzer PackageReference items inherited from Directory.Packages.props.
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
and the PackageReference items are always inherited through MSBuild imports regardless of the
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
-->
<ItemGroup>
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
<PackageReference Remove="xunit.analyzers" />
<PackageReference Remove="Moq.Analyzers" />
<PackageReference Remove="Roslynator.Analyzers" />
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251219.1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft. All rights reserved.
// Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle.
// Uses Microsoft Agent Framework with Azure AI Foundry.
// Ready for deployment to Foundry Hosted Agent service.
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// Get configuration from environment variables
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
Console.WriteLine($"Project Endpoint: {endpoint}");
Console.WriteLine($"Model Deployment: {deploymentName}");
// Simulated hotel data for Seattle
var seattleHotels = new[]
{
new Hotel("Contoso Suites", 189, 4.5, "Downtown"),
new Hotel("Fabrikam Residences", 159, 4.2, "Pike Place Market"),
new Hotel("Alpine Ski House", 249, 4.7, "Seattle Center"),
new Hotel("Margie's Travel Lodge", 219, 4.4, "Waterfront"),
new Hotel("Northwind Inn", 139, 4.0, "Capitol Hill"),
new Hotel("Relecloud Hotel", 99, 3.8, "University District"),
};
[Description("Get available hotels in Seattle for the specified dates. This simulates a call to a hotel availability API.")]
string GetAvailableHotels(
[Description("Check-in date in YYYY-MM-DD format")] string checkInDate,
[Description("Check-out date in YYYY-MM-DD format")] string checkOutDate,
[Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500)
{
try
{
// Parse dates
if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn))
{
return "Error parsing check-in date. Please use YYYY-MM-DD format.";
}
if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut))
{
return "Error parsing check-out date. Please use YYYY-MM-DD format.";
}
// Validate dates
if (checkOut <= checkIn)
{
return "Error: Check-out date must be after check-in date.";
}
var nights = (checkOut - checkIn).Days;
// Filter hotels by price
var availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList();
if (availableHotels.Count == 0)
{
return $"No hotels found in Seattle within your budget of ${maxPrice}/night.";
}
// Build response
var result = new StringBuilder();
result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):");
result.AppendLine();
foreach (var hotel in availableHotels)
{
var totalCost = hotel.PricePerNight * nights;
result.AppendLine($"**{hotel.Name}**");
result.AppendLine($" Location: {hotel.Location}");
result.AppendLine($" Rating: {hotel.Rating}/5");
result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})");
result.AppendLine();
}
return result.ToString();
}
catch (Exception ex)
{
return $"Error processing request. Details: {ex.Message}";
}
}
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create Foundry agent with hotel search tool
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
name: "SeattleHotelAgent",
model: deploymentName,
instructions: """
You are a helpful travel assistant specializing in finding hotels in Seattle, Washington.
When a user asks about hotels in Seattle:
1. Ask for their check-in and check-out dates if not provided
2. Ask about their budget preferences if not mentioned
3. Use the GetAvailableHotels tool to find available options
4. Present the results in a friendly, informative way
5. Offer to help with additional questions about the hotels or Seattle
Be conversational and helpful. If users ask about things outside of Seattle hotels,
politely let them know you specialize in Seattle hotel recommendations.
""",
tools: [AIFunctionFactory.Create(GetAvailableHotels)]);
try
{
Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088");
await agent.RunAIAgentAsync(telemetrySourceName: "Agents");
}
finally
{
// Cleanup server-side agent
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
}
// Hotel record for simulated data
internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location);
@@ -0,0 +1,167 @@
**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md).
Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct.
Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates.
Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output.
# What this sample demonstrates
This sample demonstrates a **key advantage of code-based hosted agents**:
- **Local C# tool execution** - Run custom C# methods as agent tools
Code-based agents can execute **any C# code** you write. This sample includes a Seattle Hotel Agent with a `GetAvailableHotels` tool that searches for available hotels based on check-in/check-out dates and budget preferences.
The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme) and can be deployed to Microsoft Foundry.
## How It Works
### Local Tools Integration
In [Program.cs](Program.cs), the agent uses `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package to create a Foundry agent with a local C# method (`GetAvailableHotels`) that simulates a hotel availability API. This demonstrates how code-based agents can execute custom server-side logic that prompt agents cannot access.
The tool accepts:
- **checkInDate** - Check-in date in YYYY-MM-DD format
- **checkOutDate** - Check-out date in YYYY-MM-DD format
- **maxPrice** - Maximum price per night in USD (optional, defaults to $500)
### Agent Hosting
The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme),
which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
## Running the Agent Locally
### Prerequisites
Before running this sample, ensure you have:
1. **Azure AI Foundry Project**
- Project created.
- Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`)
- Note your project endpoint URL and model deployment name
2. **Azure CLI**
- Installed and authenticated
- Run `az login` and verify with `az account show`
- Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`)
3. **.NET 10.0 SDK or later**
- Verify your version: `dotnet --version`
- Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download)
### Environment Variables
Set the following environment variables (matching `agent.yaml`):
- `AZURE_AI_PROJECT_ENDPOINT` - Your Azure AI Foundry project endpoint URL (required)
- `MODEL_DEPLOYMENT_NAME` - The deployment name for your chat model (defaults to `gpt-4o-mini`)
**PowerShell:**
```powershell
# Replace with your actual values
$env:AZURE_AI_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
$env:MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
**Bash:**
```bash
export AZURE_AI_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
export MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Running the Sample
To run the agent, execute the following command in your terminal:
```bash
dotnet restore
dotnet build
dotnet run
```
This will start the hosted agent locally on `http://localhost:8088/`.
### Interacting with the Agent
**VS Code:**
1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command.
2. Execute the following commands to start the containerized hosted agent.
```bash
dotnet restore
dotnet build
dotnet run
```
3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night."
4. The agent will use the GetAvailableHotels tool to search for available hotels matching your criteria.
> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly.
**PowerShell (Windows):**
```powershell
$body = @{
input = "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under `$200 per night"
stream = $false
} | ConvertTo-Json
Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json"
```
**Bash/curl (Linux/macOS):**
```bash
curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \
-d '{"input": "Find me hotels in Seattle for March 20-23, 2025 under $200 per night","stream":false}'
```
You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension.
The agent will use the `GetAvailableHotels` tool to search for available hotels matching your criteria.
## Deploying the Agent to Microsoft Foundry
**Preparation (required)**
Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project.
To deploy the hosted agent:
1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command.
2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs.
3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground.
**What the deploy flow does for you:**
- Creates or obtains an Azure Container Registry for the target project.
- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`).
- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime).
- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it.
## MSI Configuration in the Azure Portal
This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role.
To configure the Managed Identity:
1. In the Azure Portal, open the Foundry Project.
2. Select "Access control (IAM)" from the left-hand menu.
3. Click "Add" and choose "Add role assignment".
4. In the role selection, search for and select "Azure AI User", then click "Next".
5. For "Assign access to", choose "Managed identity".
6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select".
7. Click "Review + assign" to complete the assignment.
8. Allow a few minutes for the role assignment to propagate before running the application.
## Additional Resources
- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/)
@@ -0,0 +1,32 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
name: FoundrySingleAgent
displayName: "Foundry Single Agent with Local Tools"
description: >
A travel assistant agent that helps users find hotels in Seattle.
Demonstrates local C# tool execution - a key advantage of code-based
hosted agents over prompt agents.
metadata:
authors:
- Microsoft Agent Framework Team
tags:
- Azure AI AgentServer
- Microsoft Agent Framework
- Local Tools
- Travel Assistant
- Hotel Search
template:
kind: hosted
name: FoundrySingleAgent
protocols:
- protocol: responses
version: v1
environment_variables:
- name: AZURE_AI_PROJECT_ENDPOINT
value: ${AZURE_AI_PROJECT_ENDPOINT}
- name: MODEL_DEPLOYMENT_NAME
value: gpt-4o-mini
resources:
- name: "gpt-4o-mini"
kind: model
id: gpt-4o-mini
@@ -0,0 +1,52 @@
@host = http://localhost:8088
@endpoint = {{host}}/responses
### Health Check
GET {{host}}/readiness
### Simple hotel search - budget under $200
POST {{endpoint}}
Content-Type: application/json
{
"input": "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night",
"stream": false
}
### Hotel search with higher budget
POST {{endpoint}}
Content-Type: application/json
{
"input": "Find me hotels in Seattle for March 20-23, 2025 under $250 per night",
"stream": false
}
### Ask for recommendations without dates (agent should ask for clarification)
POST {{endpoint}}
Content-Type: application/json
{
"input": "What hotels do you recommend in Seattle?",
"stream": false
}
### Explicit input format
POST {{endpoint}}
Content-Type: application/json
{
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "I'm looking for a hotel in Seattle from 2025-04-01 to 2025-04-05, my budget is $150 per night maximum"
}
]
}
],
"stream": false
}
@@ -12,6 +12,8 @@ These samples demonstrate how to build and host AI agents using the [Azure AI Ag
| [`AgentWithHostedMCP`](./AgentWithHostedMCP/) | Hosted MCP server tool (Microsoft Learn search) |
| [`AgentWithTextSearchRag`](./AgentWithTextSearchRag/) | RAG with `TextSearchProvider` (Contoso Outdoors) |
| [`AgentsInWorkflows`](./AgentsInWorkflows/) | Sequential workflow pipeline (translation chain) |
| [`FoundryMultiAgent`](./FoundryMultiAgent/) | Multi-agent Writer-Reviewer workflow using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) |
| [`FoundrySingleAgent`](./FoundrySingleAgent/) | Single agent with local C# tool execution (hotel search) using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) |
## Common Prerequisites
@@ -23,7 +25,7 @@ Before running any sample, ensure you have:
### Authenticate with Azure CLI
All samples use `AzureCliCredential` for authentication. Make sure you're logged in:
All samples use `DefaultAzureCredential` for authentication, which automatically probes multiple credential sources (environment variables, managed identity, Azure CLI, etc.). For local development, the simplest approach is to authenticate via Azure CLI:
```powershell
az login
@@ -38,9 +40,9 @@ Most samples require one or more of these environment variables:
|----------|---------|-------------|
| `AZURE_OPENAI_ENDPOINT` | Most samples | Your Azure OpenAI resource endpoint URL |
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Most samples | Chat model deployment name (defaults to `gpt-4o-mini`) |
| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools | Azure AI Foundry project endpoint |
| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Azure AI Foundry project endpoint |
| `MCP_TOOL_CONNECTION_ID` | AgentWithTools | Foundry MCP tool connection name |
| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools | Chat model deployment name (defaults to `gpt-4o-mini`) |
| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Chat model deployment name (defaults to `gpt-4o-mini`) |
See each sample's README for the specific variables required.
@@ -39,7 +39,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <exception cref="InvalidOperationException">The agent with the specified name was not found.</exception>
/// <remarks>
/// When instantiating a <see cref="ChatClientAgent"/> by using an <see cref="AgentReference"/>, minimal information will be available about the agent in the instance level, and any logic that relies
/// on <see cref="AIAgent.GetService(Type, object?)"/> to retrieve information about the agent like <see cref="AgentVersion" /> will receive <see langword="null"/> as the result.
/// on <see cref="AIAgent.GetService{TService}(object?)"/> to retrieve information about the agent like <see cref="AgentVersion" /> will receive <see langword="null"/> as the result.
/// </remarks>
public static ChatClientAgent AsAIAgent(
this AIProjectClient aiProjectClient,
@@ -355,28 +355,27 @@ public static partial class AzureAIProjectChatClientExtensions
private static readonly ModelReaderWriterOptions s_modelWriterOptionsWire = new("W");
/// <summary>
/// Asynchronously retrieves an agent record by name using the Protocol method with user-agent header.
/// Asynchronously retrieves an agent record by name using the protocol method to inject user-agent headers.
/// </summary>
private static async Task<AgentRecord> GetAgentRecordByNameAsync(AIProjectClient aiProjectClient, string agentName, CancellationToken cancellationToken)
{
ClientResult protocolResponse = await aiProjectClient.Agents.GetAgentAsync(agentName, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
var rawResponse = protocolResponse.GetRawResponse();
AgentRecord? result = ModelReaderWriter.Read<AgentRecord>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsOpenAIContext.Default);
return ClientResult.FromOptionalValue(result, rawResponse).Value!
?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
return result ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
}
/// <summary>
/// Asynchronously creates an agent version using the Protocol method with user-agent header.
/// Asynchronously creates an agent version using the protocol method to inject user-agent headers.
/// </summary>
private static async Task<AgentVersion> CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
{
using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions, ModelReaderWriterOptions.Json, AzureAIProjectsContext.Default));
ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, protocolRequest, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
BinaryData serializedOptions = ModelReaderWriter.Write(creationOptions, s_modelWriterOptionsWire, AzureAIProjectsContext.Default);
BinaryContent content = BinaryContent.Create(serializedOptions);
ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, content, foundryFeatures: null, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
var rawResponse = protocolResponse.GetRawResponse();
AgentVersion? result = ModelReaderWriter.Read<AgentVersion>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsOpenAIContext.Default);
return ClientResult.FromValue(result, rawResponse).Value!;
return result ?? throw new InvalidOperationException($"Failed to create agent version for agent '{agentName}'.");
}
private static async Task<ChatClientAgent> CreateAIAgentAsync(
@@ -346,14 +346,14 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
{
TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty)
AIContent content = new()
{
RawRepresentation = assistantMessage
};
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
return new AgentResponseUpdate(ChatRole.Assistant, [content])
{
AgentId = this.Id,
ResponseId = assistantMessage.Data?.MessageId,
@@ -19,9 +19,10 @@ public static class AgentHostingServiceCollectionExtensions
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions)
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -30,7 +31,7 @@ public static class AgentHostingServiceCollectionExtensions
var chatClient = sp.GetRequiredService<IChatClient>();
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
});
}, lifetime);
}
/// <summary>
@@ -40,9 +41,10 @@ public static class AgentHostingServiceCollectionExtensions
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="chatClient">The chat client which the agent will use for inference.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient)
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -50,7 +52,7 @@ public static class AgentHostingServiceCollectionExtensions
{
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
});
}, lifetime);
}
/// <summary>
@@ -60,9 +62,10 @@ public static class AgentHostingServiceCollectionExtensions
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If <see langword="null"/>, a non-keyed service will be resolved.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey)
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -71,7 +74,7 @@ public static class AgentHostingServiceCollectionExtensions
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
});
}, lifetime);
}
/// <summary>
@@ -82,9 +85,10 @@ public static class AgentHostingServiceCollectionExtensions
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="description">A description of the agent.</param>
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If <see langword="null"/>, a non-keyed service will be resolved.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey)
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -93,7 +97,7 @@ public static class AgentHostingServiceCollectionExtensions
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions: instructions, name: key, description: description, tools: tools);
});
}, lifetime);
}
/// <summary>
@@ -102,15 +106,16 @@ public static class AgentHostingServiceCollectionExtensions
/// <param name="services">The service collection to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="createAgentDelegate">A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/>, <paramref name="name"/>, or <paramref name="createAgentDelegate"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when the agent factory delegate returns <see langword="null"/> or an agent whose <see cref="AIAgent.Name"/> does not match <paramref name="name"/>.</exception>
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNull(name);
Throw.IfNull(createAgentDelegate);
services.AddKeyedSingleton(name, (sp, key) =>
services.AddKeyedService(name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
@@ -122,8 +127,18 @@ public static class AgentHostingServiceCollectionExtensions
}
return agent;
});
}, lifetime);
return new HostedAgentBuilder(name, services);
return new HostedAgentBuilder(name, services, lifetime);
}
/// <summary>
/// Registers a keyed service with the specified lifetime.
/// </summary>
internal static void AddKeyedService<T>(this IServiceCollection services, object? serviceKey, Func<IServiceProvider, object?, T> factory, ServiceLifetime lifetime)
where T : class
{
var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, key) => factory(sp, key), lifetime);
services.Add(descriptor);
}
}
@@ -2,6 +2,7 @@
using System;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Shared.Diagnostics;
@@ -18,12 +19,13 @@ public static class HostApplicationBuilderAgentExtensions
/// <param name="builder">The host application builder to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The configured host application builder.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions)
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
return builder.Services.AddAIAgent(name, instructions);
return builder.Services.AddAIAgent(name, instructions, lifetime);
}
/// <summary>
@@ -33,13 +35,14 @@ public static class HostApplicationBuilderAgentExtensions
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="chatClient">The chat client which the agent will use for inference.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The configured host application builder.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient)
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
Throw.IfNullOrEmpty(name);
return builder.Services.AddAIAgent(name, instructions, chatClient);
return builder.Services.AddAIAgent(name, instructions, chatClient, lifetime);
}
/// <summary>
@@ -50,13 +53,14 @@ public static class HostApplicationBuilderAgentExtensions
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="description">A description of the agent.</param>
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The configured host application builder.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey)
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
Throw.IfNullOrEmpty(name);
return builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey);
return builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey, lifetime);
}
/// <summary>
@@ -66,12 +70,13 @@ public static class HostApplicationBuilderAgentExtensions
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The configured host application builder.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey)
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey);
return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey, lifetime);
}
/// <summary>
@@ -80,12 +85,13 @@ public static class HostApplicationBuilderAgentExtensions
/// <param name="builder">The host application builder to configure.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="createAgentDelegate">A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>The configured host application builder.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="createAgentDelegate"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when the agent factory delegate returns null or an invalid AI agent instance.</exception>
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
return builder.Services.AddAIAgent(name, createAgentDelegate);
return builder.Services.AddAIAgent(name, createAgentDelegate, lifetime);
}
}
@@ -19,19 +19,20 @@ public static class HostApplicationBuilderWorkflowExtensions
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
/// <param name="name">The unique name for the workflow.</param>
/// <param name="createWorkflowDelegate">A factory function that creates the <see cref="Workflow"/> instance. The function receives the service provider and workflow name as parameters.</param>
/// <param name="lifetime">The DI service lifetime for the workflow registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>An <see cref="IHostedWorkflowBuilder"/> that can be used to further configure the workflow.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="createWorkflowDelegate"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is empty.</exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the factory delegate returns null or a workflow with a name that doesn't match the expected name.
/// </exception>
public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, Workflow> createWorkflowDelegate)
public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, Workflow> createWorkflowDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
Throw.IfNull(name);
Throw.IfNull(createWorkflowDelegate);
builder.Services.AddKeyedSingleton(name, (sp, key) =>
builder.Services.AddKeyedService(name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
@@ -43,7 +44,7 @@ public static class HostApplicationBuilderWorkflowExtensions
}
return workflow;
});
}, lifetime);
return new HostedWorkflowBuilder(name, builder);
}
@@ -9,15 +9,17 @@ internal sealed class HostedAgentBuilder : IHostedAgentBuilder
{
public string Name { get; }
public IServiceCollection ServiceCollection { get; }
public ServiceLifetime Lifetime { get; }
public HostedAgentBuilder(string name, IHostApplicationBuilder builder)
: this(name, builder.Services)
public HostedAgentBuilder(string name, IHostApplicationBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton)
: this(name, builder.Services, lifetime)
{
}
public HostedAgentBuilder(string name, IServiceCollection serviceCollection)
public HostedAgentBuilder(string name, IServiceCollection serviceCollection, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
this.Name = name;
this.ServiceCollection = serviceCollection;
this.Lifetime = lifetime;
}
}
@@ -42,17 +42,19 @@ public static class HostedAgentBuilderExtensions
/// <param name="builder">The host agent builder to configure.</param>
/// <param name="createAgentSessionStore">A factory function that creates an agent session store instance using the provided service provider and agent
/// name.</param>
/// <param name="lifetime">The DI service lifetime for the session store registration. Defaults to <see cref="ServiceLifetime.Singleton"/>
/// because session stores persist conversation state across requests and are consumed independently of the agent's lifetime.</param>
/// <returns>The same host agent builder instance, enabling further configuration.</returns>
public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, AgentSessionStore> createAgentSessionStore)
public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, AgentSessionStore> createAgentSessionStore, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, key) =>
builder.ServiceCollection.AddKeyedService(builder.Name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
Throw.IfNullOrEmpty(keyString);
return createAgentSessionStore(sp, keyString) ??
throw new InvalidOperationException($"The agent session store factory did not return a valid {nameof(AgentSessionStore)} instance for key '{keyString}'.");
});
}, lifetime);
return builder;
}
@@ -98,13 +100,39 @@ public static class HostedAgentBuilderExtensions
/// </summary>
/// <param name="builder">The hosted agent builder.</param>
/// <param name="factory">A factory function that creates a AI tool using the provided service provider.</param>
public static IHostedAgentBuilder WithAITool(this IHostedAgentBuilder builder, Func<IServiceProvider, AITool> factory)
/// <param name="lifetime">The DI service lifetime for the tool registration. If <see langword="null"/>, the agent's lifetime is used.</param>
/// <returns>The same <see cref="IHostedAgentBuilder"/> instance so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="factory"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the effective tool lifetime is shorter than the agent's lifetime, which would cause a captive dependency.
/// For example, a singleton agent cannot use scoped or transient tools.
/// </exception>
public static IHostedAgentBuilder WithAITool(this IHostedAgentBuilder builder, Func<IServiceProvider, AITool> factory, ServiceLifetime? lifetime = null)
{
Throw.IfNull(builder);
Throw.IfNull(factory);
builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, name) => factory(sp));
var effectiveLifetime = lifetime ?? builder.Lifetime;
ValidateToolLifetime(builder.Lifetime, effectiveLifetime);
builder.ServiceCollection.AddKeyedService(builder.Name, (sp, name) => factory(sp), effectiveLifetime);
return builder;
}
/// <summary>
/// Validates that the tool lifetime is compatible with the agent lifetime.
/// A tool's lifetime must be at least as long as the agent's lifetime to prevent captive dependency issues.
/// </summary>
internal static void ValidateToolLifetime(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
{
// ServiceLifetime enum: Singleton=0, Scoped=1, Transient=2
// A higher value means a shorter lifetime.
if (toolLifetime > agentLifetime)
{
throw new InvalidOperationException(
$"A tool with lifetime '{toolLifetime}' cannot be registered for an agent with lifetime '{agentLifetime}'. " +
"The tool's lifetime must be at least as long as the agent's lifetime to avoid captive dependency issues.");
}
}
}
@@ -14,22 +14,24 @@ public static class HostedWorkflowBuilderExtensions
/// Registers the workflow as an AI agent in the dependency injection container.
/// </summary>
/// <param name="builder">The <see cref="IHostedWorkflowBuilder"/> instance to extend.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>An <see cref="IHostedAgentBuilder"/> that can be used to further configure the agent.</returns>
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder)
=> builder.AddAsAIAgent(name: null);
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton)
=> builder.AddAsAIAgent(name: null, lifetime: lifetime);
/// <summary>
/// Registers the workflow as an AI agent in the dependency injection container.
/// </summary>
/// <param name="builder">The <see cref="IHostedWorkflowBuilder"/> instance to extend.</param>
/// <param name="name">The optional name for the AI agent. If not specified, the workflow name is used.</param>
/// <param name="lifetime">The DI service lifetime for the agent registration. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>An <see cref="IHostedAgentBuilder"/> that can be used to further configure the agent.</returns>
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name)
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
var workflowName = builder.Name;
var agentName = name ?? workflowName;
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
sp.GetRequiredKeyedService<Workflow>(workflowName).AsAIAgent(name: key));
sp.GetRequiredKeyedService<Workflow>(workflowName).AsAIAgent(name: key), lifetime);
}
}
@@ -18,4 +18,9 @@ public interface IHostedAgentBuilder
/// Gets the service collection for configuration.
/// </summary>
IServiceCollection ServiceCollection { get; }
/// <summary>
/// Gets the DI service lifetime used for the agent registration.
/// </summary>
ServiceLifetime Lifetime { get; }
}
@@ -747,9 +747,15 @@ public sealed partial class ChatClientAgent : AIAgent
{
// The agent has a ChatHistoryProvider configured, but the service returned a conversation id,
// meaning the service manages chat history server-side. Both cannot be used simultaneously.
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true)
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true
&& this._logger.IsEnabled(LogLevel.Warning))
{
this._logger.LogAgentChatClientHistoryProviderConflict(nameof(ChatClientAgentSession.ConversationId), nameof(this.ChatHistoryProvider), this.Id, this.GetLoggingAgentName());
var loggingAgentName = this.GetLoggingAgentName();
this._logger.LogAgentChatClientHistoryProviderConflict(
nameof(ChatClientAgentSession.ConversationId),
nameof(this.ChatHistoryProvider),
this.Id,
loggingAgentName);
}
if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true)
@@ -350,36 +350,38 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo
string? userId = searchScope.UserId;
string? sessionId = searchScope.SessionId;
Expression<Func<Dictionary<string, object?>, bool>>? filter = null;
// Build a combined filter using a single shared parameter to avoid expression tree
// scoping issues when multiple filters are combined with AndAlso.
ParameterExpression parameter = Expression.Parameter(typeof(Dictionary<string, object?>), "x");
Expression? filterBody = null;
if (applicationId != null)
{
filter = x => (string?)x[ApplicationIdField] == applicationId;
filterBody = RebindFilterBody(x => (string?)x[ApplicationIdField] == applicationId, parameter);
}
if (agentId != null)
{
Expression<Func<Dictionary<string, object?>, bool>> agentIdFilter = x => (string?)x[AgentIdField] == agentId;
filter = filter == null ? agentIdFilter : Expression.Lambda<Func<Dictionary<string, object?>, bool>>(
Expression.AndAlso(filter.Body, agentIdFilter.Body),
filter.Parameters);
Expression body = RebindFilterBody(x => (string?)x[AgentIdField] == agentId, parameter);
filterBody = filterBody == null ? body : Expression.AndAlso(filterBody, body);
}
if (userId != null)
{
Expression<Func<Dictionary<string, object?>, bool>> userIdFilter = x => (string?)x[UserIdField] == userId;
filter = filter == null ? userIdFilter : Expression.Lambda<Func<Dictionary<string, object?>, bool>>(
Expression.AndAlso(filter.Body, userIdFilter.Body),
filter.Parameters);
Expression body = RebindFilterBody(x => (string?)x[UserIdField] == userId, parameter);
filterBody = filterBody == null ? body : Expression.AndAlso(filterBody, body);
}
if (sessionId != null)
{
Expression<Func<Dictionary<string, object?>, bool>> sessionIdFilter = x => (string?)x[SessionIdField] == sessionId;
filter = filter == null ? sessionIdFilter : Expression.Lambda<Func<Dictionary<string, object?>, bool>>(
Expression.AndAlso(filter.Body, sessionIdFilter.Body),
filter.Parameters);
Expression body = RebindFilterBody(x => (string?)x[SessionIdField] == sessionId, parameter);
filterBody = filterBody == null ? body : Expression.AndAlso(filterBody, body);
}
Expression<Func<Dictionary<string, object?>, bool>>? filter = filterBody != null
? Expression.Lambda<Func<Dictionary<string, object?>, bool>>(filterBody, parameter)
: null;
// Use search to find relevant messages
var searchResults = collection.SearchAsync(
queryText,
@@ -467,6 +469,27 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo
private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "<redacted>";
/// <summary>
/// Rebinds a filter expression's body to use the specified shared parameter,
/// replacing the original lambda parameter so that multiple filters can be safely
/// combined with <see cref="Expression.AndAlso(Expression, Expression)"/>.
/// </summary>
private static Expression RebindFilterBody(
Expression<Func<Dictionary<string, object?>, bool>> filter,
ParameterExpression sharedParameter)
{
return new ParameterReplacer(filter.Parameters[0], sharedParameter).Visit(filter.Body);
}
/// <summary>
/// An <see cref="ExpressionVisitor"/> that replaces one <see cref="ParameterExpression"/> with another.
/// </summary>
private sealed class ParameterReplacer(ParameterExpression original, ParameterExpression replacement) : ExpressionVisitor
{
protected override Expression VisitParameter(ParameterExpression node)
=> node == original ? replacement : base.VisitParameter(node);
}
/// <summary>
/// Represents the state of a <see cref="ChatHistoryMemoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
/// </summary>
@@ -17,8 +17,9 @@ namespace Microsoft.Agents.AI;
/// </summary>
/// <remarks>
/// Searches directories recursively (up to <see cref="MaxSearchDepth"/> levels) for SKILL.md files.
/// Each file is validated for YAML frontmatter and resource integrity. Invalid skills are excluded
/// with logged warnings. Resource paths are checked against path traversal and symlink escape attacks.
/// Each file is validated for YAML frontmatter. Resource files are discovered by scanning the skill
/// directory for files with matching extensions. Invalid resources are skipped with logged warnings.
/// Resource paths are checked against path traversal and symlink escape attacks.
/// </remarks>
internal sealed partial class FileAgentSkillLoader
{
@@ -33,33 +34,34 @@ internal sealed partial class FileAgentSkillLoader
// Example: "---\nname: foo\n---\nBody" → Group 1: "name: foo\n"
private static readonly Regex s_frontmatterRegex = new(@"\A\uFEFF?^---\s*$(.+?)^---\s*$", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(5));
// Matches markdown links to local resource files. Group 1 = relative file path.
// Supports optional ./ or ../ prefixes; excludes URLs (no ":" in the path character class).
// Intentionally conservative: only matches paths with word characters, hyphens, dots,
// and forward slashes. Paths with spaces or special characters are not supported.
// Examples: [doc](refs/FAQ.md) → "refs/FAQ.md", [s](./s.json) → "./s.json",
// [p](../shared/doc.txt) → "../shared/doc.txt"
private static readonly Regex s_resourceLinkRegex = new(@"\[.*?\]\((\.?\.?/?[\w][\w\-./]*\.\w+)\)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
// Matches YAML "key: value" lines. Group 1 = key, Group 2 = quoted value, Group 3 = unquoted value.
// Accepts single or double quotes; the lazy quantifier trims trailing whitespace on unquoted values.
// Examples: "name: foo" → (name, _, foo), "name: 'foo bar'" → (name, foo bar, _),
// "description: \"A skill\"" → (description, A skill, _)
private static readonly Regex s_yamlKeyValueRegex = new(@"^\s*(\w+)\s*:\s*(?:[""'](.+?)[""']|(.+?))\s*$", RegexOptions.Multiline | RegexOptions.Compiled, TimeSpan.FromSeconds(5));
// Validates skill names: lowercase letters, numbers, and hyphens only; must not start or end with a hyphen.
// Examples: "my-skill" âś“, "skill123" âś“, "-bad" âś—, "bad-" âś—, "Bad" âś—
private static readonly Regex s_validNameRegex = new(@"^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$", RegexOptions.Compiled);
// Validates skill names: lowercase letters, numbers, and hyphens only;
// must not start or end with a hyphen; must not contain consecutive hyphens.
// Examples: "my-skill" âś“, "skill123" âś“, "-bad" âś—, "bad-" âś—, "Bad" âś—, "my--skill" âś—
private static readonly Regex s_validNameRegex = new("^[a-z0-9]([a-z0-9]*-[a-z0-9])*[a-z0-9]*$", RegexOptions.Compiled);
private readonly ILogger _logger;
private readonly HashSet<string> _allowedResourceExtensions;
/// <summary>
/// Initializes a new instance of the <see cref="FileAgentSkillLoader"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
internal FileAgentSkillLoader(ILogger logger)
/// <param name="allowedResourceExtensions">File extensions to recognize as skill resources. When <see langword="null"/>, defaults are used.</param>
internal FileAgentSkillLoader(ILogger logger, IEnumerable<string>? allowedResourceExtensions = null)
{
this._logger = logger;
ValidateExtensions(allowedResourceExtensions);
this._allowedResourceExtensions = new HashSet<string>(
allowedResourceExtensions ?? [".md", ".json", ".yaml", ".yml", ".csv", ".xml", ".txt"],
StringComparer.OrdinalIgnoreCase);
}
/// <summary>
@@ -183,9 +185,9 @@ internal sealed partial class FileAgentSkillLoader
}
}
private FileAgentSkill? ParseSkillFile(string skillDirectoryPath)
private FileAgentSkill? ParseSkillFile(string skillDirectoryFullPath)
{
string skillFilePath = Path.Combine(skillDirectoryPath, SkillFileName);
string skillFilePath = Path.Combine(skillDirectoryFullPath, SkillFileName);
string content = File.ReadAllText(skillFilePath, Encoding.UTF8);
@@ -194,17 +196,12 @@ internal sealed partial class FileAgentSkillLoader
return null;
}
List<string> resourceNames = ExtractResourcePaths(body);
if (!this.ValidateResources(skillDirectoryPath, resourceNames, frontmatter.Name))
{
return null;
}
List<string> resourceNames = this.DiscoverResourceFiles(skillDirectoryFullPath, frontmatter.Name);
return new FileAgentSkill(
frontmatter: frontmatter,
body: body,
sourcePath: skillDirectoryPath,
sourcePath: skillDirectoryFullPath,
resourceNames: resourceNames);
}
@@ -248,7 +245,22 @@ internal sealed partial class FileAgentSkillLoader
if (name.Length > MaxNameLength || !s_validNameRegex.IsMatch(name))
{
LogInvalidFieldValue(this._logger, skillFilePath, "name", $"Must be {MaxNameLength} characters or fewer, using only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen.");
LogInvalidFieldValue(this._logger, skillFilePath, "name", $"Must be {MaxNameLength} characters or fewer, using only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen or contain consecutive hyphens.");
return false;
}
// skillFilePath is e.g. "/skills/my-skill/SKILL.md".
// GetDirectoryName strips the filename → "/skills/my-skill".
// GetFileName then extracts the last segment → "my-skill".
// This gives us the skill's parent directory name to validate against the frontmatter name.
string directoryName = Path.GetFileName(Path.GetDirectoryName(skillFilePath)) ?? string.Empty;
if (!string.Equals(name, directoryName, StringComparison.Ordinal))
{
if (this._logger.IsEnabled(LogLevel.Error))
{
LogNameDirectoryMismatch(this._logger, SanitizePathForLog(skillFilePath), name, SanitizePathForLog(directoryName));
}
return false;
}
@@ -270,34 +282,84 @@ internal sealed partial class FileAgentSkillLoader
return true;
}
private bool ValidateResources(string skillDirectoryPath, List<string> resourceNames, string skillName)
/// <summary>
/// Scans a skill directory for resource files matching the configured extensions.
/// </summary>
/// <remarks>
/// Recursively walks <paramref name="skillDirectoryFullPath"/> and collects files whose extension
/// matches <see cref="_allowedResourceExtensions"/>, excluding <c>SKILL.md</c> itself. Each candidate
/// is validated against path-traversal and symlink-escape checks; unsafe files are skipped with
/// a warning.
/// </remarks>
private List<string> DiscoverResourceFiles(string skillDirectoryFullPath, string skillName)
{
string normalizedSkillPath = Path.GetFullPath(skillDirectoryPath) + Path.DirectorySeparatorChar;
string normalizedSkillDirectoryFullPath = skillDirectoryFullPath + Path.DirectorySeparatorChar;
foreach (string resourceName in resourceNames)
var resources = new List<string>();
#if NET
var enumerationOptions = new EnumerationOptions
{
string fullPath = Path.GetFullPath(Path.Combine(skillDirectoryPath, resourceName));
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.ReparsePoint,
};
if (!IsPathWithinDirectory(fullPath, normalizedSkillPath))
foreach (string filePath in Directory.EnumerateFiles(skillDirectoryFullPath, "*", enumerationOptions))
#else
foreach (string filePath in Directory.EnumerateFiles(skillDirectoryFullPath, "*", SearchOption.AllDirectories))
#endif
{
string fileName = Path.GetFileName(filePath);
// Exclude SKILL.md itself
if (string.Equals(fileName, SkillFileName, StringComparison.OrdinalIgnoreCase))
{
LogResourcePathTraversal(this._logger, skillName, resourceName);
return false;
continue;
}
if (!File.Exists(fullPath))
// Filter by extension
string extension = Path.GetExtension(filePath);
if (string.IsNullOrEmpty(extension) || !this._allowedResourceExtensions.Contains(extension))
{
LogMissingResource(this._logger, skillName, resourceName);
return false;
if (this._logger.IsEnabled(LogLevel.Debug))
{
LogResourceSkippedExtension(this._logger, skillName, SanitizePathForLog(filePath), extension);
}
continue;
}
if (HasSymlinkInPath(fullPath, normalizedSkillPath))
// Normalize the enumerated path to guard against non-canonical forms
// (redundant separators, 8.3 short names, etc.) that would produce
// malformed relative resource names.
string resolvedFilePath = Path.GetFullPath(filePath);
// Path containment check
if (!IsPathWithinDirectory(resolvedFilePath, normalizedSkillDirectoryFullPath))
{
LogResourceSymlinkEscape(this._logger, skillName, resourceName);
return false;
if (this._logger.IsEnabled(LogLevel.Warning))
{
LogResourcePathTraversal(this._logger, skillName, SanitizePathForLog(filePath));
}
continue;
}
// Symlink check
if (HasSymlinkInPath(resolvedFilePath, normalizedSkillDirectoryFullPath))
{
if (this._logger.IsEnabled(LogLevel.Warning))
{
LogResourceSymlinkEscape(this._logger, skillName, SanitizePathForLog(filePath));
}
continue;
}
// Compute relative path and normalize to forward slashes
string relativePath = resolvedFilePath.Substring(normalizedSkillDirectoryFullPath.Length);
resources.Add(NormalizeResourcePath(relativePath));
}
return true;
return resources;
}
/// <summary>
@@ -336,22 +398,6 @@ internal sealed partial class FileAgentSkillLoader
return false;
}
private static List<string> ExtractResourcePaths(string content)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var paths = new List<string>();
foreach (Match m in s_resourceLinkRegex.Matches(content))
{
string path = NormalizeResourcePath(m.Groups[1].Value);
if (seen.Add(path))
{
paths.Add(path);
}
}
return paths;
}
/// <summary>
/// Normalizes a relative resource path by trimming a leading <c>./</c> prefix and replacing
/// backslashes with forward slashes so that <c>./refs/doc.md</c> and <c>refs/doc.md</c> are
@@ -372,6 +418,43 @@ internal sealed partial class FileAgentSkillLoader
return path;
}
/// <summary>
/// Replaces control characters in a file path with '?' to prevent log injection
/// via crafted filenames (e.g., filenames containing newlines on Linux).
/// </summary>
private static string SanitizePathForLog(string path)
{
char[]? chars = null;
for (int i = 0; i < path.Length; i++)
{
if (char.IsControl(path[i]))
{
chars ??= path.ToCharArray();
chars[i] = '?';
}
}
return chars is null ? path : new string(chars);
}
private static void ValidateExtensions(IEnumerable<string>? extensions)
{
if (extensions is null)
{
return;
}
foreach (string ext in extensions)
{
if (string.IsNullOrWhiteSpace(ext) || !ext.StartsWith(".", StringComparison.Ordinal))
{
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
throw new ArgumentException($"Each extension must start with '.'. Invalid value: '{ext}'", nameof(FileAgentSkillsProviderOptions.AllowedResourceExtensions));
#pragma warning restore CA2208 // Instantiate argument exceptions correctly
}
}
}
[LoggerMessage(LogLevel.Information, "Discovered {Count} potential skills")]
private static partial void LogSkillsDiscovered(ILogger logger, int count);
@@ -390,18 +473,21 @@ internal sealed partial class FileAgentSkillLoader
[LoggerMessage(LogLevel.Error, "SKILL.md at '{SkillFilePath}' has an invalid '{FieldName}' value: {Reason}")]
private static partial void LogInvalidFieldValue(ILogger logger, string skillFilePath, string fieldName, string reason);
[LoggerMessage(LogLevel.Warning, "Excluding skill '{SkillName}': referenced resource '{ResourceName}' does not exist")]
private static partial void LogMissingResource(ILogger logger, string skillName, string resourceName);
[LoggerMessage(LogLevel.Error, "SKILL.md at '{SkillFilePath}': skill name '{SkillName}' does not match parent directory name '{DirectoryName}'")]
private static partial void LogNameDirectoryMismatch(ILogger logger, string skillFilePath, string skillName, string directoryName);
[LoggerMessage(LogLevel.Warning, "Excluding skill '{SkillName}': resource '{ResourceName}' references a path outside the skill directory")]
private static partial void LogResourcePathTraversal(ILogger logger, string skillName, string resourceName);
[LoggerMessage(LogLevel.Warning, "Skipping resource in skill '{SkillName}': '{ResourcePath}' references a path outside the skill directory")]
private static partial void LogResourcePathTraversal(ILogger logger, string skillName, string resourcePath);
[LoggerMessage(LogLevel.Warning, "Duplicate skill name '{SkillName}': skill from '{NewPath}' skipped in favor of existing skill from '{ExistingPath}'")]
private static partial void LogDuplicateSkillName(ILogger logger, string skillName, string newPath, string existingPath);
[LoggerMessage(LogLevel.Warning, "Excluding skill '{SkillName}': resource '{ResourceName}' is a symlink that resolves outside the skill directory")]
private static partial void LogResourceSymlinkEscape(ILogger logger, string skillName, string resourceName);
[LoggerMessage(LogLevel.Warning, "Skipping resource in skill '{SkillName}': '{ResourcePath}' is a symlink that resolves outside the skill directory")]
private static partial void LogResourceSymlinkEscape(ILogger logger, string skillName, string resourcePath);
[LoggerMessage(LogLevel.Information, "Reading resource '{FileName}' from skill '{SkillName}'")]
private static partial void LogResourceReading(ILogger logger, string fileName, string skillName);
[LoggerMessage(LogLevel.Debug, "Skipping file '{FilePath}' in skill '{SkillName}': extension '{Extension}' is not in the allowed list")]
private static partial void LogResourceSkippedExtension(ILogger logger, string skillName, string filePath, string extension);
}
@@ -88,7 +88,7 @@ public sealed partial class FileAgentSkillsProvider : AIContextProvider
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<FileAgentSkillsProvider>();
this._loader = new FileAgentSkillLoader(this._logger);
this._loader = new FileAgentSkillLoader(this._logger, options?.AllowedResourceExtensions);
this._skills = this._loader.DiscoverAndLoadSkills(skillPaths);
this._skillsInstructionPrompt = BuildSkillsInstructionPrompt(options, this._skills);
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
@@ -17,4 +18,15 @@ public sealed class FileAgentSkillsProviderOptions
/// When <see langword="null"/>, a default template is used.
/// </summary>
public string? SkillsInstructionPrompt { get; set; }
/// <summary>
/// Gets or sets the file extensions recognized as discoverable skill resources.
/// Each value must start with a <c>'.'</c> character (for example, <c>.md</c>), and
/// extension comparisons are performed in a case-insensitive manner.
/// Files in the skill directory (and its subdirectories) whose extension matches
/// one of these values will be automatically discovered as resources.
/// When <see langword="null"/>, a default set of extensions is used
/// (<c>.md</c>, <c>.json</c>, <c>.yaml</c>, <c>.yml</c>, <c>.csv</c>, <c>.xml</c>, <c>.txt</c>).
/// </summary>
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
}
@@ -0,0 +1,9 @@
# Integration Tests Azure Credentials
Adds a helper for loading Azure credentials in integration tests.
```xml
<PropertyGroup>
<InjectSharedIntegrationTestAzureCredentialsCode>true</InjectSharedIntegrationTestAzureCredentialsCode>
</PropertyGroup>
```
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0005 // This is required in some projects and not in others.
using System;
#pragma warning restore IDE0005
using Azure.Identity;
namespace Shared.IntegrationTests;
/// <summary>
/// Provides credential instances for integration tests with
/// increased timeouts to avoid CI pipeline authentication failures.
/// </summary>
internal static class TestAzureCliCredentials
{
/// <summary>
/// The default timeout for Azure CLI credential operations.
/// Increased from the default (~13s) to accommodate CI pipeline latency.
/// </summary>
private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(60);
/// <summary>
/// Creates a new <see cref="AzureCliCredential"/> with an increased process timeout
/// suitable for CI environments.
/// </summary>
public static AzureCliCredential CreateAzureCliCredential() =>
new(new AzureCliCredentialOptions { ProcessTimeout = s_processTimeout });
}
@@ -15,11 +15,15 @@ public abstract class AgentTests<TAgentFixture>(Func<TAgentFixture> createAgentF
{
protected TAgentFixture Fixture { get; private set; } = default!;
public Task InitializeAsync()
public async ValueTask InitializeAsync()
{
this.Fixture = createAgentFixture();
return this.Fixture.InitializeAsync();
await this.Fixture.InitializeAsync();
}
public Task DisposeAsync() => this.Fixture.DisposeAsync();
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
await this.Fixture.DisposeAsync();
}
}
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CS8793</NoWarn>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
</PropertyGroup>
@@ -1,26 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllChatClientRunStreaming(Func<AnthropicChatCompletionFixture> func) : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: true));
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: false));
public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: false));
@@ -1,30 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllChatClientAgentRun(Func<AnthropicChatCompletionFixture> func) : ChatClientAgentRunTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
public class AnthropicBetaChatCompletionChatClientAgentRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: true));
: ChatClientAgentRunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: true));
: ChatClientAgentRunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionChatClientAgentRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: false));
: ChatClientAgentRunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionChatClientAgentReasoningRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: false));
: ChatClientAgentRunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: false));
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -102,9 +103,15 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture
// Chat Completion does not require/support deleting sessions, so this is a no-op.
Task.CompletedTask;
public async Task InitializeAsync() =>
public async ValueTask InitializeAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty);
this._agent = await this.CreateChatClientAgentAsync();
}
public Task DisposeAsync() =>
Task.CompletedTask;
public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
return default;
}
}
@@ -1,37 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllRunStreaming(Func<AnthropicChatCompletionFixture> func) : RunStreamingTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task SessionMaintainsHistoryAsync() => base.SessionMaintainsHistoryAsync();
}
public class AnthropicBetaChatCompletionRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: true));
: RunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionReasoningRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: true));
: RunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: false));
: RunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionReasoningRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: false));
: RunStreamingTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: false));
@@ -1,37 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllRun(Func<AnthropicChatCompletionFixture> func) : RunTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task SessionMaintainsHistoryAsync() => base.SessionMaintainsHistoryAsync();
}
public class AnthropicBetaChatCompletionRunTests()
: SkipAllRun(() => new(useReasoningChatModel: false, useBeta: true));
: RunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionReasoningRunTests()
: SkipAllRun(() => new(useReasoningChatModel: true, useBeta: true));
: RunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionRunTests()
: SkipAllRun(() => new(useReasoningChatModel: false, useBeta: false));
: RunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionReasoningRunTests()
: SkipAllRun(() => new(useReasoningChatModel: true, useBeta: false));
: RunTests<AnthropicChatCompletionFixture>(() => new(useReasoningChatModel: true, useBeta: false));
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
@@ -22,9 +22,11 @@ public sealed class AnthropicSkillsIntegrationTests
// All tests for Anthropic are intended to be ran locally as the CI pipeline for Anthropic is not setup.
private const string SkipReason = "Integrations tests for local execution only";
[Fact(Skip = SkipReason)]
[Fact]
public async Task CreateAgentWithPptxSkillAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty);
// Arrange
AnthropicClient anthropicClient = new() { ApiKey = TestConfiguration.GetRequiredValue(TestSettings.AnthropicApiKey) };
string model = TestConfiguration.GetRequiredValue(TestSettings.AnthropicChatModelName);
@@ -51,9 +53,11 @@ public sealed class AnthropicSkillsIntegrationTests
Assert.NotEmpty(response.Text);
}
[Fact(Skip = SkipReason)]
[Fact]
public async Task ListAnthropicManagedSkillsAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty);
// Arrange
AnthropicClient anthropicClient = new() { ApiKey = TestConfiguration.GetRequiredValue(TestSettings.AnthropicApiKey) };
@@ -9,10 +9,10 @@ namespace AzureAI.IntegrationTests;
public class AIProjectClientAgentRunStreamingPreviousResponseTests() : RunStreamingTests<AIProjectClientFixture>(() => new())
{
[Fact(Skip = "No messages is not supported")]
public override Task RunWithNoMessageDoesNotFailAsync()
{
return Task.CompletedTask;
Assert.Skip("No messages is not supported");
return base.RunWithNoMessageDoesNotFailAsync();
}
}
@@ -24,9 +24,9 @@ public class AIProjectClientAgentRunStreamingConversationTests() : RunTests<AIPr
return new ChatClientAgentRunOptions(new() { ConversationId = conversationId });
};
[Fact(Skip = "No messages is not supported")]
public override Task RunWithNoMessageDoesNotFailAsync()
{
return Task.CompletedTask;
Assert.Skip("No messages is not supported");
return base.RunWithNoMessageDoesNotFailAsync();
}
}
@@ -9,10 +9,10 @@ namespace AzureAI.IntegrationTests;
public class AIProjectClientAgentRunPreviousResponseTests() : RunTests<AIProjectClientFixture>(() => new())
{
[Fact(Skip = "No messages is not supported")]
public override Task RunWithNoMessageDoesNotFailAsync()
{
return Task.CompletedTask;
Assert.Skip("No messages is not supported");
return base.RunWithNoMessageDoesNotFailAsync();
}
}
@@ -24,9 +24,9 @@ public class AIProjectClientAgentRunConversationTests() : RunTests<AIProjectClie
return new ChatClientAgentRunOptions(new() { ConversationId = conversationId });
};
[Fact(Skip = "No messages is not supported")]
public override Task RunWithNoMessageDoesNotFailAsync()
{
return Task.CompletedTask;
Assert.Skip("No messages is not supported");
return base.RunWithNoMessageDoesNotFailAsync();
}
}
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
@@ -66,17 +65,23 @@ public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRu
Assert.Equal("Paris", response.Result.Name);
}
[Fact(Skip = NotSupported)]
public override Task RunWithGenericTypeReturnsExpectedResultAsync() =>
base.RunWithGenericTypeReturnsExpectedResultAsync();
public override Task RunWithGenericTypeReturnsExpectedResultAsync()
{
Assert.Skip(NotSupported);
return base.RunWithGenericTypeReturnsExpectedResultAsync();
}
[Fact(Skip = NotSupported)]
public override Task RunWithResponseFormatReturnsExpectedResultAsync() =>
base.RunWithResponseFormatReturnsExpectedResultAsync();
public override Task RunWithResponseFormatReturnsExpectedResultAsync()
{
Assert.Skip(NotSupported);
return base.RunWithResponseFormatReturnsExpectedResultAsync();
}
[Fact(Skip = NotSupported)]
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() =>
base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync()
{
Assert.Skip(NotSupported);
return base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
}
}
/// <summary>
@@ -84,7 +89,7 @@ public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRu
/// </summary>
public class AIProjectClientStructuredOutputFixture<T> : AIProjectClientFixture
{
public override Task InitializeAsync()
public override async ValueTask InitializeAsync()
{
var agentOptions = new ChatClientAgentOptions
{
@@ -94,6 +99,6 @@ public class AIProjectClientStructuredOutputFixture<T> : AIProjectClientFixture
},
};
return this.InitializeAsync(agentOptions);
await this.InitializeAsync(agentOptions);
}
}
@@ -7,9 +7,9 @@ namespace AzureAI.IntegrationTests;
public class AIProjectClientChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<AIProjectClientFixture>(() => new())
{
[Fact(Skip = "No messages is not supported")]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
{
return Task.CompletedTask;
Assert.Skip("No messages is not supported");
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
}
@@ -7,9 +7,9 @@ namespace AzureAI.IntegrationTests;
public class AIProjectClientChatClientAgentRunTests() : ChatClientAgentRunTests<AIProjectClientFixture>(() => new())
{
[Fact(Skip = "No messages is not supported")]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
{
return Task.CompletedTask;
Assert.Skip("No messages is not supported");
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
}
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Files;
@@ -17,7 +16,7 @@ namespace AzureAI.IntegrationTests;
public class AIProjectClientCreateTests
{
private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential());
private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
@@ -8,7 +8,6 @@ using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
@@ -156,25 +155,27 @@ public class AIProjectClientFixture : IChatClientAgentFixture
}
}
public Task DisposeAsync()
public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
if (this._client is not null && this._agent is not null)
{
return this._client.Agents.DeleteAgentAsync(this._agent.Name);
return new ValueTask(this._client.Agents.DeleteAgentAsync(this._agent.Name));
}
return Task.CompletedTask;
return default;
}
public virtual async Task InitializeAsync()
public virtual async ValueTask InitializeAsync()
{
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential());
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
this._agent = await this.CreateChatClientAgentAsync();
}
public async Task InitializeAsync(ChatClientAgentOptions options)
{
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential());
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
this._agent = await this.CreateChatClientAgentAsync(options);
}
}
@@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CS8793</NoWarn>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
</PropertyGroup>
<ItemGroup>
@@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CS8793</NoWarn>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
</PropertyGroup>
<ItemGroup>
@@ -6,7 +6,6 @@ using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
@@ -15,7 +14,7 @@ namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsPersistentCreateTests
{
private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential());
private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential());
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
@@ -132,10 +131,15 @@ public class AzureAIAgentsPersistentCreateTests
}
}
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithFoundryOptionsAsync")]
public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism)
[Fact]
public Task CreateAgent_CreatesAgentWithCodeInterpreter_ChatClientAgentOptionsAsync()
=> this.CreateAgent_CreatesAgentWithCodeInterpreterAsync("CreateWithChatClientAgentOptionsAsync");
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public Task CreateAgent_CreatesAgentWithCodeInterpreter_FoundryOptionsAsync()
=> this.CreateAgent_CreatesAgentWithCodeInterpreterAsync("CreateWithFoundryOptionsAsync");
private async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism)
{
// Arrange.
const string AgentInstructions = """
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using Azure;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
@@ -84,19 +84,21 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture
return Task.CompletedTask;
}
public Task DisposeAsync()
public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
if (this._persistentAgentsClient is not null && this._agent is not null)
{
return this._persistentAgentsClient.Administration.DeleteAgentAsync(this._agent.Id);
return new ValueTask(this._persistentAgentsClient.Administration.DeleteAgentAsync(this._agent.Id));
}
return Task.CompletedTask;
return default;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential());
this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential());
this._agent = await this.CreateChatClientAgentAsync();
}
}
@@ -9,15 +9,21 @@ public class AzureAIAgentsPersistentStructuredOutputRunTests() : StructuredOutpu
{
private const string SkipReason = "Fails intermittently on the build agent/CI";
[Fact(Skip = SkipReason)]
public override Task RunWithResponseFormatReturnsExpectedResultAsync() =>
base.RunWithResponseFormatReturnsExpectedResultAsync();
public override Task RunWithResponseFormatReturnsExpectedResultAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty);
return base.RunWithResponseFormatReturnsExpectedResultAsync();
}
[Fact(Skip = SkipReason)]
public override Task RunWithGenericTypeReturnsExpectedResultAsync() =>
base.RunWithGenericTypeReturnsExpectedResultAsync();
public override Task RunWithGenericTypeReturnsExpectedResultAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty);
return base.RunWithGenericTypeReturnsExpectedResultAsync();
}
[Fact(Skip = SkipReason)]
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() =>
base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty);
return base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
}
}
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CS8793</NoWarn>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
@@ -28,16 +28,24 @@ public class CopilotStudioFixture : IAgentFixture
// Chat Completion does not require/support deleting threads, so this is a no-op.
Task.CompletedTask;
public Task InitializeAsync()
public ValueTask InitializeAsync()
{
const string CopilotStudioHttpClientName = nameof(CopilotStudioAgent);
var settings = new CopilotStudioConnectionSettings(
TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioTenantId),
TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioAgentAppId))
CopilotStudioConnectionSettings? settings = null;
try
{
DirectConnectUrl = TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioDirectConnectUrl),
};
settings = new CopilotStudioConnectionSettings(
TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioTenantId),
TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioAgentAppId))
{
DirectConnectUrl = TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioDirectConnectUrl),
};
}
catch (InvalidOperationException ex)
{
Assert.Skip("CopilotStudio configuration could not be loaded. Error:" + ex.Message);
}
ServiceCollection services = new();
@@ -56,8 +64,12 @@ public class CopilotStudioFixture : IAgentFixture
this.Agent = new CopilotStudioAgent(client);
return Task.CompletedTask;
return default;
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
return default;
}
}
@@ -10,23 +10,33 @@ public class CopilotStudioRunStreamingTests() : RunStreamingTests<CopilotStudioF
// Set to null to run the tests.
private const string ManualVerification = "For manual verification";
[Fact(Skip = "Copilot Studio does not support session history retrieval, so this test is not applicable.")]
public override Task SessionMaintainsHistoryAsync() =>
Task.CompletedTask;
public override Task SessionMaintainsHistoryAsync()
{
Assert.Skip("Copilot Studio does not support session history retrieval, so this test is not applicable.");
return base.SessionMaintainsHistoryAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithChatMessageReturnsExpectedResultAsync() =>
base.RunWithChatMessageReturnsExpectedResultAsync();
public override Task RunWithChatMessageReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithChatMessagesReturnsExpectedResultAsync() =>
base.RunWithChatMessagesReturnsExpectedResultAsync();
public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithNoMessageDoesNotFailAsync() =>
base.RunWithNoMessageDoesNotFailAsync();
public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithNoMessageDoesNotFailAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithStringReturnsExpectedResultAsync() =>
base.RunWithStringReturnsExpectedResultAsync();
public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithStringReturnsExpectedResultAsync();
}
}
@@ -10,23 +10,33 @@ public class CopilotStudioRunTests() : RunTests<CopilotStudioFixture>(() => new(
// Set to null to run the tests.
private const string ManualVerification = "For manual verification";
[Fact(Skip = "Copilot Studio does not support session history retrieval, so this test is not applicable.")]
public override Task SessionMaintainsHistoryAsync() =>
Task.CompletedTask;
public override Task SessionMaintainsHistoryAsync()
{
Assert.Skip("Copilot Studio does not support session history retrieval, so this test is not applicable.");
return base.SessionMaintainsHistoryAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
public override Task RunWithChatMessageReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithChatMessagesReturnsExpectedResultAsync() =>
public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}
base.RunWithChatMessagesReturnsExpectedResultAsync();
public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithNoMessageDoesNotFailAsync();
}
[Fact(Skip = ManualVerification)]
public override Task RunWithNoMessageDoesNotFailAsync() =>
base.RunWithNoMessageDoesNotFailAsync();
[Fact(Skip = ManualVerification)]
public override Task RunWithStringReturnsExpectedResultAsync() =>
base.RunWithStringReturnsExpectedResultAsync();
public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty);
return base.RunWithStringReturnsExpectedResultAsync();
}
}
+8 -5
View File
@@ -6,22 +6,25 @@
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<IsAotCompatible>false</IsAotCompatible>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
<NoWarn>$(NoWarn);Moq1410;xUnit2023;MAAI001</NoWarn>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<NoWarn>$(NoWarn);Moq1410;xUnit1051;MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xRetry" />
<PackageReference Include="xunit" />
<PackageReference Include="xRetry.v3" />
<PackageReference Include="xunit.v3.mtp-v2" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<Using Include="xRetry" />
<Using Include="xRetry.v3" />
<Using Include="Xunit" />
</ItemGroup>
@@ -467,7 +467,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithModelAndOptions_CreatesValidAgentAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", instructions: "Test instructions");
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -475,7 +475,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
var agent = await client.CreateAIAgentAsync("test-model", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -490,7 +490,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithModelAndOptions_WithClientFactory_AppliesFactoryCorrectlyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", instructions: "Test instructions");
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -499,7 +499,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
TestChatClient? testChatClient = null;
// Act
var agent = await client.CreateAIAgentAsync(
var agent = await testClient.Client.CreateAIAgentAsync(
"test-model",
options,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
@@ -560,12 +560,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -582,12 +582,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definitionResponse = GeneratePromptDefinitionResponse(definition, null);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -602,12 +602,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definition);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definition);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -628,12 +628,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Create a response definition with the same tool
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -667,12 +667,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
definitionResponse.Tools.Add(tool);
}
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -803,10 +803,10 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definitionResponse = GeneratePromptDefinitionResponse(new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }, tools);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
// Act
var agent = await client.CreateAIAgentAsync(
var agent = await testClient.Client.CreateAIAgentAsync(
"test-agent",
"test-model",
"Test instructions",
@@ -831,14 +831,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDefinitionTools_CreatesAgentAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("async_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -885,7 +885,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var sharepointOptions = new SharePointGroundingToolOptions();
sharepointOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id"));
var structuredOutputs = new StructuredOutputDefinition("name", "description", BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()), false);
var structuredOutputs = new StructuredOutputDefinition("name", "description", new Dictionary<string, BinaryData> { ["schema"] = BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()) }, false);
// Add tools to the definition
definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
@@ -902,12 +902,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Generate agent definition response with the tools
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -942,12 +942,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(functionTool);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -961,7 +961,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDeclarativeFunctionFromDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
// Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration
@@ -974,7 +974,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -1001,12 +1001,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -1027,12 +1027,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
var definitionResponse = GeneratePromptDefinitionResponse(definition, null);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync("test-agent", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
@@ -1083,7 +1083,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
new PromptAgentDefinition("test-model") { Instructions = "Test" },
tools);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new ChatClientAgentOptions
{
@@ -1092,7 +1092,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
var agent = await client.CreateAIAgentAsync("test-model", options);
var agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -1278,14 +1278,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithClientFactory_ReceivesCorrectUnderlyingClientAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
IChatClient? receivedClient = null;
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync(
var agent = await testClient.Client.CreateAIAgentAsync(
"test-agent",
options,
clientFactory: (innerClient) =>
@@ -1340,10 +1340,10 @@ public sealed class AzureAIProjectChatClientExtensionsTests
const string AgentName = "test-agent";
const string Model = "test-model";
const string Instructions = "Test instructions";
AIProjectClient client = this.CreateTestAgentClient(AgentName, Instructions);
using var testClient = CreateTestAgentClientWithHandler(AgentName, Instructions);
// Act
var agent = await client.CreateAIAgentAsync(
var agent = await testClient.Client.CreateAIAgentAsync(
AgentName,
Model,
Instructions,
@@ -1367,12 +1367,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var agentDefinitionResponse = GeneratePromptDefinitionResponse(definition, null);
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", agentDefinitionResponse: agentDefinitionResponse);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: agentDefinitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await client.CreateAIAgentAsync(
var agent = await testClient.Client.CreateAIAgentAsync(
"test-agent",
options,
clientFactory: (innerClient) => new TestChatClient(innerClient));
@@ -1390,7 +1390,8 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#region User-Agent Header Tests
/// <summary>
/// Verifies that the user-agent header is added to both synchronous and asynchronous requests made by agent creation methods.
/// Verifies that the MEAI user-agent header is added to CreateAIAgentAsync POST requests
/// via the protocol method's RequestOptions pipeline policy.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_UserAgentHeaderAddedToRequestsAsync()
@@ -1398,9 +1399,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
using var httpHandler = new HttpHandlerAssert(request =>
{
Assert.Equal("POST", request.Method.Method);
Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
// Verify MEAI user-agent header is present on CreateAgentVersion POST request
Assert.True(request.Headers.TryGetValues("User-Agent", out var userAgentValues));
Assert.Contains(userAgentValues, v => v.Contains("MEAI"));
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
@@ -1940,7 +1944,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithTextResponseFormat_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -1952,7 +1956,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -1966,7 +1970,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithoutSchema_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -1978,7 +1982,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -1992,7 +1996,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchema_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var options = new ChatClientAgentOptions
@@ -2006,7 +2010,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2020,7 +2024,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictMode_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var additionalProps = new AdditionalPropertiesDictionary
@@ -2039,7 +2043,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2053,7 +2057,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictModeFalse_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var additionalProps = new AdditionalPropertiesDictionary
@@ -2072,7 +2076,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2090,7 +2094,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithRawRepresentationFactory_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2102,7 +2106,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2116,7 +2120,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNull_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2128,7 +2132,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2142,7 +2146,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNonCreateResponseOptions_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2154,7 +2158,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2172,7 +2176,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithDescription_SetsDescriptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient(description: "Test description");
using var testClient = CreateTestAgentClientWithHandler(description: "Test description");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2181,7 +2185,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2195,7 +2199,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithoutDescription_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
@@ -2203,7 +2207,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2688,7 +2692,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithHostedToolTypes_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
using var testClient = CreateTestAgentClientWithHandler();
var webSearchTool = new HostedWebSearchTool();
var options = new ChatClientAgentOptions
@@ -2702,7 +2706,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
};
// Act
ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options);
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
@@ -2855,6 +2859,54 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse);
}
/// <summary>
/// Creates a test AIProjectClient backed by an HTTP handler that returns canned responses.
/// Used for tests that exercise the protocol-method code path (CreateAgentVersion).
/// The returned client must be disposed to clean up the underlying HttpClient/handler.
/// </summary>
private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(agentName, agentDefinitionResponse, instructions, description);
var httpHandler = new HttpHandlerAssert(_ =>
new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseJson, Encoding.UTF8, "application/json") });
#pragma warning disable CA5399
var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AIProjectClient(
new Uri("https://test.openai.azure.com/"),
new FakeAuthenticationTokenProvider(),
new() { Transport = new HttpClientPipelineTransport(httpClient) });
return new DisposableTestClient(client, httpClient, httpHandler);
}
/// <summary>
/// Wraps an AIProjectClient and its disposable dependencies for deterministic cleanup.
/// </summary>
private sealed class DisposableTestClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly HttpHandlerAssert _httpHandler;
public DisposableTestClient(AIProjectClient client, HttpClient httpClient, HttpHandlerAssert httpHandler)
{
this.Client = client;
this._httpClient = httpClient;
this._httpHandler = httpHandler;
}
public AIProjectClient Client { get; }
public void Dispose()
{
this._httpClient.Dispose();
this._httpHandler.Dispose();
}
}
/// <summary>
/// Creates a test AgentRecord for testing.
/// </summary>
@@ -3039,25 +3091,13 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
@@ -22,7 +22,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
{
requestTriggered = true;
@@ -71,7 +71,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
{
requestTriggered = true;
@@ -120,7 +120,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
{
requestTriggered = true;
@@ -169,7 +169,7 @@ public class AzureAIProjectChatClientTests
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
{
requestTriggered = true;
@@ -58,7 +58,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
private bool _preserveContainer;
private CosmosClient? _setupClient; // Only used for test setup/cleanup
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
// Fail fast if emulator is not available
this.SkipIfEmulatorNotAvailable();
@@ -100,8 +100,10 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
}
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
if (this._setupClient != null && this._emulatorAvailable)
{
try
@@ -143,12 +145,12 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
// Locally: Skip if emulator connection check failed
var ciEmulatorAvailable = string.Equals(Environment.GetEnvironmentVariable("COSMOSDB_EMULATOR_AVAILABLE"), bool.TrueString, StringComparison.OrdinalIgnoreCase);
Xunit.Skip.If(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available");
Assert.SkipWhen(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available");
}
#region Constructor Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void StateKeys_ReturnsDefaultKey_WhenNoStateKeyProvided()
{
@@ -163,7 +165,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Contains("CosmosChatHistoryProvider", provider.StateKeys);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void StateKeys_ReturnsCustomKey_WhenSetViaConstructor()
{
@@ -179,7 +181,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Contains("custom-key", provider.StateKeys);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithConnectionString_ShouldCreateInstance()
{
@@ -196,7 +198,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(TestContainerId, provider.ContainerId);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithNullConnectionString_ShouldThrowArgumentException()
{
@@ -206,7 +208,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
_ => new CosmosChatHistoryProvider.State("test-conversation")));
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithNullStateInitializer_ShouldThrowArgumentNullException()
{
@@ -221,7 +223,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
#region InvokedAsync Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync()
{
@@ -286,7 +288,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(ChatRole.User, messageList[0].Role);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync()
{
@@ -329,7 +331,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
#region InvokingAsync Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_WithNoMessages_ShouldReturnEmptyAsync()
{
@@ -347,7 +349,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Empty(messages);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_WithConversationIsolation_ShouldOnlyReturnMessagesForConversationAsync()
{
@@ -391,7 +393,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
#region Integration Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task FullWorkflow_AddAndGet_ShouldWorkCorrectlyAsync()
{
@@ -442,7 +444,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
#region Disposal Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Dispose_AfterUse_ShouldNotThrow()
{
@@ -455,7 +457,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
provider.Dispose(); // Should not throw
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Dispose_MultipleCalls_ShouldNotThrow()
{
@@ -473,7 +475,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
#region Hierarchical Partitioning Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalConnectionString_ShouldCreateInstance()
{
@@ -490,7 +492,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(HierarchicalTestContainerId, provider.ContainerId);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalEndpoint_ShouldCreateInstance()
{
@@ -508,7 +510,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(HierarchicalTestContainerId, provider.ContainerId);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalCosmosClient_ShouldCreateInstance()
{
@@ -525,7 +527,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(HierarchicalTestContainerId, provider.ContainerId);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void State_WithEmptyConversationId_ShouldThrowArgumentException()
{
@@ -534,7 +536,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
new CosmosChatHistoryProvider.State(""));
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public void State_WithWhitespaceConversationId_ShouldThrowArgumentException()
{
@@ -543,7 +545,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
new CosmosChatHistoryProvider.State(" "));
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithHierarchicalPartitioning_ShouldAddMessageWithMetadataAsync()
{
@@ -597,7 +599,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(SessionId, (string)document!.sessionId);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMessagesAsync()
{
@@ -636,7 +638,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Third hierarchical message", messageList[2].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_WithHierarchicalPartitionIsolation_ShouldIsolateMessagesByUserIdAsync()
{
@@ -682,7 +684,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Message from user 2", messageList2[0].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task StateBag_WithHierarchicalPartitioning_ShouldPreserveStateAcrossProviderInstancesAsync()
{
@@ -717,7 +719,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(HierarchicalTestContainerId, newStore.ContainerId);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task HierarchicalAndSimplePartitioning_ShouldCoexistAsync()
{
@@ -759,7 +761,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Hierarchical partitioning message", hierarchicalMessageList[0].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task MaxMessagesToRetrieve_ShouldLimitAndReturnMostRecentAsync()
{
@@ -800,7 +802,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Message 10", messageList[4].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task MaxMessagesToRetrieve_Null_ShouldReturnAllMessagesAsync()
{
@@ -836,7 +838,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Message 10", messageList[9].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task GetMessageCountAsync_WithMessages_ShouldReturnCorrectCountAsync()
{
@@ -868,7 +870,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(5, count);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task GetMessageCountAsync_WithNoMessages_ShouldReturnZeroAsync()
{
@@ -887,7 +889,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal(0, count);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task ClearMessagesAsync_WithMessages_ShouldDeleteAndReturnCountAsync()
{
@@ -935,7 +937,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Empty(retrievedMessages);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task ClearMessagesAsync_WithNoMessages_ShouldReturnZeroAsync()
{
@@ -958,7 +960,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
#region Message Filter Tests
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_DefaultFilter_ExcludesChatHistoryMessagesFromStorageAsync()
{
@@ -993,7 +995,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Response", messages[2].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync()
{
@@ -1031,7 +1033,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
Assert.Equal("Response", messages[1].Text);
}
[SkippableFact]
[Fact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_RetrievalOutputFilter_FiltersRetrievedMessagesAsync()
{
@@ -55,7 +55,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
return options;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
// Fail fast if emulator is not available
this.SkipIfEmulatorNotAvailable();
@@ -88,8 +88,10 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
}
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
if (this._cosmosClient != null && this._emulatorAvailable)
{
try
@@ -124,12 +126,12 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
// Locally: Skip if emulator connection check failed
var ciEmulatorAvailable = string.Equals(Environment.GetEnvironmentVariable("COSMOSDB_EMULATOR_AVAILABLE"), bool.TrueString, StringComparison.OrdinalIgnoreCase);
Xunit.Skip.If(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available");
Assert.SkipWhen(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available");
}
#region Constructor Tests
[SkippableFact]
[Fact]
public void Constructor_WithCosmosClient_SetsProperties()
{
// Arrange
@@ -143,7 +145,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.Equal(TestContainerId, store.ContainerId);
}
[SkippableFact]
[Fact]
public void Constructor_WithConnectionString_SetsProperties()
{
// Arrange
@@ -157,7 +159,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.Equal(TestContainerId, store.ContainerId);
}
[SkippableFact]
[Fact]
public void Constructor_WithNullCosmosClient_ThrowsArgumentNullException()
{
// Act & Assert
@@ -165,7 +167,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
new CosmosCheckpointStore((CosmosClient)null!, s_testDatabaseId, TestContainerId));
}
[SkippableFact]
[Fact]
public void Constructor_WithNullConnectionString_ThrowsArgumentException()
{
// Act & Assert
@@ -177,7 +179,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
#region Checkpoint Operations Tests
[SkippableFact]
[Fact]
public async Task CreateCheckpointAsync_NewCheckpoint_CreatesSuccessfullyAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -197,7 +199,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.NotEmpty(checkpointInfo.CheckpointId);
}
[SkippableFact]
[Fact]
public async Task RetrieveCheckpointAsync_ExistingCheckpoint_ReturnsCorrectValueAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -218,7 +220,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.Equal("Hello, World!", messageProp.GetString());
}
[SkippableFact]
[Fact]
public async Task RetrieveCheckpointAsync_NonExistentCheckpoint_ThrowsInvalidOperationExceptionAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -233,7 +235,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
store.RetrieveCheckpointAsync(sessionId, fakeCheckpointInfo).AsTask());
}
[SkippableFact]
[Fact]
public async Task RetrieveIndexAsync_EmptyStore_ReturnsEmptyCollectionAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -250,7 +252,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.Empty(index);
}
[SkippableFact]
[Fact]
public async Task RetrieveIndexAsync_WithCheckpoints_ReturnsAllCheckpointsAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -275,7 +277,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.Contains(index, c => c.CheckpointId == checkpoint3.CheckpointId);
}
[SkippableFact]
[Fact]
public async Task CreateCheckpointAsync_WithParent_CreatesHierarchyAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -295,7 +297,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
Assert.Equal(sessionId, childCheckpoint.SessionId);
}
[SkippableFact]
[Fact]
public async Task RetrieveIndexAsync_WithParentFilter_ReturnsFilteredResultsAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -331,7 +333,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
#region Run Isolation Tests
[SkippableFact]
[Fact]
public async Task CheckpointOperations_DifferentRuns_IsolatesDataAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -361,7 +363,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
#region Error Handling Tests
[SkippableFact]
[Fact]
public async Task CreateCheckpointAsync_WithNullSessionId_ThrowsArgumentExceptionAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -375,7 +377,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
store.CreateCheckpointAsync(null!, checkpointValue).AsTask());
}
[SkippableFact]
[Fact]
public async Task CreateCheckpointAsync_WithEmptySessionId_ThrowsArgumentExceptionAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -389,7 +391,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
store.CreateCheckpointAsync("", checkpointValue).AsTask());
}
[SkippableFact]
[Fact]
public async Task RetrieveCheckpointAsync_WithNullCheckpointInfo_ThrowsArgumentNullExceptionAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -407,7 +409,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
#region Disposal Tests
[SkippableFact]
[Fact]
public async Task Dispose_AfterDisposal_ThrowsObjectDisposedExceptionAsync()
{
this.SkipIfEmulatorNotAvailable();
@@ -424,7 +426,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
store.CreateCheckpointAsync("test-run", checkpointValue).AsTask());
}
[SkippableFact]
[Fact]
public void Dispose_MultipleCalls_DoesNotThrow()
{
this.SkipIfEmulatorNotAvailable();
@@ -17,7 +17,6 @@
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Azure.Cosmos" />
<PackageReference Include="Xunit.SkippableFact" />
</ItemGroup>
</Project>
@@ -9,7 +9,6 @@ using Microsoft.DurableTask.Client.Entities;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Configuration;
using OpenAI.Chat;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
@@ -6,7 +6,6 @@ using System.Reflection;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
@@ -30,7 +29,7 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper)
private readonly ITestOutputHelper _outputHelper = outputHelper;
async Task IAsyncLifetime.InitializeAsync()
async ValueTask IAsyncLifetime.InitializeAsync()
{
if (!s_infrastructureStarted)
{
@@ -39,7 +38,7 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper)
}
}
async Task IAsyncLifetime.DisposeAsync()
async ValueTask IAsyncDisposable.DisposeAsync()
{
// Nothing to clean up
await Task.CompletedTask;
@@ -736,6 +735,9 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper)
private async Task RunSampleTestAsync(string samplePath, Func<Process, BlockingCollection<OutputLog>, Task> testAction)
{
// Build the sample project first (it may not have been built as part of the solution)
await this.BuildSampleAsync(samplePath);
// Generate a unique TaskHub name for this sample test to prevent cross-test interference
// when multiple tests run together and share the same DTS emulator.
string uniqueTaskHubName = $"sample-{Guid.NewGuid().ToString("N").Substring(0, 6)}";
@@ -814,12 +816,44 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper)
return null;
}
private async Task BuildSampleAsync(string samplePath)
{
this._outputHelper.WriteLine($"Building sample at {samplePath}...");
ProcessStartInfo buildInfo = new()
{
FileName = "dotnet",
Arguments = $"build --framework {s_dotnetTargetFramework}",
WorkingDirectory = samplePath,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
using Process buildProcess = new() { StartInfo = buildInfo };
buildProcess.Start();
// Read both streams asynchronously to avoid deadlocks from filled pipe buffers
Task<string> stdoutTask = buildProcess.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = buildProcess.StandardError.ReadToEndAsync();
await buildProcess.WaitForExitAsync();
string stderr = await stderrTask;
if (buildProcess.ExitCode != 0)
{
string stdout = await stdoutTask;
throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}");
}
this._outputHelper.WriteLine($"Build completed for {samplePath}.");
}
private Process StartConsoleApp(string samplePath, BlockingCollection<OutputLog> logs, string taskHubName)
{
ProcessStartInfo startInfo = new()
{
FileName = "dotnet",
Arguments = $"run --framework {s_dotnetTargetFramework}",
Arguments = $"run --no-build --framework {s_dotnetTargetFramework}",
WorkingDirectory = samplePath,
UseShellExecute = false,
RedirectStandardOutput = true,
@@ -9,7 +9,6 @@ using Microsoft.DurableTask.Client;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI.Chat;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
@@ -2,7 +2,6 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging;
@@ -2,7 +2,6 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging;
@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
</PropertyGroup>
<!-- Public packages required by integration tests -->
@@ -7,7 +7,6 @@ using Microsoft.DurableTask.Client;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI.Chat;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
@@ -2,7 +2,6 @@
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
@@ -14,7 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenAI.Chat;
using Xunit.Abstractions;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
@@ -166,7 +165,7 @@ internal sealed class TestHelper : IDisposable
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), new AzureCliCredential());
: new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), TestAzureCliCredentials.CreateAzureCliCredential());
return client.GetChatClient(azureOpenAiDeploymentName);
}

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