mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16230d3b20 | ||
|
|
8d53b20026 | ||
|
|
c376868ec9 | ||
|
|
8bb9927f3c | ||
|
|
194486c4cc | ||
|
|
0413f4220a | ||
|
|
67e83042cf | ||
|
|
5da1c2fd4c | ||
|
|
989b6ebe71 | ||
|
|
3481914981 | ||
|
|
4c6a5d4aa1 | ||
|
|
191779ce80 | ||
|
|
638fbb5f03 | ||
|
|
523305ac62 | ||
|
|
3f4eeb00be | ||
|
|
b378ca75d1 | ||
|
|
0d9ae1920d | ||
|
|
90964acd2d | ||
|
|
1949193a2e | ||
|
|
bc8dbe4b05 | ||
|
|
b01fd23cd2 | ||
|
|
2f0b2db12a | ||
|
|
7be860e4c4 | ||
|
|
1dce2581b8 | ||
|
|
9313168eeb | ||
|
|
b391088a68 |
@@ -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 |
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ model:
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
|
||||
@@ -11,7 +11,7 @@ model:
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
|
||||
@@ -11,7 +11,7 @@ model:
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+32
-32
@@ -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
|
||||
```
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
|
||||
```
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+15
@@ -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>
|
||||
+98
@@ -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();
|
||||
+90
@@ -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.|
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+5
-1
@@ -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>
|
||||
|
||||
|
||||
+2
-42
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+4
-4
@@ -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>
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
|
||||
+2
-3
@@ -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();
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+4
-16
@@ -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()
|
||||
{
|
||||
|
||||
+2
-2
@@ -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";
|
||||
|
||||
|
||||
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
-7
@@ -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" />
|
||||
|
||||
+2
-2
@@ -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.
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
@@ -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
Reference in New Issue
Block a user