Compare commits

...
Author SHA1 Message Date
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
16230d3b20 Bump actions/checkout from 5 to 6 (#2404)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-12-11 18:43:52 +00:00
Dmytro StrukandGitHub 8d53b20026 Python: Updated package versions (#2784)
* Updated package versions

* Small fix
2025-12-11 18:39:08 +00:00
Eduard van ValkenburgandGitHub c376868ec9 Python: added more complete parsing for mcp tool arguments (#2756)
* added more complete parsing for mcp tool arguments

* fixed mypy

* added nonlocal model counter, and some fixes

* fixes in naming logic

* extracted json parsing function, added parametrized test and checked coverage
2025-12-11 17:24:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8bb9927f3c Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2778)
---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 14:08:20 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
194486c4cc Bump Azure.Identity from 1.17.0 to 1.17.1 (#2780)
---
updated-dependencies:
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 11:04:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0413f4220a Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.7 to 4.0.4.11 (#2777)
---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 11:01:19 +00:00
CopilotGitHubrogerbarretoCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
67e83042cf .NET: Add Conversation State Sample (Step05) (#2697)
* Initial plan

* Add Agent_OpenAI_Step05_Conversation sample for conversation state management

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

* Update Program.cs comment to accurately describe the sample

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

* Update the code to use the ConversationClient more in line with the samples in OpenAI

* Apply suggestions from code review

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

* Changing sample to use ChatClientAgent and conversationId in GetNewThread

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 10:58:42 +00:00
5da1c2fd4c code ql sm04598 (#2723)
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
2025-12-11 10:34:58 +00:00
SergeyMenshykhandGitHub 989b6ebe71 .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)
* prevent nulls in AIAgent property

* address feedback
2025-12-11 09:50:25 +00:00
Evan MattsonandGitHub 3481914981 Capture file IDs from code interpreter in streaming responses (#2741) 2025-12-11 07:30:19 +00:00
KurtandGitHub 4c6a5d4aa1 Python: fix: GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outpu… (#2750)
* fix: ManagerSelectionResponse JSON Schema for OpenAI Structured Output Strict Mode

* refactor: install pre-commit then commit again
2025-12-11 02:34:12 +00:00
Tao ChenandGitHub 191779ce80 Python: Add factory pattern to concurrent orchestration builder (#2738)
* Add factory pattern to concurrent orchestration builder

* Update readme

* Address AI comments

* Fix unit tests

* Fix import

* Prevent multiple calls to set participants or factories

* Add comments

* Mitigate warnings

* Fix mypy

* Address comments

* Address Copilot comments

* Fix tests
2025-12-11 01:23:28 +00:00
CopilotGitHubstephentoubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Roger Barreto
638fbb5f03 Update Microsoft.Extensions.AI.* packages to 10.1.0 (#2735)
* Initial plan

* Update Microsoft.Extensions.AI dependencies to latest versions (10.1.0)

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2025-12-10 19:04:05 +00:00
Tao ChenandGitHub 523305ac62 Python: Add factory pattern to sequential orchestration builder (#2710)
* Add factory pattern to sequential orchestration builder

* Use temp list to avoid override

* Add sample and some other fixes

* Fix comments

* Small fix

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

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

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

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

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

* Remove test for logging change per review feedback

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

---------

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

* Cleanup around naming

* Fix typing issues

* Clean up

* Naming clean up

* Updates to HITL to make it cleaner

* Rename human input hook to orchestration request info

* Clean up per PR feedback
2025-12-10 15:59:29 +00:00
0d9ae1920d .NET: use valid Gemini model (#2695)
* use proper Gemini model

* use named parameters

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2025-12-10 12:23:06 +00:00
CopilotGitHubrogerbarretocopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
90964acd2d .NET: Add LoggingAgent wrapper for ILogger-based observability (#2701)
* Initial plan

* Add LoggingAgent class and UseLogging extension method

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

* Add unit tests for LoggingAgent and fix JSON serialization error handling

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

* Add comments explaining unreachable code in test async iterators

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

* Fix file encoding - add UTF-8 BOM to all C# files

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

* Address Format issues

* Addres format

* Break up extensions in dedicated files

* Adjust class names

* Add xmldoc info

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2025-12-10 12:17:01 +00:00
Roger BarretoandGitHub 1949193a2e .NET: Update the migration guidelines with latest changes (#2703)
* Update the migration guidelines with latest changes

* Update code interpreter sample
2025-12-10 11:57:10 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
bc8dbe4b05 Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.6 to 4.0.4.7 (#2173)
---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-12-10 11:42:42 +00:00
Stephen ToubandGitHub b01fd23cd2 Update analyzers for .NET 10 SDK (#2611) 2025-12-10 10:34:56 +00:00
2f0b2db12a fix: Standardize OpenAI API key environment variable naming (#1001) (#2629)
- Replace OPENAI_APIKEY with OPENAI_API_KEY across all samples
- Replace AZURE_FOUNDRY_OPENAI_APIKEY with AZURE_FOUNDRY_OPENAI_API_KEY
- Ensures consistency with OpenAI's standard naming convention
- Applies to .NET and Python samples

Fixes #1001

Co-authored-by: Alexander Zarei <alzarei@users.noreply.github.com>
2025-12-10 10:33:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
7be860e4c4 Bump Aspire.Hosting.Azure.CognitiveServices from 13.0.1 to 13.0.2 (#2624)
---
updated-dependencies:
- dependency-name: Aspire.Hosting.Azure.CognitiveServices
  dependency-version: 13.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-12-10 09:32:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
1dce2581b8 Bump Aspire.Hosting.AppHost from 13.0.1 to 13.0.2 (#2623)
---
updated-dependencies:
- dependency-name: Aspire.Hosting.AppHost
  dependency-version: 13.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-12-10 09:32:04 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
9313168eeb Bump Aspire.Microsoft.Azure.Cosmos from 13.0.1 to 13.0.2 (#2625)
---
updated-dependencies:
- dependency-name: Aspire.Microsoft.Azure.Cosmos
  dependency-version: 13.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-12-10 09:31:10 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
b391088a68 Bump MishaKav/pytest-coverage-comment from 1.1.59 to 1.2.0 (#2255)
Bumps [MishaKav/pytest-coverage-comment](https://github.com/mishakav/pytest-coverage-comment) from 1.1.59 to 1.2.0.
- [Release notes](https://github.com/mishakav/pytest-coverage-comment/releases)
- [Changelog](https://github.com/MishaKav/pytest-coverage-comment/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mishakav/pytest-coverage-comment/compare/v1.1.59...v1.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-12-10 02:21:56 +00:00
163 changed files with 6315 additions and 888 deletions
@@ -142,9 +142,9 @@ Replace these Semantic Kernel agent classes with their Agent Framework equivalen
|----------------------|----------------------------|-------------------|
| `IChatCompletionService` | `IChatClient` | Convert to `IChatClient` using `chatService.AsChatClient()` extensions |
| `ChatCompletionAgent` | `ChatClientAgent` | Remove `Kernel` parameter, add `IChatClient` parameter |
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | ⚠️ **Deprecated** - Use Responses API instead. <br> **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
| `AzureAIAgent` | `AIAgent` (via extension) | **New**: `PersistentAgentsClient.CreateAIAgent()` <br> **Existing**: `PersistentAgentsClient.GetAIAgent(agentId)` |
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient().CreateAIAgent()` |
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient(modelId).CreateAIAgent()` |
| `A2AAgent` | `AIAgent` (via extension) | Replace with `A2ACardResolver.GetAIAgentAsync()` |
| `BedrockAgent` | Not supported | Custom implementation required |
@@ -529,14 +529,14 @@ AIAgent agent = new OpenAIClient(apiKey)
.CreateAIAgent(instructions: instructions);
```
**OpenAI Assistants (New):**
**OpenAI Assistants (New):** ⚠️ *Deprecated - Use Responses API instead*
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetAssistantClient()
.CreateAIAgent(modelId, instructions: instructions);
```
**OpenAI Assistants (Existing):**
**OpenAI Assistants (Existing):** ⚠️ *Deprecated - Use Responses API instead*
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetAssistantClient()
@@ -562,6 +562,20 @@ AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
.GetAIAgentAsync(agentId);
```
**OpenAI Responses:** *(Recommended for OpenAI)*
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetOpenAIResponseClient(modelId)
.CreateAIAgent(instructions: instructions);
```
**Azure OpenAI Responses:** *(Recommended for Azure OpenAI)*
```csharp
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.GetOpenAIResponseClient(deploymentName)
.CreateAIAgent(instructions: instructions);
```
**A2A:**
```csharp
A2ACardResolver resolver = new(new Uri(agentHost));
@@ -762,35 +776,57 @@ await foreach (var content in agent.InvokeAsync(userInput, thread))
**With this Agent Framework CodeInterpreter pattern:**
```csharp
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var result = await agent.RunAsync(userInput, thread);
Console.WriteLine(result);
// Extract chat response MEAI type via first level breaking glass
var chatResponse = result.RawRepresentation as ChatResponse;
// Get the CodeInterpreterToolCallContent (code input)
CodeInterpreterToolCallContent? toolCallContent = result.Messages
.SelectMany(m => m.Contents)
.OfType<CodeInterpreterToolCallContent>()
.FirstOrDefault();
// Extract underlying SDK updates via second level breaking glass
var underlyingStreamingUpdates = chatResponse?.RawRepresentation as IEnumerable<object?> ?? [];
StringBuilder generatedCode = new();
foreach (object? underlyingUpdate in underlyingStreamingUpdates ?? [])
if (toolCallContent?.Inputs is not null)
{
if (underlyingUpdate is RunStepDetailsUpdate stepDetailsUpdate && stepDetailsUpdate.CodeInterpreterInput is not null)
DataContent? codeInput = toolCallContent.Inputs.OfType<DataContent>().FirstOrDefault();
if (codeInput?.HasTopLevelMediaType("text") ?? false)
{
generatedCode.Append(stepDetailsUpdate.CodeInterpreterInput);
Console.WriteLine($"Code Input: {Encoding.UTF8.GetString(codeInput.Data.ToArray())}");
}
}
if (!string.IsNullOrEmpty(generatedCode.ToString()))
// Get the CodeInterpreterToolResultContent (code output)
CodeInterpreterToolResultContent? toolResultContent = result.Messages
.SelectMany(m => m.Contents)
.OfType<CodeInterpreterToolResultContent>()
.FirstOrDefault();
if (toolResultContent?.Outputs is not null)
{
Console.WriteLine($"\n# {chatResponse?.Messages[0].Role}:Generated Code:\n{generatedCode}");
TextContent? resultOutput = toolResultContent.Outputs.OfType<TextContent>().FirstOrDefault();
if (resultOutput is not null)
{
Console.WriteLine($"Code Tool Result: {resultOutput.Text}");
}
}
// Getting any annotations generated by the tool
foreach (AIAnnotation annotation in result.Messages
.SelectMany(m => m.Contents)
.SelectMany(c => c.Annotations ?? []))
{
Console.WriteLine($"Annotation: {annotation}");
}
```
**Functional differences:**
1. Code interpreter output is separate from text content, not a metadata property
2. Access code via `RunStepDetailsUpdate.CodeInterpreterInput` instead of metadata
3. Use breaking glass pattern to access underlying SDK objects
4. Process text content and code interpreter output independently
1. Code interpreter content is now available via MEAI abstractions - no breaking glass required
2. Use `CodeInterpreterToolCallContent` to access code inputs (the generated code)
3. Use `CodeInterpreterToolResultContent` to access code outputs (execution results)
4. Annotations are accessible via `AIAnnotation` on content items
</behavioral_changes>
#### Provider-Specific Options Configuration
@@ -980,6 +1016,8 @@ AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential(
### 3. OpenAI Assistants Migration
> ⚠️ **DEPRECATION WARNING**: The OpenAI Assistants API has been deprecated. The Agent Framework extension methods for Assistants are marked as `[Obsolete]`. **Please use the Responses API instead** (see Section 6: OpenAI Responses Migration).
<configuration_changes>
**Remove Semantic Kernel Packages:**
```xml
@@ -1291,52 +1329,7 @@ var result = await agent.RunAsync(userInput, thread);
```
</api_changes>
### 8. A2A Migration
<configuration_changes>
**Remove Semantic Kernel Packages:**
```xml
<PackageReference Include="Microsoft.SemanticKernel.Agents.A2A" />
```
**Add Agent Framework Packages:**
```xml
<PackageReference Include="Microsoft.Agents.AI.A2A" />
```
</configuration_changes>
<api_changes>
**Replace this Semantic Kernel pattern:**
```csharp
using A2A;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.A2A;
using var httpClient = CreateHttpClient();
var client = new A2AClient(agentUrl, httpClient);
var cardResolver = new A2ACardResolver(url, httpClient);
var agentCard = await cardResolver.GetAgentCardAsync();
Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
var agent = new A2AAgent(client, agentCard);
```
**With this Agent Framework pattern:**
```csharp
using System;
using A2A;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.A2A;
// Initialize an A2ACardResolver to get an A2A agent card.
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent agent = await agentCardResolver.GetAIAgentAsync();
```
</api_changes>
### 9. Unsupported Providers (Require Custom Implementation)
### 8. Unsupported Providers (Require Custom Implementation)
<behavioral_changes>
#### BedrockAgent Migration
@@ -1507,7 +1500,7 @@ Console.WriteLine(result);
```
</behavioral_changes>
### 10. Function Invocation Filtering
### 9. Function Invocation Filtering
**Invocation Context**
@@ -1615,25 +1608,4 @@ var filteredAgent = originalAgent
.Build();
```
### 11. Function Invocation Contexts
**Invocation Context**
Semantic Kernel's `IAutoFunctionInvocationFilter` provides a `AutoFunctionInvocationContext` where Agent Framework provides `FunctionInvocationContext`
The property mapping guide from a `AutoFunctionInvocationContext` to a `FunctionInvocationContext` is as follows:
| Semantic Kernel | Agent Framework |
| --- | --- |
| RequestSequenceIndex | Iteration |
| FunctionSequenceIndex | FunctionCallIndex |
| ToolCallId | CallContent.CallId |
| ChatMessageContent | Messages[0] |
| ExecutionSettings | Options |
| ChatHistory | Messages |
| Function | Function |
| Kernel | N/A |
| Result | Use `return` from the delegate |
| Terminate | Terminate |
| CancellationToken | provided via argument to middleware delegate |
| Arguments | Arguments |
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
+2 -2
View File
@@ -37,7 +37,7 @@ jobs:
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -68,7 +68,7 @@ jobs:
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: |
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04
# check out the latest version of the code
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up python and install the project
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -59,7 +59,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -75,7 +75,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -135,7 +135,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -19,7 +19,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Download coverage report
uses: actions/download-artifact@v6
with:
@@ -39,7 +39,7 @@ jobs:
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@v1.1.59
uses: MishaKav/pytest-coverage-comment@v1.2.0
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
issue-number: ${{ env.PR_NUMBER }}
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
env:
UV_PYTHON: "3.10"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Save the PR number to a file since the workflow_run event
# in the coverage report workflow does not have access to it
- name: Save PR number
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+1 -1
View File
@@ -11,7 +11,7 @@ model:
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_APIKEY
key: =Env.OPENAI_API_KEY
outputSchema:
properties:
language:
+1 -1
View File
@@ -11,7 +11,7 @@ model:
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_APIKEY
key: =Env.OPENAI_API_KEY
outputSchema:
properties:
language:
+1 -1
View File
@@ -11,7 +11,7 @@ model:
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_APIKEY
key: =Env.OPENAI_API_KEY
outputSchema:
properties:
language:
+1 -2
View File
@@ -3,8 +3,7 @@
<!-- Default properties inherited by all projects. Projects can override. -->
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisLevel>10.0-all</AnalysisLevel>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
+13 -12
View File
@@ -7,7 +7,7 @@
</PropertyGroup>
<PropertyGroup>
<!-- Aspire -->
<AspireAppHostSdkVersion>13.0.1</AspireAppHostSdkVersion>
<AspireAppHostSdkVersion>13.0.2</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
@@ -23,7 +23,7 @@
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.6.0" />
@@ -33,18 +33,18 @@
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.1" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.1" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
@@ -61,10 +61,10 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.0" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
@@ -72,11 +72,11 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
@@ -101,7 +101,8 @@
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
<!-- Inference SDKs -->
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.6" />
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.11" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.7.0" />
+1
View File
@@ -129,6 +129,7 @@
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
</Folder>
<Folder Name="/Samples/Purview/" />
<Folder Name="/Samples/Purview/AgentWithPurview/">
+1
View File
@@ -1,6 +1,7 @@
# Suppressing errors for Sample projects under dotnet/samples folder
[*.cs]
dotnet_diagnostic.CA1716.severity = none # Add summary to documentation comment.
dotnet_diagnostic.CA1873.severity = none # Evaluation of logging arguments may be expensive
dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on object before all references to it are out of scope
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
@@ -10,7 +10,6 @@ using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using ChatClient = OpenAI.Chat.ChatClient;
namespace AGUIDojoServer;
@@ -12,7 +12,7 @@ using OpenAI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_OPENAI_ENDPOINT is not set.");
var apiKey = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_APIKEY");
var apiKey = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_API_KEY");
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_MODEL_DEPLOYMENT") ?? "Phi-4-mini-instruct";
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Azure Foundry.
@@ -27,7 +27,7 @@ Set the following environment variables:
$env:AZURE_FOUNDRY_OPENAI_ENDPOINT="https://ai-foundry-<myresourcename>.services.ai.azure.com/openai/v1/"
# Optional, defaults to using Azure CLI for authentication if not provided
$env:AZURE_FOUNDRY_OPENAI_APIKEY="************"
$env:AZURE_FOUNDRY_OPENAI_API_KEY="************"
# Optional, defaults to Phi-4-mini-instruct
$env:AZURE_FOUNDRY_MODEL_DEPLOYMENT="Phi-4-mini-instruct"
@@ -49,7 +49,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
GenerateContentResponse generateResult = await this._models.GenerateContentAsync(modelId!, contents, config).ConfigureAwait(false);
// Create the response.
ChatResponse chatResponse = new(new ChatMessage(ChatRole.Assistant, new List<AIContent>()))
ChatResponse chatResponse = new(new ChatMessage(ChatRole.Assistant, []))
{
CreatedAt = generateResult.CreateTime is { } dt ? new DateTimeOffset(dt) : null,
ModelId = !string.IsNullOrWhiteSpace(generateResult.ModelVersion) ? generateResult.ModelVersion : modelId,
@@ -82,7 +82,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
await foreach (GenerateContentResponse generateResult in this._models.GenerateContentStreamAsync(modelId!, contents, config).WithCancellation(cancellationToken).ConfigureAwait(false))
{
// Create a response update for each result in the stream.
ChatResponseUpdate responseUpdate = new(ChatRole.Assistant, new List<AIContent>())
ChatResponseUpdate responseUpdate = new(ChatRole.Assistant, [])
{
CreatedAt = generateResult.CreateTime is { } dt ? new DateTimeOffset(dt) : null,
ModelId = !string.IsNullOrWhiteSpace(generateResult.ModelVersion) ? generateResult.ModelVersion : modelId,
@@ -148,7 +148,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
// create the request instance, allowing the caller to populate it with GenAI-specific options. Otherwise, create
// a new instance directly.
string? model = this._defaultModelId;
List<Content> contents = new();
List<Content> contents = [];
GenerateContentConfig config = options?.RawRepresentationFactory?.Invoke(this) as GenerateContentConfig ?? new();
if (options is not null)
@@ -160,7 +160,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
if (options.Instructions is { } instructions)
{
((config.SystemInstruction ??= new()).Parts ??= new()).Add(new() { Text = instructions });
((config.SystemInstruction ??= new()).Parts ??= []).Add(new() { Text = instructions });
}
if (options.MaxOutputTokens is { } maxOutputTokens)
@@ -185,7 +185,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
if (options.StopSequences is { } stopSequences)
{
(config.StopSequences ??= new()).AddRange(stopSequences);
(config.StopSequences ??= []).AddRange(stopSequences);
}
if (options.Temperature is { } temperature)
@@ -213,7 +213,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
switch (tool)
{
case AIFunctionDeclaration af:
functionDeclarations ??= new();
functionDeclarations ??= [];
functionDeclarations.Add(new()
{
Name = af.Name,
@@ -223,15 +223,15 @@ internal sealed class GoogleGenAIChatClient : IChatClient
break;
case HostedCodeInterpreterTool:
(config.Tools ??= new()).Add(new() { CodeExecution = new() });
(config.Tools ??= []).Add(new() { CodeExecution = new() });
break;
case HostedFileSearchTool:
(config.Tools ??= new()).Add(new() { Retrieval = new() });
(config.Tools ??= []).Add(new() { Retrieval = new() });
break;
case HostedWebSearchTool:
(config.Tools ??= new()).Add(new() { GoogleSearch = new() });
(config.Tools ??= []).Add(new() { GoogleSearch = new() });
break;
}
}
@@ -240,8 +240,8 @@ internal sealed class GoogleGenAIChatClient : IChatClient
if (functionDeclarations is { Count: > 0 })
{
Tool functionTools = new();
(functionTools.FunctionDeclarations ??= new()).AddRange(functionDeclarations);
(config.Tools ??= new()).Add(functionTools);
(functionTools.FunctionDeclarations ??= []).AddRange(functionDeclarations);
(config.Tools ??= []).Add(functionTools);
}
// Transfer over the tool mode if there are any tools.
@@ -261,7 +261,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.ANY } };
if (required.RequiredFunctionName is not null)
{
((config.ToolConfig.FunctionCallingConfig ??= new()).AllowedFunctionNames ??= new()).Add(required.RequiredFunctionName);
((config.ToolConfig.FunctionCallingConfig ??= new()).AllowedFunctionNames ??= []).Add(required.RequiredFunctionName);
}
break;
}
@@ -287,14 +287,14 @@ internal sealed class GoogleGenAIChatClient : IChatClient
string instruction = message.Text;
if (!string.IsNullOrWhiteSpace(instruction))
{
((config.SystemInstruction ??= new()).Parts ??= new()).Add(new() { Text = instruction });
((config.SystemInstruction ??= new()).Parts ??= []).Add(new() { Text = instruction });
}
continue;
}
Content content = new() { Role = message.Role == ChatRole.Assistant ? "model" : "user" };
content.Parts ??= new();
content.Parts ??= [];
AddPartsForAIContents(ref callIdToFunctionNames, message.Contents, content.Parts);
contents.Add(content);
@@ -367,7 +367,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
break;
case FunctionCallContent functionCallContent:
(callIdToFunctionNames ??= new())[functionCallContent.CallId] = functionCallContent.Name;
(callIdToFunctionNames ??= [])[functionCallContent.CallId] = functionCallContent.Name;
callIdToFunctionNames[""] = functionCallContent.Name; // track last function name in case calls don't have IDs
part = new()
@@ -480,22 +480,22 @@ internal sealed class GoogleGenAIChatClient : IChatClient
{
foreach (var citation in citations)
{
textContent.Annotations = new List<AIAnnotation>()
{
new CitationAnnotation()
{
Title = citation.Title,
Url = Uri.TryCreate(citation.Uri, UriKind.Absolute, out Uri? uri) ? uri : null,
AnnotatedRegions = new List<AnnotatedRegion>()
{
new TextSpanAnnotatedRegion()
{
StartIndex = citation.StartIndex,
EndIndex = citation.EndIndex,
}
},
}
};
textContent.Annotations =
[
new CitationAnnotation()
{
Title = citation.Title,
Url = Uri.TryCreate(citation.Uri, UriKind.Absolute, out Uri? uri) ? uri : null,
AnnotatedRegions =
[
new TextSpanAnnotatedRegion()
{
StartIndex = citation.StartIndex,
EndIndex = citation.EndIndex,
}
],
}
];
}
}
}
@@ -551,7 +551,7 @@ internal sealed class GoogleGenAIChatClient : IChatClient
{
if (value is int i)
{
(details.AdditionalCounts ??= new())[key] = i;
(details.AdditionalCounts ??= [])[key] = i;
}
}
}
@@ -11,7 +11,7 @@ const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
string apiKey = Environment.GetEnvironmentVariable("GOOGLE_GENAI_API_KEY") ?? throw new InvalidOperationException("Please set the GOOGLE_GENAI_API_KEY environment variable.");
string model = Environment.GetEnvironmentVariable("GOOGLE_GENAI_MODEL") ?? "gemini-2.5-fast";
string model = Environment.GetEnvironmentVariable("GOOGLE_GENAI_MODEL") ?? "gemini-2.5-flash";
// Using a Google GenAI IChatClient implementation
// Until the PR https://github.com/googleapis/dotnet-genai/pull/81 is not merged this option
@@ -28,7 +28,7 @@ Console.WriteLine($"Google GenAI client based agent response:\n{response}");
// Using a community driven Mscc.GenerativeAI.Microsoft package
ChatClientAgent agentCommunity = new(
new GeminiChatClient(apiKey, model),
new GeminiChatClient(apiKey: apiKey, model: model),
name: JokerName,
instructions: JokerInstructions);
@@ -11,7 +11,7 @@ using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Assistants;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
const string JokerName = "Joker";
@@ -11,6 +11,6 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
$env:OPENAI_APIKEY="*****" # Replace with your OpenAI API key
$env:OPENAI_API_KEY="*****" # Replace with your OpenAI API key
$env:OPENAI_MODEL="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
@@ -7,7 +7,7 @@ using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
AIAgent agent = new OpenAIClient(
@@ -8,6 +8,6 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
$env:OPENAI_APIKEY="*****" # Replace with your OpenAI api key
$env:OPENAI_API_KEY="*****" # Replace with your OpenAI api key
$env:OPENAI_MODEL="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
@@ -6,7 +6,7 @@ using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Responses;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
AIAgent agent = new OpenAIClient(
@@ -8,6 +8,6 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
$env:OPENAI_APIKEY="*****" # Replace with your OpenAI api key
$env:OPENAI_API_KEY="*****" # Replace with your OpenAI api key
$env:OPENAI_MODEL="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
@@ -7,7 +7,7 @@ using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5";
var client = new OpenAIClient(apiKey)
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to maintain conversation state using the OpenAIResponseClientAgent
// and AgentThread. By passing the same thread to multiple agent invocations, the agent
// automatically maintains the conversation history, allowing the AI model to understand
// context from previous exchanges.
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Conversations;
string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
string model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
// Create a ConversationClient directly from OpenAIClient
OpenAIClient openAIClient = new(apiKey);
ConversationClient conversationClient = openAIClient.GetConversationClient();
// Create an agent directly from the OpenAIResponseClient using OpenAIResponseClientAgent
ChatClientAgent agent = new(openAIClient.GetOpenAIResponseClient(model).AsIChatClient(), instructions: "You are a helpful assistant.", name: "ConversationAgent");
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createConversationResult.GetRawResponse().Content.ToString());
string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!;
// Create a thread for the conversation - this enables conversation state management for subsequent turns
AgentThread thread = agent.GetNewThread(conversationId);
Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
// First turn: Ask about a topic
Console.WriteLine("User: What is the capital of France?");
UserChatMessage firstMessage = new("What is the capital of France?");
// After this call, the conversation state associated in the options is stored in 'thread' and used in subsequent calls
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread);
Console.WriteLine($"Assistant: {firstResponse.Content.Last().Text}\n");
// Second turn: Follow-up question that relies on conversation context
Console.WriteLine("User: What famous landmarks are located there?");
UserChatMessage secondMessage = new("What famous landmarks are located there?");
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
Console.WriteLine($"Assistant: {secondResponse.Content.Last().Text}\n");
// Third turn: Another follow-up that demonstrates context continuity
Console.WriteLine("User: How tall is the most famous one?");
UserChatMessage thirdMessage = new("How tall is the most famous one?");
ChatCompletion thirdResponse = await agent.RunAsync([thirdMessage], thread);
Console.WriteLine($"Assistant: {thirdResponse.Content.Last().Text}\n");
Console.WriteLine("=== End of Conversation ===");
// Show full conversation history
Console.WriteLine("Full Conversation History:");
ClientResult getConversationResult = await conversationClient.GetConversationAsync(conversationId);
Console.WriteLine("Conversation created.");
Console.WriteLine($" Conversation ID: {conversationId}");
Console.WriteLine();
CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId);
foreach (ClientResult result in getConversationItemsResults.GetRawPages())
{
Console.WriteLine("Message contents retrieved. Order is most recent first by default.");
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
{
string messageId = element.GetProperty("id"u8).ToString();
string messageRole = element.GetProperty("role"u8).ToString();
Console.WriteLine($" Message ID: {messageId}");
Console.WriteLine($" Message Role: {messageRole}");
foreach (var content in element.GetProperty("content").EnumerateArray())
{
string messageContentText = content.GetProperty("text"u8).ToString();
Console.WriteLine($" Message Text: {messageContentText}");
}
Console.WriteLine();
}
}
ClientResult deleteConversationResult = conversationClient.DeleteConversation(conversationId);
using JsonDocument deleteConversationResultAsJson = JsonDocument.Parse(deleteConversationResult.GetRawResponse().Content.ToString());
bool deleted = deleteConversationResultAsJson.RootElement
.GetProperty("deleted"u8)
.GetBoolean();
Console.WriteLine("Conversation deleted.");
Console.WriteLine($" Deleted: {deleted}");
Console.WriteLine();
@@ -0,0 +1,90 @@
# Managing Conversation State with OpenAI
This sample demonstrates how to maintain conversation state across multiple turns using the Agent Framework with OpenAI's Conversation API.
## What This Sample Shows
- **Conversation State Management**: Shows how to use `ConversationClient` and `AgentThread` to maintain conversation context across multiple agent invocations
- **Multi-turn Conversations**: Demonstrates follow-up questions that rely on context from previous messages in the conversation
- **Server-Side Storage**: Uses OpenAI's Conversation API to manage conversation history server-side, allowing the model to access previous messages without resending them
- **Conversation Lifecycle**: Demonstrates creating, retrieving, and deleting conversations
## Key Concepts
### ConversationClient for Server-Side Storage
The `ConversationClient` manages conversations on OpenAI's servers:
```csharp
// Create a ConversationClient from OpenAIClient
OpenAIClient openAIClient = new(apiKey);
ConversationClient conversationClient = openAIClient.GetConversationClient();
// Create a new conversation
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
```
### AgentThread for Conversation State
The `AgentThread` works with `ChatClientAgentRunOptions` to link the agent to a server-side conversation:
```csharp
// Set up agent run options with the conversation ID
ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } };
// Create a thread for the conversation
AgentThread thread = agent.GetNewThread();
// First call links the thread to the conversation
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions);
// Subsequent calls use the thread without needing to pass options again
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
```
### Retrieving Conversation History
You can retrieve the full conversation history from the server:
```csharp
CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId);
foreach (ClientResult result in getConversationItemsResults.GetRawPages())
{
// Process conversation items
}
```
### How It Works
1. **Create an OpenAI Client**: Initialize an `OpenAIClient` with your API key
2. **Create a Conversation**: Use `ConversationClient` to create a server-side conversation
3. **Create an Agent**: Initialize an `OpenAIResponseClientAgent` with the desired model and instructions
4. **Create a Thread**: Call `agent.GetNewThread()` to create a new conversation thread
5. **Link Thread to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call
6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the thread - context is maintained
7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()`
## Running the Sample
1. Set the required environment variables:
```powershell
$env:OPENAI_API_KEY = "your_api_key_here"
$env:OPENAI_MODEL = "gpt-4o-mini"
```
2. Run the sample:
```powershell
dotnet run
```
## Expected Output
The sample demonstrates a three-turn conversation where each follow-up question relies on context from previous messages:
1. First question asks about the capital of France
2. Second question asks about landmarks "there" - requiring understanding of the previous answer
3. Third question asks about "the most famous one" - requiring context from both previous turns
After the conversation, the sample retrieves and displays the full conversation history from the server, then cleans up by deleting the conversation.
This demonstrates that the conversation state is properly maintained across multiple agent invocations using OpenAI's server-side conversation storage.
@@ -13,4 +13,5 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent with native OpenAI SDK types. Shows both regular and streaming invocation of the agent.|
|[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.|
|[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.|
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentThread for context continuity.|
@@ -20,7 +20,7 @@ IChatClient anthropic = new Anthropic.AnthropicClient(
.AsIChatClient("claude-sonnet-4-20250514");
IChatClient openai = new OpenAI.OpenAIClient(
Environment.GetEnvironmentVariable("OPENAI_APIKEY")!).GetChatClient("gpt-4o-mini")
Environment.GetEnvironmentVariable("OPENAI_API_KEY")!).GetChatClient("gpt-4o-mini")
.AsIChatClient();
// Define our agents.
@@ -36,11 +36,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -37,9 +37,9 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -37,9 +37,9 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251125.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -198,7 +198,7 @@ internal sealed class A2AAgent : AIAgent
}
/// <inheritdoc/>
public override string Id => this._id ?? base.Id;
protected override string? IdCore => this._id;
/// <inheritdoc/>
public override string? Name => this._name ?? base.Name;
@@ -281,7 +281,7 @@ internal sealed class A2AAgent : AIAgent
private static A2AContinuationToken? CreateContinuationToken(string taskId, TaskState state)
{
if (state == TaskState.Submitted || state == TaskState.Working)
if (state is TaskState.Submitted or TaskState.Working)
{
return new A2AContinuationToken(taskId);
}
@@ -220,7 +220,11 @@ public sealed class AGUIChatClient : DelegatingChatClient
if (options?.Tools is { Count: > 0 })
{
input.Tools = options.Tools.AsAGUITools();
this._logger.LogDebug("[AGUIChatClient] Tool count: {ToolCount}", options.Tools.Count);
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("[AGUIChatClient] Tool count: {ToolCount}", options.Tools.Count);
}
}
var clientToolSet = new HashSet<string>();
@@ -22,9 +22,6 @@ namespace Microsoft.Agents.AI;
[DebuggerDisplay("{DisplayName,nq}")]
public abstract class AIAgent
{
/// <summary>Default ID of this agent instance.</summary>
private readonly string _id = Guid.NewGuid().ToString("N");
/// <summary>
/// Gets the unique identifier for this agent instance.
/// </summary>
@@ -37,7 +34,19 @@ public abstract class AIAgent
/// agent instances in multi-agent scenarios. They should remain stable for the lifetime
/// of the agent instance.
/// </remarks>
public virtual string Id => this._id;
public string Id { get => this.IdCore ?? field; } = Guid.NewGuid().ToString("N");
/// <summary>
/// Gets a custom identifier for the agent, which can be overridden by derived classes.
/// </summary>
/// <value>
/// A string representing the agent's identifier, or <see langword="null"/> if the default ID should be used.
/// </value>
/// <remarks>
/// Derived classes can override this property to provide a custom identifier.
/// When <see langword="null"/> is returned, the <see cref="Id"/> property will use the default randomly-generated identifier.
/// </remarks>
protected virtual string? IdCore => null;
/// <summary>
/// Gets the human-readable name of the agent.
@@ -61,7 +70,7 @@ public abstract class AIAgent
/// This property provides a guaranteed non-null string suitable for display in user interfaces,
/// logs, or other contexts where a readable identifier is needed.
/// </remarks>
public virtual string DisplayName => this.Name ?? this.Id ?? this._id; // final fallback to _id in case Id override returns null
public virtual string DisplayName => this.Name ?? this.Id;
/// <summary>
/// Gets a description of the agent's purpose, capabilities, or behavior.
@@ -54,7 +54,7 @@ public class DelegatingAIAgent : AIAgent
protected AIAgent InnerAgent { get; }
/// <inheritdoc />
public override string Id => this.InnerAgent.Id;
protected override string? IdCore => this.InnerAgent.Id;
/// <inheritdoc />
public override string? Name => this.InnerAgent.Name;
@@ -27,7 +27,7 @@ internal static class ActivityProcessor
{
yield return CreateChatMessageFromActivity(activity, [new TextContent(activity.Text)]);
}
else
else if (logger.IsEnabled(LogLevel.Warning))
{
logger.LogWarning("Unknown activity type '{ActivityType}' received.", activity.Type);
}
@@ -274,7 +274,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
throw new ArgumentException("Invalid serialized state", nameof(serializedStoreState));
}
var state = JsonSerializer.Deserialize<StoreState>(serializedStoreState, jsonSerializerOptions);
var state = serializedStoreState.Deserialize<StoreState>(jsonSerializerOptions);
if (state?.ConversationIdentifier is not { } conversationId)
{
throw new ArgumentException("Invalid serialized state", nameof(serializedStoreState));
@@ -81,9 +81,13 @@ internal sealed partial class DevUIMiddleware
}
context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Response.Headers.Location = redirectUrl;
context.Response.Headers.Location = redirectUrl; // CodeQL [SM04598] justification: The redirect URL is constructed from a server-configured base path (_basePath), not user input. The query string is only appended as parameters and cannot change the redirect destination since this is a relative URL.
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("Redirecting {OriginalPath} to {RedirectUrl}", NewlineRegex().Replace(path, ""), NewlineRegex().Replace(redirectUrl, ""));
}
this._logger.LogDebug("Redirecting {OriginalPath} to {RedirectUrl}", NewlineRegex().Replace(path, ""), NewlineRegex().Replace(redirectUrl, ""));
return;
}
@@ -123,7 +127,11 @@ internal sealed partial class DevUIMiddleware
{
if (!this._resourceCache.TryGetValue(resourcePath.Replace('.', '/'), out var cacheEntry))
{
this._logger.LogDebug("Embedded resource not found: {ResourcePath}", resourcePath);
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("Embedded resource not found: {ResourcePath}", resourcePath);
}
return false;
}
@@ -133,7 +141,12 @@ internal sealed partial class DevUIMiddleware
if (context.Request.Headers.IfNoneMatch == cacheEntry.ETag)
{
response.StatusCode = StatusCodes.Status304NotModified;
this._logger.LogDebug("Resource not modified (304): {ResourcePath}", resourcePath);
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("Resource not modified (304): {ResourcePath}", resourcePath);
}
return true;
}
@@ -161,12 +174,20 @@ internal sealed partial class DevUIMiddleware
await response.Body.WriteAsync(content, context.RequestAborted).ConfigureAwait(false);
this._logger.LogDebug("Served embedded resource: {ResourcePath} (compressed: {Compressed})", resourcePath, serveCompressed);
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("Served embedded resource: {ResourcePath} (compressed: {Compressed})", resourcePath, serveCompressed);
}
return true;
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error serving embedded resource: {ResourcePath}", resourcePath);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(ex, "Error serving embedded resource: {ResourcePath}", resourcePath);
}
return false;
}
}
@@ -98,8 +98,10 @@ public sealed class DurableAIAgent : AIAgent
responseFormat = chatClientOptions.ChatOptions?.ResponseFormat;
}
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames);
request.OrchestrationId = this._context.InstanceId;
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames)
{
OrchestrationId = this._context.InstanceId
};
try
{
@@ -19,7 +19,7 @@ internal sealed class EntityAgentWrapper(
private readonly IServiceProvider? _entityScopedServices = entityScopedServices;
// The ID of the agent is always the entity ID.
public override string Id => this._entityContext.Id.ToString();
protected override string? IdCore => this._entityContext.Id.ToString();
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
@@ -46,7 +46,7 @@ internal sealed class DurableAgentStateFunctionCallContent : DurableAgentStateCo
{
return new DurableAgentStateFunctionCallContent()
{
Arguments = content.Arguments?.ToImmutableDictionary() ?? ImmutableDictionary<string, object?>.Empty,
Arguments = content.Arguments?.ToDictionary() ?? [],
CallId = content.CallId,
Name = content.Name
};
@@ -20,8 +20,6 @@ internal sealed partial class AGUIServerSentEventsResult : IResult, IDisposable
private readonly ILogger<AGUIServerSentEventsResult> _logger;
private Utf8JsonWriter? _jsonWriter;
public int? StatusCode => StatusCodes.Status200OK;
internal AGUIServerSentEventsResult(IAsyncEnumerable<BaseEvent> events, ILogger<AGUIServerSentEventsResult> logger)
{
this._events = events;
@@ -59,7 +59,11 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
AIAgent? agent = this._serviceProvider.GetKeyedService<AIAgent>(agentName);
if (agent is null)
{
this._logger.LogWarning("Failed to resolve agent with name '{AgentName}'", agentName);
if (this._logger.IsEnabled(LogLevel.Warning))
{
this._logger.LogWarning("Failed to resolve agent with name '{AgentName}'", agentName);
}
return ValueTask.FromResult<ResponseError?>(new ResponseError
{
Code = "agent_not_found",
@@ -133,7 +133,7 @@ internal sealed class Mem0Client
[JsonPropertyName("agent_id")] public string? AgentId { get; set; }
[JsonPropertyName("run_id")] public string? RunId { get; set; }
[JsonPropertyName("user_id")] public string? UserId { get; set; }
[JsonPropertyName("messages")] public CreateMemoryMessage[] Messages { get; set; } = Array.Empty<CreateMemoryMessage>();
[JsonPropertyName("messages")] public CreateMemoryMessage[] Messages { get; set; } = [];
}
internal sealed class CreateMemoryMessage
@@ -153,7 +153,7 @@ public sealed class Mem0Provider : AIContextProvider
? null
: $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
if (this._logger is not null)
if (this._logger?.IsEnabled(LogLevel.Information) is true)
{
this._logger.LogInformation(
"Mem0AIContextProvider: Retrieved {Count} memories. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
@@ -162,7 +162,8 @@ public sealed class Mem0Provider : AIContextProvider
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
if (outputMessageText is not null)
if (outputMessageText is not null && this._logger.IsEnabled(LogLevel.Trace))
{
this._logger.LogTrace(
"Mem0AIContextProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\nApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
@@ -186,13 +187,16 @@ public sealed class Mem0Provider : AIContextProvider
}
catch (Exception ex)
{
this._logger?.LogError(
ex,
"Mem0AIContextProvider: Failed to search Mem0 for memories due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
if (this._logger?.IsEnabled(LogLevel.Error) is true)
{
this._logger.LogError(
ex,
"Mem0AIContextProvider: Failed to search Mem0 for memories due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
}
return new AIContext();
}
}
@@ -212,13 +216,16 @@ public sealed class Mem0Provider : AIContextProvider
}
catch (Exception ex)
{
this._logger?.LogError(
ex,
"Mem0AIContextProvider: Failed to send messages to Mem0 due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._storageScope.ApplicationId,
this._storageScope.AgentId,
this._storageScope.ThreadId,
this.SanitizeLogData(this._storageScope.UserId));
if (this._logger?.IsEnabled(LogLevel.Error) is true)
{
this._logger.LogError(
ex,
"Mem0AIContextProvider: Failed to send messages to Mem0 due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._storageScope.ApplicationId,
this._storageScope.AgentId,
this._storageScope.ThreadId,
this.SanitizeLogData(this._storageScope.UserId));
}
}
}
@@ -43,7 +43,10 @@ internal sealed class BackgroundJobRunner
}
catch (Exception e) when (e is not OperationCanceledException and not SystemException)
{
this._logger.LogError(e, "Error running background job {BackgroundJobError}.", e.Message);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(e, "Error running background job {BackgroundJobError}.", e.Message);
}
}
}
});
@@ -73,7 +73,10 @@ internal class ChannelHandler : IChannelHandler
}
catch (Exception e) when (this._purviewSettings.IgnoreExceptions)
{
this._logger.LogError(e, "Error queuing job: {ExceptionMessage}", e.Message);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(e, "Error queuing job: {ExceptionMessage}", e.Message);
}
}
}
@@ -38,16 +38,12 @@ public class PurviewAppLocation
/// <exception cref="InvalidOperationException">Thrown when an invalid location type is provided.</exception>
internal PolicyLocation GetPolicyLocation()
{
switch (this.LocationType)
return this.LocationType switch
{
case PurviewLocationType.Application:
return new PolicyLocation($"{Constants.ODataGraphNamespace}.policyLocationApplication", this.LocationValue);
case PurviewLocationType.Uri:
return new PolicyLocation($"{Constants.ODataGraphNamespace}.policyLocationUrl", this.LocationValue);
case PurviewLocationType.Domain:
return new PolicyLocation($"{Constants.ODataGraphNamespace}.policyLocationDomain", this.LocationValue);
default:
throw new InvalidOperationException("Invalid location type.");
}
PurviewLocationType.Application => new($"{Constants.ODataGraphNamespace}.policyLocationApplication", this.LocationValue),
PurviewLocationType.Uri => new($"{Constants.ODataGraphNamespace}.policyLocationUrl", this.LocationValue),
PurviewLocationType.Domain => new($"{Constants.ODataGraphNamespace}.policyLocationDomain", this.LocationValue),
_ => throw new InvalidOperationException("Invalid location type."),
};
}
}
@@ -58,7 +58,7 @@ internal sealed class PurviewClient : IPurviewClient
this._tokenCredential = tokenCredential;
this._httpClient = httpClient;
this._scopes = new string[] { $"https://{purviewSettings.GraphBaseUri.Host}/.default" };
this._scopes = [$"https://{purviewSettings.GraphBaseUri.Host}/.default"];
this._graphUri = purviewSettings.GraphBaseUri.ToString().TrimEnd('/');
this._logger = logger ?? NullLogger.Instance;
}
@@ -176,7 +176,11 @@ internal sealed class PurviewClient : IPurviewClient
throw new PurviewRequestException(DeserializeError);
}
this._logger.LogError("Failed to process content. Status code: {StatusCode}", response.StatusCode);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError("Failed to process content. Status code: {StatusCode}", response.StatusCode);
}
throw CreateExceptionForStatusCode(response.StatusCode, "processContent");
}
}
@@ -241,7 +245,11 @@ internal sealed class PurviewClient : IPurviewClient
throw new PurviewRequestException(DeserializeError);
}
this._logger.LogError("Failed to retrieve protection scopes. Status code: {StatusCode}", response.StatusCode);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError("Failed to retrieve protection scopes. Status code: {StatusCode}", response.StatusCode);
}
throw CreateExceptionForStatusCode(response.StatusCode, "protectionScopes/compute");
}
}
@@ -304,7 +312,11 @@ internal sealed class PurviewClient : IPurviewClient
throw new PurviewRequestException(DeserializeError);
}
this._logger.LogError("Failed to create content activities. Status code: {StatusCode}", response.StatusCode);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError("Failed to create content activities. Status code: {StatusCode}", response.StatusCode);
}
throw CreateExceptionForStatusCode(response.StatusCode, "contentActivities");
}
}
@@ -73,13 +73,20 @@ internal sealed class PurviewWrapper : IDisposable
(bool shouldBlockPrompt, resolvedUserId) = await this._scopedProcessor.ProcessMessagesAsync(messages, options?.ConversationId, Activity.UploadText, this._purviewSettings, null, cancellationToken).ConfigureAwait(false);
if (shouldBlockPrompt)
{
this._logger.LogInformation("Prompt blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedPromptMessage);
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation("Prompt blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedPromptMessage);
}
return new ChatResponse(new ChatMessage(ChatRole.System, this._purviewSettings.BlockedPromptMessage));
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error processing prompt: {ExceptionMessage}", ex.Message);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(ex, "Error processing prompt: {ExceptionMessage}", ex.Message);
}
if (!this._purviewSettings.IgnoreExceptions)
{
@@ -94,13 +101,20 @@ internal sealed class PurviewWrapper : IDisposable
(bool shouldBlockResponse, _) = await this._scopedProcessor.ProcessMessagesAsync(response.Messages, options?.ConversationId, Activity.UploadText, this._purviewSettings, resolvedUserId, cancellationToken).ConfigureAwait(false);
if (shouldBlockResponse)
{
this._logger.LogInformation("Response blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedResponseMessage);
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation("Response blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedResponseMessage);
}
return new ChatResponse(new ChatMessage(ChatRole.System, this._purviewSettings.BlockedResponseMessage));
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error processing response: {ExceptionMessage}", ex.Message);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(ex, "Error processing response: {ExceptionMessage}", ex.Message);
}
if (!this._purviewSettings.IgnoreExceptions)
{
@@ -132,13 +146,20 @@ internal sealed class PurviewWrapper : IDisposable
if (shouldBlockPrompt)
{
this._logger.LogInformation("Prompt blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedPromptMessage);
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation("Prompt blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedPromptMessage);
}
return new AgentRunResponse(new ChatMessage(ChatRole.System, this._purviewSettings.BlockedPromptMessage));
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error processing prompt: {ExceptionMessage}", ex.Message);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(ex, "Error processing prompt: {ExceptionMessage}", ex.Message);
}
if (!this._purviewSettings.IgnoreExceptions)
{
@@ -154,13 +175,20 @@ internal sealed class PurviewWrapper : IDisposable
if (shouldBlockResponse)
{
this._logger.LogInformation("Response blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedResponseMessage);
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation("Response blocked by policy. Sending message: {Message}", this._purviewSettings.BlockedResponseMessage);
}
return new AgentRunResponse(new ChatMessage(ChatRole.System, this._purviewSettings.BlockedResponseMessage));
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error processing response: {ExceptionMessage}", ex.Message);
if (this._logger.IsEnabled(LogLevel.Error))
{
this._logger.LogError(ex, "Error processing response: {ExceptionMessage}", ex.Message);
}
if (!this._purviewSettings.IgnoreExceptions)
{
@@ -242,23 +242,16 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
/// </summary>
/// <param name="pcResponse">The process content response which may contain DLP actions.</param>
/// <param name="actionInfos">DLP actions returned from protection scopes.</param>
/// <returns>The process content response with the protection scopes DLP actions added. Actions are deduplicated.</returns>
/// <returns>The process content response with the protection scopes DLP actions added.</returns>
private static ProcessContentResponse CombinePolicyActions(ProcessContentResponse pcResponse, List<DlpActionInfo>? actionInfos)
{
if (actionInfos == null || actionInfos.Count == 0)
if (actionInfos?.Count > 0)
{
return pcResponse;
pcResponse.PolicyActions = pcResponse.PolicyActions is null ?
actionInfos :
[.. pcResponse.PolicyActions, .. actionInfos];
}
if (pcResponse.PolicyActions == null)
{
pcResponse.PolicyActions = actionInfos;
return pcResponse;
}
List<DlpActionInfo> pcActionInfos = new(pcResponse.PolicyActions);
pcActionInfos.AddRange(actionInfos);
pcResponse.PolicyActions = pcActionInfos;
return pcResponse;
}
@@ -339,20 +332,14 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
/// <returns>The protection scopes activity.</returns>
private static ProtectionScopeActivities TranslateActivity(Activity activity)
{
switch (activity)
return activity switch
{
case Activity.Unknown:
return ProtectionScopeActivities.None;
case Activity.UploadText:
return ProtectionScopeActivities.UploadText;
case Activity.UploadFile:
return ProtectionScopeActivities.UploadFile;
case Activity.DownloadText:
return ProtectionScopeActivities.DownloadText;
case Activity.DownloadFile:
return ProtectionScopeActivities.DownloadFile;
default:
return ProtectionScopeActivities.UnknownFutureValue;
}
Activity.Unknown => ProtectionScopeActivities.None,
Activity.UploadText => ProtectionScopeActivities.UploadText,
Activity.UploadFile => ProtectionScopeActivities.UploadFile,
Activity.DownloadText => ProtectionScopeActivities.DownloadText,
Activity.DownloadFile => ProtectionScopeActivities.DownloadFile,
_ => ProtectionScopeActivities.UnknownFutureValue,
};
}
}
@@ -46,7 +46,7 @@ internal static class RepresentationExtensions
keySelector: sourceId => sourceId,
elementSelector: sourceId => workflow.Edges[sourceId].Select(ToEdgeInfo).ToList());
HashSet<RequestPortInfo> inputPorts = new(workflow.Ports.Values.Select(ToPortInfo));
HashSet<RequestPortInfo> inputPorts = [.. workflow.Ports.Values.Select(ToPortInfo)];
return new WorkflowInfo(executors, edges, inputPorts, workflow.StartExecutorId, workflow.OutputExecutors);
}
@@ -41,8 +41,8 @@ public sealed class EdgeConnection : IEquatable<EdgeConnection>
/// contains duplicate values.</exception>
public static EdgeConnection CreateChecked(List<string> sourceIds, List<string> sinkIds)
{
HashSet<string> sourceSet = new(Throw.IfNull(sourceIds));
HashSet<string> sinkSet = new(Throw.IfNull(sinkIds));
HashSet<string> sourceSet = [.. Throw.IfNull(sourceIds)];
HashSet<string> sinkSet = [.. Throw.IfNull(sinkIds)];
if (sourceSet.Count != sourceIds.Count)
{
@@ -14,7 +14,7 @@ internal sealed class FanInEdgeState
public FanInEdgeState(FanInEdgeData fanInEdge)
{
this.SourceIds = fanInEdge.SourceIds.ToArray();
this.Unseen = new(this.SourceIds);
this.Unseen = [.. this.SourceIds];
this._pendingMessages = [];
}
@@ -40,7 +40,7 @@ internal sealed class FanInEdgeState
if (this.Unseen.Count == 0)
{
List<PortableMessageEnvelope> takenMessages = Interlocked.Exchange(ref this._pendingMessages, []);
this.Unseen = new(this.SourceIds);
this.Unseen = [.. this.SourceIds];
if (takenMessages.Count == 0)
{
@@ -380,7 +380,7 @@ public class WorkflowBuilder
}
// Make sure that all nodes are connected to the start executor (transitively)
HashSet<string> remainingExecutors = new(this._executorBindings.Keys);
HashSet<string> remainingExecutors = [.. this._executorBindings.Keys];
Queue<string> toVisit = new([this._startExecutorId]);
if (!validateOrphans)
@@ -39,7 +39,7 @@ internal sealed class WorkflowHostAgent : AIAgent
this._describeTask = this._workflow.DescribeProtocolAsync().AsTask();
}
public override string Id => this._id ?? base.Id;
protected override string? IdCore => this._id;
public override string? Name { get; }
public override string? Description { get; }
@@ -121,7 +121,7 @@ public sealed partial class ChatClientAgent : AIAgent
public IChatClient ChatClient { get; }
/// <inheritdoc/>
public override string Id => this._agentOptions?.Id ?? base.Id;
protected override string? IdCore => this._agentOptions?.Id;
/// <inheritdoc/>
public override string? Name => this._agentOptions?.Name;
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides extension methods for configuring and customizing <see cref="AIAgentBuilder"/> instances.
/// </summary>
public static class FunctionInvocationDelegatingAgentBuilderExtensions
{
/// <summary>
/// Adds function invocation callbacks to the <see cref="AIAgent"/> pipeline that intercepts and processes <see cref="AIFunction"/> calls.
/// </summary>
/// <param name="builder">The <see cref="AIAgentBuilder"/> to which the function invocation callback is added.</param>
/// <param name="callback">
/// A delegate that processes function invocations. The delegate receives the <see cref="AIAgent"/> instance,
/// the function invocation context, and a continuation delegate representing the next callback in the pipeline.
/// It returns a task representing the result of the function invocation.
/// </param>
/// <returns>The <see cref="AIAgentBuilder"/> instance with the function invocation callback added, enabling method chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="callback"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// The callback must call the provided continuation delegate to proceed with the function invocation,
/// unless it intends to completely replace the function's behavior.
/// </para>
/// <para>
/// The inner agent or the pipeline wrapping it must include a <see cref="FunctionInvokingChatClient"/>. If one does not exist,
/// the <see cref="AIAgent"/> added to the pipline by this method will throw an exception when it is invoked.
/// </para>
/// </remarks>
public static AIAgentBuilder Use(this AIAgentBuilder builder, Func<AIAgent, FunctionInvocationContext, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>>, CancellationToken, ValueTask<object?>> callback)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(callback);
return builder.Use((innerAgent, _) =>
{
// Function calling requires a ChatClientAgent inner agent.
if (innerAgent.GetService<FunctionInvokingChatClient>() is null)
{
throw new InvalidOperationException($"The function invocation middleware can only be used with decorations of a {nameof(AIAgent)} that support usage of FunctionInvokingChatClient decorated chat clients.");
}
return new FunctionInvocationDelegatingAgent(innerAgent, callback);
});
}
}
@@ -0,0 +1,209 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Microsoft.Agents.AI;
/// <summary>
/// A delegating AI agent that logs agent operations to an <see cref="ILogger"/>.
/// </summary>
/// <remarks>
/// <para>
/// The provided implementation of <see cref="AIAgent"/> is thread-safe for concurrent use so long as the
/// <see cref="ILogger"/> employed is also thread-safe for concurrent use.
/// </para>
/// <para>
/// When the employed <see cref="ILogger"/> enables <see cref="LogLevel.Trace"/>, the contents of
/// messages, options, and responses are logged. These may contain sensitive application data.
/// <see cref="LogLevel.Trace"/> is disabled by default and should never be enabled in a production environment.
/// Messages and options are not logged at other logging levels.
/// </para>
/// </remarks>
public sealed partial class LoggingAgent : DelegatingAIAgent
{
/// <summary>An <see cref="ILogger"/> instance used for all logging.</summary>
private readonly ILogger _logger;
/// <summary>The <see cref="JsonSerializerOptions"/> to use for serialization of state written to the logger.</summary>
private JsonSerializerOptions _jsonSerializerOptions;
/// <summary>Initializes a new instance of the <see cref="LoggingAgent"/> class.</summary>
/// <param name="innerAgent">The underlying <see cref="AIAgent"/>.</param>
/// <param name="logger">An <see cref="ILogger"/> instance that will be used for all logging.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> or <paramref name="logger"/> is <see langword="null"/>.</exception>
public LoggingAgent(AIAgent innerAgent, ILogger logger)
: base(innerAgent)
{
this._logger = Throw.IfNull(logger);
this._jsonSerializerOptions = AgentJsonUtilities.DefaultOptions;
}
/// <summary>Gets or sets JSON serialization options to use when serializing logging data.</summary>
public JsonSerializerOptions JsonSerializerOptions
{
get => this._jsonSerializerOptions;
set => this._jsonSerializerOptions = Throw.IfNull(value);
}
/// <inheritdoc/>
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
if (this._logger.IsEnabled(LogLevel.Debug))
{
if (this._logger.IsEnabled(LogLevel.Trace))
{
this.LogInvokedSensitive(nameof(RunAsync), this.AsJson(messages), this.AsJson(options), this.AsJson(this.GetService<AIAgentMetadata>()));
}
else
{
this.LogInvoked(nameof(RunAsync));
}
}
try
{
AgentRunResponse response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
if (this._logger.IsEnabled(LogLevel.Debug))
{
if (this._logger.IsEnabled(LogLevel.Trace))
{
this.LogCompletedSensitive(nameof(RunAsync), this.AsJson(response));
}
else
{
this.LogCompleted(nameof(RunAsync));
}
}
return response;
}
catch (OperationCanceledException)
{
this.LogInvocationCanceled(nameof(RunAsync));
throw;
}
catch (Exception ex)
{
this.LogInvocationFailed(nameof(RunAsync), ex);
throw;
}
}
/// <inheritdoc/>
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (this._logger.IsEnabled(LogLevel.Debug))
{
if (this._logger.IsEnabled(LogLevel.Trace))
{
this.LogInvokedSensitive(nameof(RunStreamingAsync), this.AsJson(messages), this.AsJson(options), this.AsJson(this.GetService<AIAgentMetadata>()));
}
else
{
this.LogInvoked(nameof(RunStreamingAsync));
}
}
IAsyncEnumerator<AgentRunResponseUpdate> e;
try
{
e = base.RunStreamingAsync(messages, thread, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (OperationCanceledException)
{
this.LogInvocationCanceled(nameof(RunStreamingAsync));
throw;
}
catch (Exception ex)
{
this.LogInvocationFailed(nameof(RunStreamingAsync), ex);
throw;
}
try
{
AgentRunResponseUpdate? update = null;
while (true)
{
try
{
if (!await e.MoveNextAsync().ConfigureAwait(false))
{
break;
}
update = e.Current;
}
catch (OperationCanceledException)
{
this.LogInvocationCanceled(nameof(RunStreamingAsync));
throw;
}
catch (Exception ex)
{
this.LogInvocationFailed(nameof(RunStreamingAsync), ex);
throw;
}
if (this._logger.IsEnabled(LogLevel.Trace))
{
this.LogStreamingUpdateSensitive(this.AsJson(update));
}
yield return update;
}
this.LogCompleted(nameof(RunStreamingAsync));
}
finally
{
await e.DisposeAsync().ConfigureAwait(false);
}
}
private string AsJson<T>(T value)
{
try
{
return JsonSerializer.Serialize(value, this._jsonSerializerOptions.GetTypeInfo(typeof(T)));
}
catch
{
// If serialization fails, return a simple string representation
return value?.ToString() ?? "null";
}
}
[LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")]
private partial void LogInvoked(string methodName);
[LoggerMessage(LogLevel.Trace, "{MethodName} invoked: {Messages}. Options: {Options}. Metadata: {Metadata}.")]
private partial void LogInvokedSensitive(string methodName, string messages, string options, string metadata);
[LoggerMessage(LogLevel.Debug, "{MethodName} completed.")]
private partial void LogCompleted(string methodName);
[LoggerMessage(LogLevel.Trace, "{MethodName} completed: {Response}.")]
private partial void LogCompletedSensitive(string methodName, string response);
[LoggerMessage(LogLevel.Trace, "RunStreamingAsync received update: {Update}")]
private partial void LogStreamingUpdateSensitive(string update);
[LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")]
private partial void LogInvocationCanceled(string methodName);
[LoggerMessage(LogLevel.Error, "{MethodName} failed.")]
private partial void LogInvocationFailed(string methodName, Exception error);
}
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.Diagnostics;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides extension methods for adding logging support to <see cref="AIAgentBuilder"/> instances.
/// </summary>
public static class LoggingAgentBuilderExtensions
{
/// <summary>
/// Adds logging to the agent pipeline, enabling detailed observability of agent operations.
/// </summary>
/// <param name="builder">The <see cref="AIAgentBuilder"/> to which logging support will be added.</param>
/// <param name="loggerFactory">
/// An optional <see cref="ILoggerFactory"/> used to create a logger with which logging should be performed.
/// If not supplied, a required instance will be resolved from the service provider.
/// </param>
/// <param name="configure">
/// An optional callback that provides additional configuration of the <see cref="LoggingAgent"/> instance.
/// This allows for fine-tuning logging behavior such as customizing JSON serialization options.
/// </param>
/// <returns>The <see cref="AIAgentBuilder"/> with logging support added, enabling method chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// When the employed <see cref="ILogger"/> enables <see cref="LogLevel.Trace"/>, the contents of
/// messages, options, and responses are logged. These may contain sensitive application data.
/// <see cref="LogLevel.Trace"/> is disabled by default and should never be enabled in a production environment.
/// Messages and options are not logged at other logging levels.
/// </para>
/// <para>
/// If the resolved or provided <see cref="ILoggerFactory"/> is <see cref="NullLoggerFactory"/>, this will be a no-op where
/// logging will be effectively disabled. In this case, the <see cref="LoggingAgent"/> will not be added.
/// </para>
/// </remarks>
public static AIAgentBuilder UseLogging(
this AIAgentBuilder builder,
ILoggerFactory? loggerFactory = null,
Action<LoggingAgent>? configure = null)
{
_ = Throw.IfNull(builder);
return builder.Use((innerAgent, services) =>
{
loggerFactory ??= services.GetRequiredService<ILoggerFactory>();
// If the factory we resolve is for the null logger, the LoggingAgent will end up
// being an expensive nop, so skip adding it and just return the inner agent.
if (loggerFactory == NullLoggerFactory.Instance)
{
return innerAgent;
}
LoggingAgent agent = new(innerAgent, loggerFactory.CreateLogger(nameof(LoggingAgent)));
configure?.Invoke(agent);
return agent;
});
}
}
@@ -212,13 +212,17 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
}
catch (Exception ex)
{
this._logger?.LogError(
ex,
"ChatHistoryMemoryProvider: Failed to search for chat history due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
if (this._logger?.IsEnabled(LogLevel.Error) is true)
{
this._logger.LogError(
ex,
"ChatHistoryMemoryProvider: Failed to search for chat history due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
}
return new AIContext();
}
}
@@ -264,13 +268,16 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
}
catch (Exception ex)
{
this._logger?.LogError(
ex,
"ChatHistoryMemoryProvider: Failed to add messages to chat history vector store due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
if (this._logger?.IsEnabled(LogLevel.Error) is true)
{
this._logger.LogError(
ex,
"ChatHistoryMemoryProvider: Failed to add messages to chat history vector store due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
}
}
}
@@ -302,14 +309,18 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
var formatted = $"{this._contextPrompt}\n{outputResultsText}";
this._logger?.LogTrace(
"ChatHistoryMemoryProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\n ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this.SanitizeLogData(userQuestion),
this.SanitizeLogData(formatted),
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
if (this._logger?.IsEnabled(LogLevel.Trace) is true)
{
this._logger.LogTrace(
"ChatHistoryMemoryProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\n ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
this.SanitizeLogData(userQuestion),
this.SanitizeLogData(formatted),
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
}
return formatted;
}
@@ -383,13 +394,16 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
results.Add(result.Record);
}
this._logger?.LogInformation(
"ChatHistoryMemoryProvider: Retrieved {Count} search results. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
results.Count,
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
if (this._logger?.IsEnabled(LogLevel.Information) is true)
{
this._logger.LogInformation(
"ChatHistoryMemoryProvider: Retrieved {Count} search results. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'.",
results.Count,
this._searchScope.ApplicationId,
this._searchScope.AgentId,
this._searchScope.ThreadId,
this.SanitizeLogData(this._searchScope.UserId));
}
return results;
}
@@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.VectorData.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
</ItemGroup>
@@ -1,55 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides extension methods for configuring and customizing <see cref="AIAgentBuilder"/> instances.
/// Provides extension methods for adding OpenTelemetry instrumentation to <see cref="AIAgentBuilder"/> instances.
/// </summary>
public static class AIAgentBuilderExtensions
public static class OpenTelemetryAgentBuilderExtensions
{
/// <summary>
/// Adds function invocation callbacks to the <see cref="AIAgent"/> pipeline that intercepts and processes <see cref="AIFunction"/> calls.
/// </summary>
/// <param name="builder">The <see cref="AIAgentBuilder"/> to which the function invocation callback is added.</param>
/// <param name="callback">
/// A delegate that processes function invocations. The delegate receives the <see cref="AIAgent"/> instance,
/// the function invocation context, and a continuation delegate representing the next callback in the pipeline.
/// It returns a task representing the result of the function invocation.
/// </param>
/// <returns>The <see cref="AIAgentBuilder"/> instance with the function invocation callback added, enabling method chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="callback"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// The callback must call the provided continuation delegate to proceed with the function invocation,
/// unless it intends to completely replace the function's behavior.
/// </para>
/// <para>
/// The inner agent or the pipeline wrapping it must include a <see cref="FunctionInvokingChatClient"/>. If one does not exist,
/// the <see cref="AIAgent"/> added to the pipline by this method will throw an exception when it is invoked.
/// </para>
/// </remarks>
public static AIAgentBuilder Use(this AIAgentBuilder builder, Func<AIAgent, FunctionInvocationContext, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>>, CancellationToken, ValueTask<object?>> callback)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(callback);
return builder.Use((innerAgent, _) =>
{
// Function calling requires a ChatClientAgent inner agent.
if (innerAgent.GetService<FunctionInvokingChatClient>() is null)
{
throw new InvalidOperationException($"The function invocation middleware can only be used with decorations of a {nameof(AIAgent)} that support usage of FunctionInvokingChatClient decorated chat clients.");
}
return new FunctionInvocationDelegatingAgent(innerAgent, callback);
});
}
/// <summary>
/// Adds OpenTelemetry instrumentation to the agent pipeline, enabling comprehensive observability for agent operations.
/// </summary>
@@ -134,7 +134,11 @@ public sealed class TextSearchProvider : AIContextProvider
// Search
var results = await this._searchAsync(input, cancellationToken).ConfigureAwait(false);
IList<TextSearchResult> materialized = results as IList<TextSearchResult> ?? results.ToList();
this._logger?.LogInformation("TextSearchProvider: Retrieved {Count} search results.", materialized.Count);
if (this._logger?.IsEnabled(LogLevel.Information) is true)
{
this._logger?.LogInformation("TextSearchProvider: Retrieved {Count} search results.", materialized.Count);
}
if (materialized.Count == 0)
{
@@ -144,7 +148,10 @@ public sealed class TextSearchProvider : AIContextProvider
// Format search results
string formatted = this.FormatResults(materialized);
this._logger?.LogTrace("TextSearchProvider: Search Results\nInput:{Input}\nOutput:{MessageText}", input, formatted);
if (this._logger?.IsEnabled(LogLevel.Trace) is true)
{
this._logger.LogTrace("TextSearchProvider: Search Results\nInput:{Input}\nOutput:{MessageText}", input, formatted);
}
return new AIContext
{
@@ -230,8 +237,15 @@ public sealed class TextSearchProvider : AIContextProvider
IList<TextSearchResult> materialized = results as IList<TextSearchResult> ?? results.ToList();
string outputText = this.FormatResults(materialized);
this._logger?.LogInformation("TextSearchProvider: Retrieved {Count} search results.", materialized.Count);
this._logger?.LogTrace("TextSearchProvider Input:{UserQuestion}\nOutput:{MessageText}", userQuestion, outputText);
if (this._logger?.IsEnabled(LogLevel.Information) is true)
{
this._logger.LogInformation("TextSearchProvider: Retrieved {Count} search results.", materialized.Count);
if (this._logger.IsEnabled(LogLevel.Trace))
{
this._logger.LogTrace("TextSearchProvider Input:{UserQuestion}\nOutput:{MessageText}", userQuestion, outputText);
}
}
return outputText;
}
+4
View File
@@ -1,6 +1,10 @@
# Suppressing errors for Test projects under dotnet/tests folder
[*.cs]
dotnet_diagnostic.CA1822.severity = none # Member does not access instance data and can be marked as static
dotnet_diagnostic.CA1873.severity = none # Evaluation of logging arguments may be expensive
dotnet_diagnostic.CA1875.severity = none # Regex.IsMatch/Count instead of Regex.Match(...).Success/Regex.Matches(...).Count
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
dotnet_diagnostic.CA2249.severity = none # Use `string.Contains` instead of `string.IndexOf` to improve readability
dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
@@ -547,7 +547,7 @@ public sealed class A2AAgentTests : IDisposable
var result = await this._agent.RunAsync("Test message");
// Assert
if (taskState == TaskState.Submitted || taskState == TaskState.Working)
if (taskState is TaskState.Submitted or TaskState.Working)
{
Assert.NotNull(result.ContinuationToken);
}
@@ -64,12 +64,12 @@ public sealed class A2AArtifactExtensionsTests
{
ArtifactId = "artifact-ai-multi",
Name = "test",
Parts = new List<Part>
{
Parts =
[
new TextPart { Text = "Part 1" },
new TextPart { Text = "Part 2" },
new TextPart { Text = "Part 3" }
},
],
Metadata = null
};
@@ -93,7 +93,7 @@ public sealed class A2AArtifactExtensionsTests
{
ArtifactId = "artifact-empty",
Name = "test",
Parts = new List<Part>(),
Parts = [],
Metadata = null
};
@@ -919,7 +919,7 @@ public sealed class AGUIAgentTests
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Test")];
// Act - First turn
List<ChatMessage> conversation = new(messages);
List<ChatMessage> conversation = [.. messages];
string? conversationId = null;
await foreach (var update in chatClient.GetStreamingResponseAsync(conversation, options))
{
@@ -214,13 +214,31 @@ public class AIAgentTests
[Fact]
public void ValidateAgentIDIsIdempotent()
{
// Arrange
var agent = new MockAgent();
// Act
string id = agent.Id;
// Assert
Assert.NotNull(id);
Assert.Equal(id, agent.Id);
}
[Fact]
public void ValidateAgentIDCanBeProvidedByDerivedAgentClass()
{
// Arrange
var agent = new MockAgent(id: "test-agent-id");
// Act
string id = agent.Id;
// Assert
Assert.NotNull(id);
Assert.Equal("test-agent-id", id);
}
#region GetService Method Tests
/// <summary>
@@ -344,6 +362,13 @@ public class AIAgentTests
private sealed class MockAgent : AIAgent
{
public MockAgent(string? id = null)
{
this.IdCore = id;
}
protected override string? IdCore { get; }
public override AgentThread GetNewThread()
=> throw new NotImplementedException();
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -31,7 +32,7 @@ public class DelegatingAIAgentTests
this._testThread = new TestAgentThread();
// Setup inner agent mock
this._innerAgentMock.Setup(x => x.Id).Returns("test-agent-id");
this._innerAgentMock.Protected().SetupGet<string>("IdCore").Returns("test-agent-id");
this._innerAgentMock.Setup(x => x.Name).Returns("Test Agent");
this._innerAgentMock.Setup(x => x.Description).Returns("Test Description");
this._innerAgentMock.Setup(x => x.GetNewThread()).Returns(this._testThread);
@@ -93,7 +94,7 @@ public class DelegatingAIAgentTests
// Assert
Assert.Equal("test-agent-id", id);
this._innerAgentMock.Verify(x => x.Id, Times.Once);
this._innerAgentMock.Protected().VerifyGet<string>("IdCore", Times.Once());
}
/// <summary>
@@ -2684,13 +2684,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
private sealed class MockPipelineResponse : PipelineResponse
{
private readonly int _status;
private readonly BinaryData _content;
private readonly MockPipelineResponseHeaders _headers;
public MockPipelineResponse(int status, BinaryData? content = null)
{
this._status = status;
this._content = content ?? BinaryData.Empty;
this.Content = content ?? BinaryData.Empty;
this._headers = new MockPipelineResponseHeaders();
}
@@ -2704,7 +2703,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
set { }
}
public override BinaryData Content => this._content;
public override BinaryData Content { get; }
protected override PipelineResponseHeaders HeadersCore => this._headers;
@@ -8,7 +8,6 @@ using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.AI;
using Xunit;
@@ -81,7 +80,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
throughput: 400);
// Create container for hierarchical partitioning tests with hierarchical partition key
var hierarchicalContainerProperties = new ContainerProperties(HierarchicalTestContainerId, new List<string> { "/tenantId", "/userId", "/sessionId" });
var hierarchicalContainerProperties = new ContainerProperties(HierarchicalTestContainerId, ["/tenantId", "/userId", "/sessionId"]);
await databaseResponse.Database.CreateContainerIfNotExistsAsync(
hierarchicalContainerProperties,
throughput: 400);
@@ -247,7 +246,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
PartitionKey = new PartitionKey(conversationId)
});
List<dynamic> rawResults = new();
List<dynamic> rawResults = [];
while (rawIterator.HasMoreResults)
{
var rawResponse = await rawIterator.ReadNextAsync();
@@ -77,7 +77,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable
this._emulatorAvailable = true;
}
catch (Exception ex) when (!(ex is OutOfMemoryException || ex is StackOverflowException || ex is AccessViolationException))
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
// Emulator not available, tests will be skipped
this._emulatorAvailable = false;
@@ -170,7 +170,7 @@ internal static class PromptAgents
topP: 0.95
connection:
kind: apiKey
key: =Env.OPENAI_APIKEY
key: =Env.OPENAI_API_KEY
outputSchema:
properties:
language:
@@ -276,15 +276,9 @@ public sealed class BasicStreamingTests : IAsyncDisposable
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via dependency injection")]
internal sealed class FakeChatClientAgent : AIAgent
{
public FakeChatClientAgent()
{
this.Id = "fake-agent";
this.Description = "A fake agent for testing";
}
protected override string? IdCore => "fake-agent";
public override string Id { get; }
public override string? Description { get; }
public override string? Description => "A fake agent for testing";
public override AgentThread GetNewThread()
{
@@ -350,15 +344,9 @@ internal sealed class FakeChatClientAgent : AIAgent
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via dependency injection")]
internal sealed class FakeMultiMessageAgent : AIAgent
{
public FakeMultiMessageAgent()
{
this.Id = "fake-multi-message-agent";
this.Description = "A fake agent that sends multiple messages for testing";
}
protected override string? IdCore => "fake-multi-message-agent";
public override string Id { get; }
public override string? Description { get; }
public override string? Description => "A fake agent that sends multiple messages for testing";
public override AgentThread GetNewThread()
{
@@ -421,7 +421,7 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
private sealed class MultiResponseAgent : AIAgent
{
public override string Id => "multi-response-agent";
protected override string? IdCore => "multi-response-agent";
public override string? Description => "Agent that produces multiple text chunks";
@@ -510,7 +510,7 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
private sealed class TestAgent : AIAgent
{
public override string Id => "test-agent";
protected override string? IdCore => "test-agent";
public override string? Description => "Test agent";
@@ -100,9 +100,6 @@ public sealed class AGUIServerSentEventsResultTests
// Act
await result.ExecuteAsync(httpContext);
// Assert
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
}
[Fact]
@@ -49,14 +49,14 @@ public sealed class Mem0ProviderTests : IDisposable
var sut = new Mem0Provider(this._httpClient, storageScope);
await sut.ClearStoredMemoriesAsync();
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }));
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(new[] { input }, aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext([input], aiContextProviderMessages: null));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
await sut.ClearStoredMemoriesAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }));
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
@@ -73,14 +73,14 @@ public sealed class Mem0ProviderTests : IDisposable
var sut = new Mem0Provider(this._httpClient, storageScope);
await sut.ClearStoredMemoriesAsync();
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }));
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(new[] { assistantIntro }, aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
await sut.ClearStoredMemoriesAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }));
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
@@ -99,13 +99,13 @@ public sealed class Mem0ProviderTests : IDisposable
await sut1.ClearStoredMemoriesAsync();
await sut2.ClearStoredMemoriesAsync();
var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }));
var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }));
var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext([question]));
Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
// Act
await sut1.InvokedAsync(new AIContextProvider.InvokedContext(new[] { assistantIntro }, aiContextProviderMessages: null));
await sut1.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
var ctxAfterAdding1 = await GetContextWithRetryAsync(sut1, question);
var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, question);
@@ -123,7 +123,7 @@ public sealed class Mem0ProviderTests : IDisposable
AIContext? ctx = null;
for (int i = 0; i < attempts; i++)
{
ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question }), CancellationToken.None);
ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext([question]), CancellationToken.None);
var text = ctx.Messages?[0].Text;
if (!string.IsNullOrEmpty(text) && text.IndexOf("Caoimhe", StringComparison.OrdinalIgnoreCase) >= 0)
{
@@ -35,6 +35,10 @@ public sealed class Mem0ProviderTests : IDisposable
.Setup(f => f.CreateLogger(typeof(Mem0Provider).FullName!))
.Returns(this._loggerMock.Object);
this._loggerMock
.Setup(f => f.IsEnabled(It.IsAny<LogLevel>()))
.Returns(true);
this._httpClient = new HttpClient(this._handler)
{
BaseAddress = new Uri("https://localhost/")
@@ -131,10 +135,10 @@ public sealed class Mem0ProviderTests : IDisposable
}
[Theory]
[InlineData(false, false, 2)]
[InlineData(true, false, 2)]
[InlineData(false, true, 1)]
[InlineData(true, true, 1)]
[InlineData(false, false, 4)]
[InlineData(true, false, 4)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokingAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations)
{
// Arrange
@@ -157,7 +161,7 @@ public sealed class Mem0ProviderTests : IDisposable
var options = new Mem0ProviderOptions { EnableSensitiveTelemetryData = enableSensitiveTelemetryData };
var sut = new Mem0Provider(this._httpClient, storageScope, options: options, loggerFactory: this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Who am I?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Who am I?")]);
// Act
await sut.InvokingAsync(invokingContext, CancellationToken.None);
@@ -166,7 +170,12 @@ public sealed class Mem0ProviderTests : IDisposable
Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
var state = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2]);
if (logInvocation.Method.Name == nameof(ILogger.IsEnabled))
{
continue;
}
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user" : "<redacted>", userIdValue);
@@ -275,8 +284,8 @@ public sealed class Mem0ProviderTests : IDisposable
[Theory]
[InlineData(false, false, 0)]
[InlineData(true, false, 0)]
[InlineData(false, true, 1)]
[InlineData(true, true, 1)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokedAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogCount)
{
// Arrange
@@ -315,7 +324,12 @@ public sealed class Mem0ProviderTests : IDisposable
Assert.Equal(expectedLogCount, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
var state = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2]);
if (logInvocation.Method.Name == nameof(ILogger.IsEnabled))
{
continue;
}
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user" : "<redacted>", userIdValue);
}
@@ -386,7 +400,7 @@ public sealed class Mem0ProviderTests : IDisposable
// Arrange
var storageScope = new Mem0ProviderScope { ApplicationId = "app" };
var provider = new Mem0Provider(this._httpClient, storageScope, loggerFactory: this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
@@ -50,10 +49,10 @@ public sealed class PurviewClientTests : IDisposable
{
Id = "test-id-123",
ProtectionScopeState = ProtectionScopeState.NotModified,
PolicyActions = new List<DlpActionInfo>
{
PolicyActions =
[
new() { Action = DlpAction.NotifyUser }
}
]
};
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
@@ -228,8 +227,8 @@ public sealed class PurviewClientTests : IDisposable
var expectedResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -238,7 +237,7 @@ public sealed class PurviewClientTests : IDisposable
new ("microsoft.graph.policyLocationApplication", "app-123")
]
}
}
]
};
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
@@ -264,7 +263,7 @@ public sealed class PurviewClientTests : IDisposable
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id");
var expectedResponse = new ProtectionScopesResponse { Scopes = new List<PolicyScopeBase>() };
var expectedResponse = new ProtectionScopesResponse { Scopes = [] };
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProtectionScopesResponse)));
@@ -56,8 +56,8 @@ public sealed class ScopedContentProcessorTests
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -67,7 +67,7 @@ public sealed class ScopedContentProcessorTests
],
ExecutionMode = ExecutionMode.EvaluateInline
}
}
]
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
@@ -76,10 +76,10 @@ public sealed class ScopedContentProcessorTests
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>
{
PolicyActions =
[
new() { Action = DlpAction.BlockAccess }
}
]
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
@@ -115,8 +115,8 @@ public sealed class ScopedContentProcessorTests
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -126,7 +126,7 @@ public sealed class ScopedContentProcessorTests
],
ExecutionMode = ExecutionMode.EvaluateInline
}
}
]
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
@@ -135,10 +135,10 @@ public sealed class ScopedContentProcessorTests
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>
{
PolicyActions =
[
new() { RestrictionAction = RestrictionAction.Block }
}
]
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
@@ -174,8 +174,8 @@ public sealed class ScopedContentProcessorTests
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -185,7 +185,7 @@ public sealed class ScopedContentProcessorTests
],
ExecutionMode = ExecutionMode.EvaluateInline
}
}
]
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
@@ -194,10 +194,10 @@ public sealed class ScopedContentProcessorTests
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>
{
PolicyActions =
[
new() { Action = DlpAction.NotifyUser }
}
]
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
@@ -229,8 +229,8 @@ public sealed class ScopedContentProcessorTests
var cachedPsResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -240,7 +240,7 @@ public sealed class ScopedContentProcessorTests
],
ExecutionMode = ExecutionMode.EvaluateInline
}
}
]
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
@@ -249,7 +249,7 @@ public sealed class ScopedContentProcessorTests
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>()
PolicyActions = []
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
@@ -285,8 +285,8 @@ public sealed class ScopedContentProcessorTests
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -296,7 +296,7 @@ public sealed class ScopedContentProcessorTests
],
ExecutionMode = ExecutionMode.EvaluateInline
}
}
]
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
@@ -306,7 +306,7 @@ public sealed class ScopedContentProcessorTests
var pcResponse = new ProcessContentResponse
{
ProtectionScopeState = ProtectionScopeState.Modified,
PolicyActions = new List<DlpActionInfo>()
PolicyActions = []
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
@@ -342,8 +342,8 @@ public sealed class ScopedContentProcessorTests
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
@@ -352,7 +352,7 @@ public sealed class ScopedContentProcessorTests
new ("microsoft.graph.policyLocationApplication", "app-456")
]
}
}
]
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
@@ -436,7 +436,7 @@ public sealed class ScopedContentProcessorTests
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse { Scopes = new List<PolicyScopeBase>() };
var psResponse = new ProtectionScopesResponse { Scopes = [] };
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
@@ -471,7 +471,7 @@ public sealed class ScopedContentProcessorTests
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse { Scopes = new List<PolicyScopeBase>() };
var psResponse = new ProtectionScopesResponse { Scopes = [] };
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
@@ -30,6 +30,10 @@ public sealed class TextSearchProviderTests
this._loggerFactoryMock
.Setup(f => f.CreateLogger(typeof(TextSearchProvider).FullName!))
.Returns(this._loggerMock.Object);
this._loggerMock
.Setup(f => f.IsEnabled(It.IsAny<LogLevel>()))
.Returns(true);
}
[Theory]
@@ -135,7 +139,7 @@ public sealed class TextSearchProviderTests
FunctionToolDescription = overrideDescription
};
var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -154,7 +158,7 @@ public sealed class TextSearchProviderTests
{
// Arrange
var provider = new TextSearchProvider(this.FailingSearchAsync, default, null, loggerFactory: this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -247,7 +251,7 @@ public sealed class TextSearchProviderTests
ContextFormatter = r => $"Custom formatted context with {r.Count} results."
};
var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -281,7 +285,7 @@ public sealed class TextSearchProviderTests
ContextFormatter = r => string.Join(",", r.Select(x => ((RawPayload)x.RawRepresentation!).Id))
};
var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -298,7 +302,7 @@ public sealed class TextSearchProviderTests
// Arrange
var options = new TextSearchProviderOptions { SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke };
var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options);
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -338,10 +342,10 @@ public sealed class TextSearchProviderTests
};
await provider.InvokedAsync(new(initialMessages, aiContextProviderMessages: null) { InvokeException = new InvalidOperationException("Request Failed") });
var invokingContext = new AIContextProvider.InvokingContext(new[]
{
var invokingContext = new AIContextProvider.InvokingContext(
[
new ChatMessage(ChatRole.User, "E")
});
]);
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -378,10 +382,10 @@ public sealed class TextSearchProviderTests
};
await provider.InvokedAsync(new(initialMessages, aiContextProviderMessages: null));
var invokingContext = new AIContextProvider.InvokingContext(new[]
{
var invokingContext = new AIContextProvider.InvokingContext(
[
new ChatMessage(ChatRole.User, "E")
});
]);
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -409,21 +413,21 @@ public sealed class TextSearchProviderTests
var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options);
// First memory update (A,B)
await provider.InvokedAsync(new(new[]
{
await provider.InvokedAsync(new(
[
new ChatMessage(ChatRole.User, "A"),
new ChatMessage(ChatRole.Assistant, "B"),
}, aiContextProviderMessages: null));
], aiContextProviderMessages: null));
// Second memory update (C,D,E)
await provider.InvokedAsync(new(new[]
{
await provider.InvokedAsync(new(
[
new ChatMessage(ChatRole.User, "C"),
new ChatMessage(ChatRole.Assistant, "D"),
new ChatMessage(ChatRole.User, "E"),
}, aiContextProviderMessages: null));
], aiContextProviderMessages: null));
var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "F") });
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "F")]);
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -460,10 +464,10 @@ public sealed class TextSearchProviderTests
};
await provider.InvokedAsync(new(initialMessages, null));
var invokingContext = new AIContextProvider.InvokingContext(new[]
{
var invokingContext = new AIContextProvider.InvokingContext(
[
new ChatMessage(ChatRole.User, "Question?") // Current request message always appended.
});
]);
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="LoggingAgentBuilderExtensions"/> UseLogging extension method.
/// </summary>
public class LoggingAgentBuilderExtensionsTests
{
/// <summary>
/// Verify that UseLogging throws ArgumentNullException when builder is null.
/// </summary>
[Fact]
public void UseLogging_WithNullBuilder_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>("builder", () => ((AIAgentBuilder)null!).UseLogging());
}
/// <summary>
/// Verify that UseLogging returns a LoggingAgent when logger factory is provided.
/// </summary>
[Fact]
public void UseLogging_WithLoggerFactory_ReturnsLoggingAgent()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var builder = new AIAgentBuilder(mockAgent.Object);
using var loggerFactory = LoggerFactory.Create(builder => { });
// Act
AIAgent result = builder.UseLogging(loggerFactory: loggerFactory).Build();
// Assert
Assert.IsType<LoggingAgent>(result);
}
/// <summary>
/// Verify that UseLogging returns the inner agent when NullLoggerFactory is provided.
/// </summary>
[Fact]
public void UseLogging_WithNullLoggerFactory_ReturnsInnerAgent()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var builder = new AIAgentBuilder(mockAgent.Object);
// Act
AIAgent result = builder.UseLogging(loggerFactory: NullLoggerFactory.Instance).Build();
// Assert
Assert.NotNull(result);
Assert.IsNotType<LoggingAgent>(result);
}
/// <summary>
/// Verify that UseLogging with configure action works correctly.
/// </summary>
[Fact]
public void UseLogging_WithConfigureAction_CallsConfigureAction()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var builder = new AIAgentBuilder(mockAgent.Object);
using var loggerFactory = LoggerFactory.Create(builder => { });
var configureWasCalled = false;
// Act
AIAgent result = builder.UseLogging(
loggerFactory: loggerFactory,
configure: agent =>
{
configureWasCalled = true;
Assert.NotNull(agent);
Assert.IsType<LoggingAgent>(agent);
}).Build();
// Assert
Assert.True(configureWasCalled);
Assert.IsType<LoggingAgent>(result);
}
/// <summary>
/// Verify that UseLogging returns the same builder instance for chaining.
/// </summary>
[Fact]
public void UseLogging_ReturnsBuilderForChaining()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var builder = new AIAgentBuilder(mockAgent.Object);
using var loggerFactory = LoggerFactory.Create(builder => { });
// Act
AIAgentBuilder result = builder.UseLogging(loggerFactory: loggerFactory);
// Assert
Assert.Same(builder, result);
}
/// <summary>
/// Verify that UseLogging with all parameters works correctly.
/// </summary>
[Fact]
public void UseLogging_WithAllParameters_WorksCorrectly()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
using var loggerFactory = LoggerFactory.Create(builder => { });
var builder = new AIAgentBuilder(mockAgent.Object);
var configureWasCalled = false;
// Act
AIAgent result = builder.UseLogging(
loggerFactory: loggerFactory,
configure: agent =>
{
configureWasCalled = true;
Assert.NotNull(agent);
}).Build();
// Assert
Assert.True(configureWasCalled);
Assert.IsType<LoggingAgent>(result);
}
/// <summary>
/// Verify that UseLogging resolves ILoggerFactory from service provider when not provided.
/// </summary>
[Fact]
public void UseLogging_WithoutLoggerFactory_ResolvesFromServiceProvider()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var builder = new AIAgentBuilder(mockAgent.Object);
var services = new ServiceCollection();
using var loggerFactory = LoggerFactory.Create(builder => { });
services.AddSingleton(loggerFactory);
builder.Use((innerAgent, serviceProvider) =>
{
Assert.NotNull(serviceProvider);
return innerAgent;
});
// Act
AIAgent result = builder.UseLogging().Build(services.BuildServiceProvider());
// Assert
Assert.IsType<LoggingAgent>(result);
}
/// <summary>
/// Verify that UseLogging with configure action can customize JsonSerializerOptions.
/// </summary>
[Fact]
public void UseLogging_ConfigureJsonSerializerOptions_WorksCorrectly()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var builder = new AIAgentBuilder(mockAgent.Object);
using var loggerFactory = LoggerFactory.Create(builder => { });
var customOptions = new System.Text.Json.JsonSerializerOptions();
// Act
AIAgent result = builder.UseLogging(
loggerFactory: loggerFactory,
configure: agent => agent.JsonSerializerOptions = customOptions).Build();
// Assert
Assert.IsType<LoggingAgent>(result);
Assert.Same(customOptions, ((LoggingAgent)result).JsonSerializerOptions);
}
}
@@ -0,0 +1,401 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="LoggingAgent"/> class.
/// </summary>
public class LoggingAgentTests
{
[Fact]
public void Ctor_InvalidArgs_Throws()
{
var mockLogger = new Mock<ILogger>();
Assert.Throws<ArgumentNullException>("innerAgent", () => new LoggingAgent(null!, mockLogger.Object));
Assert.Throws<ArgumentNullException>("logger", () => new LoggingAgent(new TestAIAgent(), null!));
}
[Fact]
public void Properties_DelegateToInnerAgent()
{
// Arrange
TestAIAgent innerAgent = new()
{
NameFunc = () => "TestAgent",
DescriptionFunc = () => "This is a test agent.",
};
var mockLogger = new Mock<ILogger>();
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
// Act & Assert
Assert.Equal("TestAgent", agent.Name);
Assert.Equal("This is a test agent.", agent.Description);
Assert.Equal(innerAgent.Id, agent.Id);
Assert.Equal(innerAgent.DisplayName, agent.DisplayName);
}
[Fact]
public void JsonSerializerOptions_Roundtrips()
{
// Arrange
var mockLogger = new Mock<ILogger>();
var agent = new LoggingAgent(new TestAIAgent(), mockLogger.Object);
JsonSerializerOptions options = new();
// Act
agent.JsonSerializerOptions = options;
// Assert
Assert.Same(options, agent.JsonSerializerOptions);
}
[Fact]
public void JsonSerializerOptions_SetNull_Throws()
{
// Arrange
var mockLogger = new Mock<ILogger>();
var agent = new LoggingAgent(new TestAIAgent(), mockLogger.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => agent.JsonSerializerOptions = null!);
}
[Fact]
public async Task RunAsync_LogsAtDebugLevelAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(false);
var innerAgent = new TestAIAgent
{
RunAsyncFunc = async (messages, thread, options, cancellationToken) =>
{
await Task.Yield();
return new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
}
};
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
await agent.RunAsync(messages);
// Assert
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync invoked")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync completed")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task RunAsync_LogsAtTraceLevel_IncludesSensitiveDataAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(true);
var innerAgent = new TestAIAgent
{
RunAsyncFunc = async (messages, thread, options, cancellationToken) =>
{
await Task.Yield();
return new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
}
};
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
await agent.RunAsync(messages);
// Assert
mockLogger.Verify(
l => l.Log(
LogLevel.Trace,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync invoked")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Trace,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync completed")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task RunAsync_OnCancellation_LogsCanceledAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
var innerAgent = new TestAIAgent
{
RunAsyncFunc = (messages, thread, options, cancellationToken) =>
throw new OperationCanceledException()
};
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() => agent.RunAsync(messages));
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("canceled")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task RunAsync_OnException_LogsFailedAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
mockLogger.Setup(l => l.IsEnabled(LogLevel.Error)).Returns(true);
var innerAgent = new TestAIAgent
{
RunAsyncFunc = (messages, thread, options, cancellationToken) =>
throw new InvalidOperationException("Test exception")
};
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(messages));
mockLogger.Verify(
l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("failed")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_LogsAtDebugLevelAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(false);
var innerAgent = new TestAIAgent
{
RunStreamingAsyncFunc = CallbackAsync
};
static async IAsyncEnumerable<AgentRunResponseUpdate> CallbackAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
yield return new AgentRunResponseUpdate(ChatRole.Assistant, "Test");
}
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
await foreach (var update in agent.RunStreamingAsync(messages))
{
// Consume the stream
}
// Assert
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunStreamingAsync invoked")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunStreamingAsync completed")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_LogsUpdatesAtTraceLevelAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(true);
var innerAgent = new TestAIAgent
{
RunStreamingAsyncFunc = CallbackAsync
};
static async IAsyncEnumerable<AgentRunResponseUpdate> CallbackAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
yield return new AgentRunResponseUpdate(ChatRole.Assistant, "Update 1");
yield return new AgentRunResponseUpdate(ChatRole.Assistant, "Update 2");
}
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
await foreach (var update in agent.RunStreamingAsync(messages))
{
// Consume the stream
}
// Assert
mockLogger.Verify(
l => l.Log(
LogLevel.Trace,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("received update")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Exactly(2));
}
[Fact]
public async Task RunStreamingAsync_OnCancellation_LogsCanceledAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
var innerAgent = new TestAIAgent
{
RunStreamingAsyncFunc = CallbackAsync
};
static async IAsyncEnumerable<AgentRunResponseUpdate> CallbackAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
throw new OperationCanceledException();
// The following yield statement is required for async iterator methods but is unreachable.
// This pattern is intentional for testing exception scenarios in async iterators.
#pragma warning disable CS0162 // Unreachable code detected
yield break;
#pragma warning restore CS0162 // Unreachable code detected
}
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(messages))
{
// Consume the stream
}
});
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("canceled")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_OnException_LogsFailedAsync()
{
// Arrange
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
mockLogger.Setup(l => l.IsEnabled(LogLevel.Error)).Returns(true);
var innerAgent = new TestAIAgent
{
RunStreamingAsyncFunc = CallbackAsync
};
static async IAsyncEnumerable<AgentRunResponseUpdate> CallbackAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
throw new InvalidOperationException("Test exception");
// The following yield statement is required for async iterator methods but is unreachable.
// This pattern is intentional for testing exception scenarios in async iterators.
#pragma warning disable CS0162 // Unreachable code detected
yield break;
#pragma warning restore CS0162 // Unreachable code detected
}
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(messages))
{
// Consume the stream
}
});
mockLogger.Verify(
l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("failed")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
}
@@ -36,6 +36,10 @@ public class ChatHistoryMemoryProviderTests
.Setup(f => f.CreateLogger(typeof(ChatHistoryMemoryProvider).FullName!))
.Returns(this._loggerMock.Object);
this._loggerMock
.Setup(f => f.IsEnabled(It.IsAny<LogLevel>()))
.Returns(true);
this._vectorStoreCollectionMock = new(MockBehavior.Strict);
this._vectorStoreMock = new(MockBehavior.Strict);
@@ -218,8 +222,8 @@ public class ChatHistoryMemoryProviderTests
[Theory]
[InlineData(false, false, 0)]
[InlineData(true, false, 0)]
[InlineData(false, true, 1)]
[InlineData(true, true, 1)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokedAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations)
{
// Arrange
@@ -259,6 +263,11 @@ public class ChatHistoryMemoryProviderTests
Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
if (logInvocation.Method.Name == nameof(ILogger.IsEnabled))
{
continue;
}
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user1" : "<redacted>", userIdValue);
@@ -385,10 +394,10 @@ public class ChatHistoryMemoryProviderTests
}
[Theory]
[InlineData(false, false, 1)]
[InlineData(true, false, 1)]
[InlineData(false, true, 1)]
[InlineData(true, true, 1)]
[InlineData(false, false, 2)]
[InlineData(true, false, 2)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokingAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations)
{
// Arrange
@@ -442,7 +451,12 @@ public class ChatHistoryMemoryProviderTests
Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
var state = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2]);
if (logInvocation.Method.Name == nameof(ILogger.IsEnabled))
{
continue;
}
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user1" : "<redacted>", userIdValue);
@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
@@ -7,9 +7,9 @@ using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AIAgentBuilderExtensions"/> class.
/// Unit tests for the <see cref="OpenTelemetryAgentBuilderExtensions"/> class.
/// </summary>
public class OpenTelemetryAIAgentBuilderExtensionsTests
public class OpenTelemetryAgentBuilderExtensionsTests
{
/// <summary>
/// Verify that UseOpenTelemetry throws ArgumentNullException when builder is null.
@@ -57,7 +57,7 @@ internal sealed class HelloAgent(string id = nameof(HelloAgent)) : AIAgent
public const string Greeting = "Hello World!";
public const string DefaultId = nameof(HelloAgent);
public override string Id => id;
protected override string? IdCore => id;
public override string? Name => id;
public override AgentThread GetNewThread()
@@ -19,7 +19,7 @@ public class SpecializedExecutorSmokeTests
{
public class TestAIAgent(List<ChatMessage>? messages = null, string? id = null, string? name = null) : AIAgent
{
public override string Id => id ?? base.Id;
protected override string? IdCore => id;
public override string? Name => name;
public static List<ChatMessage> ToChatMessages(params string[] messages)
@@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Workflows.UnitTests;
internal class TestEchoAgent(string? id = null, string? name = null, string? prefix = null) : AIAgent
{
public override string Id => id ?? base.Id;
protected override string? IdCore => id;
public override string? Name => name ?? base.Name;
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
@@ -57,7 +57,7 @@ internal class TestEchoAgent(string? id = null, string? name = null, string? pre
protected virtual IEnumerable<ChatMessage> GetEpilogueMessages(AgentRunOptions? options = null)
{
return Enumerable.Empty<ChatMessage>();
return [];
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+21 -1
View File
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0b251211] - 2025-12-11
### Added
- **agent-framework-core**: Extend HITL support for all orchestration patterns (#2620)
- **agent-framework-core**: Add factory pattern to concurrent orchestration builder (#2738)
- **agent-framework-core**: Add factory pattern to sequential orchestration builder (#2710)
- **agent-framework-azure-ai**: Capture file IDs from code interpreter in streaming responses (#2741)
### Changed
- **agent-framework-azurefunctions**: Change DurableAIAgent log level from warning to debug when invoked without thread (#2736)
### Fixed
- **agent-framework-core**: Added more complete parsing for mcp tool arguments (#2756)
- **agent-framework-core**: Fix GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outputs (#2750)
- **samples**: Standardize OpenAI API key environment variable naming (#2629)
## [1.0.0b251209] - 2025-12-09
### Added
@@ -347,7 +366,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251209...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251211...HEAD
[1.0.0b251211]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251209...python-1.0.0b251211
[1.0.0b251209]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251204...python-1.0.0b251209
[1.0.0b251204]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251120...python-1.0.0b251204
[1.0.0b251120]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251117...python-1.0.0b251120

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