mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6adcac2e97 | ||
|
|
8fca71e5ad | ||
|
|
2bde58f915 | ||
|
|
03a403d2fa | ||
|
|
e319707058 | ||
|
|
54f482df73 | ||
|
|
754dfb2c9d | ||
|
|
b15466f058 | ||
|
|
3a7047f6e4 | ||
|
|
2f06fe557a | ||
|
|
1dbf3fd5cf | ||
|
|
0132cf65e4 | ||
|
|
a53a3c7af8 | ||
|
|
3c322c91e7 | ||
|
|
958a488f96 | ||
|
|
11d6dcfe80 | ||
|
|
3139347526 | ||
|
|
3c379718e9 | ||
|
|
a7298757f5 | ||
|
|
0dcebc6eae | ||
|
|
e0ff153ee9 | ||
|
|
e008144187 | ||
|
|
0fc7933a92 | ||
|
|
d7434d59ce | ||
|
|
eb1117fff4 | ||
|
|
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
|
||||
@@ -803,7 +839,7 @@ var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 8000,
|
||||
// Breaking glass to access provider-specific options
|
||||
RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions()
|
||||
RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
|
||||
{
|
||||
ReasoningOptions = new()
|
||||
{
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -35,19 +35,25 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
|
||||
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
dotnet:
|
||||
- 'dotnet/**'
|
||||
cosmosdb:
|
||||
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
|
||||
# run only if 'dotnet' files were changed
|
||||
- name: dotnet tests
|
||||
if: steps.filter.outputs.dotnet == 'true'
|
||||
run: echo "Dotnet file"
|
||||
- name: dotnet CosmosDB tests
|
||||
if: steps.filter.outputs.cosmosdb == 'true'
|
||||
run: echo "Dotnet CosmosDB changes"
|
||||
# run only if not 'dotnet' files were changed
|
||||
- name: not dotnet tests
|
||||
if: steps.filter.outputs.dotnet != 'true'
|
||||
@@ -68,7 +74,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -77,6 +83,16 @@ jobs:
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
|
||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.0.1
|
||||
@@ -123,17 +139,7 @@ jobs:
|
||||
popd
|
||||
popd
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# Start Cosmos DB Emulator for Cosmos-based unit tests (only on Windows)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
|
||||
- name: Run Unit Tests
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -225,7 +231,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage report artifact
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
|
||||
path: ./TestResults/Reports # Directory containing files to upload
|
||||
|
||||
@@ -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
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
|
||||
@@ -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,9 +19,9 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download coverage report
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
@@ -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
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Run all tests with coverage report
|
||||
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: |
|
||||
python/python-coverage.xml
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,19 +10,19 @@ model:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_APIKEY
|
||||
kind: key
|
||||
apiKey: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# Time-To-Live (TTL) for durable agent sessions
|
||||
|
||||
## Overview
|
||||
|
||||
The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity.
|
||||
|
||||
## What is TTL?
|
||||
|
||||
Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime.
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions
|
||||
- **Cost optimization**: Reduces storage costs by automatically removing unused session state
|
||||
- **Resource management**: Prevents unbounded growth of agent session state in storage
|
||||
- **Configurable**: Set TTL globally or per-agent type to match your application's needs
|
||||
|
||||
## Configuration
|
||||
|
||||
TTL can be configured at two levels:
|
||||
|
||||
1. **Global default TTL**: Applies to all agent sessions unless overridden
|
||||
2. **Per-agent type TTL**: Overrides the global default for specific agent types
|
||||
|
||||
Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes.
|
||||
|
||||
> [!NOTE]
|
||||
> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution.
|
||||
|
||||
### Default values
|
||||
|
||||
- **Default TTL**: 14 days
|
||||
- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases)
|
||||
|
||||
### Configuration examples
|
||||
|
||||
#### .NET
|
||||
|
||||
```csharp
|
||||
// Configure global default TTL and minimum signal delay
|
||||
services.ConfigureDurableAgents(
|
||||
options =>
|
||||
{
|
||||
// Set global default TTL to 7 days
|
||||
options.DefaultTimeToLive = TimeSpan.FromDays(7);
|
||||
|
||||
// Add agents (will use global default TTL)
|
||||
options.AddAIAgent(myAgent);
|
||||
});
|
||||
|
||||
// Configure per-agent TTL
|
||||
services.ConfigureDurableAgents(
|
||||
options =>
|
||||
{
|
||||
options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default
|
||||
|
||||
// Agent with custom TTL of 1 day
|
||||
options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));
|
||||
|
||||
// Agent with custom TTL of 90 days
|
||||
options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));
|
||||
|
||||
// Agent using global default (14 days)
|
||||
options.AddAIAgent(defaultAgent);
|
||||
});
|
||||
|
||||
// Disable TTL for specific agents by setting TTL to null
|
||||
services.ConfigureDurableAgents(
|
||||
options =>
|
||||
{
|
||||
options.DefaultTimeToLive = TimeSpan.FromDays(14);
|
||||
|
||||
// Agent with no TTL (never expires)
|
||||
options.AddAIAgent(permanentAgent, timeToLive: null);
|
||||
});
|
||||
```
|
||||
|
||||
## How TTL works
|
||||
|
||||
The following sections describe how TTL works in detail.
|
||||
|
||||
### Expiration tracking
|
||||
|
||||
Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message:
|
||||
|
||||
1. When a message is sent to an agent session, the expiration time is set to `current time + TTL`
|
||||
2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints)
|
||||
3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time.
|
||||
|
||||
### State deletion
|
||||
|
||||
When an agent session expires, its entire state is deleted, including:
|
||||
|
||||
- Conversation history
|
||||
- Any custom state data
|
||||
- Expiration timestamps
|
||||
|
||||
After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history.
|
||||
|
||||
## Behavior examples
|
||||
|
||||
The following examples illustrate how TTL works in different scenarios.
|
||||
|
||||
### Example 1: Agent session expires after TTL
|
||||
|
||||
1. Agent configured with 30-day TTL
|
||||
2. User sends message at Day 0 → agent session created, expiration set to Day 30
|
||||
3. No further messages sent
|
||||
4. At Day 30 → Agent session is deleted
|
||||
5. User sends message at Day 31 → New agent session created with fresh conversation history
|
||||
|
||||
### Example 2: TTL reset on interaction
|
||||
|
||||
1. Agent configured with 30-day TTL
|
||||
2. User sends message at Day 0 → agent session created, expiration set to Day 30
|
||||
3. User sends message at Day 15 → Expiration reset to Day 45
|
||||
4. User sends message at Day 40 → Expiration reset to Day 70
|
||||
5. Agent session remains active as long as there are regular interactions
|
||||
|
||||
## Logging
|
||||
|
||||
The TTL feature includes comprehensive logging to track state changes:
|
||||
|
||||
- **Expiration time updated**: Logged when TTL expiration time is set or updated
|
||||
- **Deletion scheduled**: Logged when a deletion check signal is scheduled
|
||||
- **Deletion check**: Logged when a deletion check operation runs
|
||||
- **Session expired**: Logged when an agent session is deleted due to expiration
|
||||
- **TTL rescheduled**: Logged when a deletion signal is rescheduled
|
||||
|
||||
These logs help monitor TTL behavior and troubleshoot any issues.
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state.
|
||||
|
||||
2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes.
|
||||
|
||||
3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed.
|
||||
|
||||
4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods.
|
||||
|
||||
## Limitations
|
||||
|
||||
- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp.
|
||||
- Deletion checks are durably scheduled operations and may have slight delays depending on system load.
|
||||
- Once an agent session is deleted, its conversation history cannot be recovered.
|
||||
- TTL deletion requires at least one worker to be available to process the deletion operation message.
|
||||
@@ -209,6 +209,7 @@ dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on ob
|
||||
dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates
|
||||
dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter
|
||||
dotnet_diagnostic.CA2249.severity = suggestion # Consider using 'Contains' method instead of 'IndexOf' method
|
||||
dotnet_diagnostic.CA2252.severity = none # Requires preview
|
||||
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
|
||||
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
|
||||
dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload
|
||||
|
||||
@@ -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.* -->
|
||||
@@ -19,11 +19,11 @@
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.3" />
|
||||
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.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.AI.OpenAI" Version="2.8.0-beta.1" />
|
||||
<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,9 @@
|
||||
<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.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" Version="10.1.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
|
||||
<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 +71,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,10 +100,10 @@
|
||||
<!-- 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="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" />
|
||||
<PackageVersion Include="OpenAI" Version="2.8.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
|
||||
<!-- Workflows -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenAPI" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AgentWebChat.AgentHost.Utilities;
|
||||
using Azure;
|
||||
using Azure.AI.Inference;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OllamaSharp;
|
||||
|
||||
@@ -24,7 +22,6 @@ public static class ChatClientExtensions
|
||||
ClientChatProvider.Ollama => builder.AddOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddAzureOpenAIClient(connectionName).AddChatClient(connectionInfo.SelectedModel),
|
||||
ClientChatProvider.AzureAIInference => builder.AddAzureInferenceClient(connectionName, connectionInfo),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
@@ -44,16 +41,6 @@ public static class ChatClientExtensions
|
||||
})
|
||||
.AddChatClient(connectionInfo.SelectedModel);
|
||||
|
||||
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
|
||||
builder.Services.AddChatClient(sp =>
|
||||
{
|
||||
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
|
||||
|
||||
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
|
||||
|
||||
return client.AsIChatClient(connectionInfo.SelectedModel);
|
||||
});
|
||||
|
||||
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
@@ -83,7 +70,6 @@ public static class ChatClientExtensions
|
||||
ClientChatProvider.Ollama => builder.AddKeyedOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddKeyedOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddKeyedAzureOpenAIClient(connectionName).AddKeyedChatClient(connectionName, connectionInfo.SelectedModel),
|
||||
ClientChatProvider.AzureAIInference => builder.AddKeyedAzureInferenceClient(connectionName, connectionInfo),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
@@ -103,16 +89,6 @@ public static class ChatClientExtensions
|
||||
})
|
||||
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
|
||||
|
||||
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
|
||||
builder.Services.AddKeyedChatClient(connectionName, sp =>
|
||||
{
|
||||
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
|
||||
|
||||
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
|
||||
|
||||
return client.AsIChatClient(connectionInfo.SelectedModel);
|
||||
});
|
||||
|
||||
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
|
||||
@@ -27,7 +27,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
|
||||
Transport = new HttpClientPipelineTransport(httpClient)
|
||||
};
|
||||
|
||||
var openAiClient = new OpenAIResponseClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var chatOptions = new ChatOptions()
|
||||
{
|
||||
ConversationId = threadId
|
||||
|
||||
@@ -32,6 +32,6 @@ AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstruct
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.Build();
|
||||
app.Run();
|
||||
|
||||
+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"
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
|
||||
+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,12 +6,12 @@ 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(
|
||||
apiKey)
|
||||
.GetOpenAIResponseClient(model)
|
||||
.GetResponsesClient(model)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
+3
-3
@@ -7,15 +7,15 @@ 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)
|
||||
.GetOpenAIResponseClient(model)
|
||||
.GetResponsesClient(model)
|
||||
.AsIChatClient().AsBuilder()
|
||||
.ConfigureOptions(o =>
|
||||
{
|
||||
o.RawRepresentationFactory = _ => new ResponseCreationOptions()
|
||||
o.RawRepresentationFactory = _ => new CreateResponseOptions()
|
||||
{
|
||||
ReasoningOptions = new()
|
||||
{
|
||||
|
||||
+7
-7
@@ -16,13 +16,13 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <summary>
|
||||
/// Initialize an instance of <see cref="OpenAIResponseClientAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="client">Instance of <see cref="OpenAIResponseClient"/></param>
|
||||
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
|
||||
/// <param name="instructions">Optional instructions for the agent.</param>
|
||||
/// <param name="name">Optional name for the agent.</param>
|
||||
/// <param name="description">Optional description for the agent.</param>
|
||||
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
|
||||
public OpenAIResponseClientAgent(
|
||||
OpenAIResponseClient client,
|
||||
ResponsesClient client,
|
||||
string? instructions = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
@@ -39,11 +39,11 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <summary>
|
||||
/// Initialize an instance of <see cref="OpenAIResponseClientAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="client">Instance of <see cref="OpenAIResponseClient"/></param>
|
||||
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
|
||||
/// <param name="options">Options to create the agent.</param>
|
||||
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
|
||||
public OpenAIResponseClientAgent(
|
||||
OpenAIResponseClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
|
||||
ResponsesClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
|
||||
base(new ChatClientAgent((client ?? throw new ArgumentNullException(nameof(client))).AsIChatClient(), options, loggerFactory))
|
||||
{
|
||||
}
|
||||
@@ -55,8 +55,8 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="OpenAIResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public virtual async Task<OpenAIResponse> RunAsync(
|
||||
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public virtual async Task<ResponseResult> RunAsync(
|
||||
IEnumerable<ResponseItem> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -74,7 +74,7 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="OpenAIResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public virtual async IAsyncEnumerable<StreamingResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ResponseItem> messages,
|
||||
AgentThread? thread = null,
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an OpenAIResponseClient instance.
|
||||
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an ResponsesClient instance.
|
||||
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
@@ -9,16 +9,16 @@ using OpenAIResponseClientSample;
|
||||
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";
|
||||
|
||||
// Create an OpenAIResponseClient directly from OpenAIClient
|
||||
OpenAIResponseClient responseClient = new OpenAIClient(apiKey).GetOpenAIResponseClient(model);
|
||||
// Create a ResponsesClient directly from OpenAIClient
|
||||
ResponsesClient responseClient = new OpenAIClient(apiKey).GetResponsesClient(model);
|
||||
|
||||
// Create an agent directly from the OpenAIResponseClient using OpenAIResponseClientAgent
|
||||
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
|
||||
OpenAIResponseClientAgent agent = new(responseClient, instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
ResponseItem userMessage = ResponseItem.CreateUserMessageItem("Tell me a joke about a pirate.");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
OpenAIResponse response = await agent.RunAsync([userMessage]);
|
||||
ResponseResult response = await agent.RunAsync([userMessage]);
|
||||
Console.WriteLine(response.GetOutputText());
|
||||
|
||||
// Invoke the agent with streaming support.
|
||||
|
||||
+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 ResponsesClient using OpenAIResponseClientAgent
|
||||
ChatClientAgent agent = new(openAIClient.GetResponsesClient(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
@@ -22,7 +22,7 @@ var stateStore = new Dictionary<string, JsonElement?>();
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
name: "SpaceNovelWriter",
|
||||
instructions: "You are a space novel writer. Always research relevant facts and generate character profiles for the main characters before writing novels." +
|
||||
|
||||
@@ -13,7 +13,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent();
|
||||
|
||||
// Enable background responses (only supported by OpenAI Responses at this time).
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ internal sealed class Program
|
||||
Dictionary<string, byte[]> screenshots = ComputerUseUtil.LoadScreenshotAssets();
|
||||
|
||||
ChatOptions chatOptions = new();
|
||||
ResponseCreationOptions responseCreationOptions = new()
|
||||
CreateResponseOptions responseCreationOptions = new()
|
||||
{
|
||||
TruncationMode = ResponseTruncationMode.Auto
|
||||
};
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ var mcpTool = new HostedMcpServerTool(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgent",
|
||||
@@ -57,7 +57,7 @@ var mcpToolWithApproval = new HostedMcpServerTool(
|
||||
AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgentWithApproval",
|
||||
|
||||
+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 -->
|
||||
|
||||
@@ -27,7 +27,7 @@ TokenCredential browserCredential = new InteractiveBrowserCredential(
|
||||
using IChatClient client = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.WithPurview(browserCredential, new PurviewSettings("Agent Framework Test App"))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,11 +23,6 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
private readonly AgentRecord? _agentRecord;
|
||||
private readonly ChatOptions? _chatOptions;
|
||||
private readonly AgentReference _agentReference;
|
||||
/// <summary>
|
||||
/// The usage of a no-op model is a necessary change to avoid OpenAIClients to throw exceptions when
|
||||
/// used with Azure AI Agents as the model used is now defined at the agent creation time.
|
||||
/// </summary>
|
||||
private const string NoOpModel = "no-op";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AzureAIProjectChatClient"/> class.
|
||||
@@ -42,7 +37,7 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentReference agentReference, string? defaultModelId, ChatOptions? chatOptions)
|
||||
: base(Throw.IfNull(aiProjectClient)
|
||||
.GetProjectOpenAIClient()
|
||||
.GetOpenAIResponseClient(defaultModelId ?? NoOpModel)
|
||||
.GetProjectResponsesClientForAgent(agentReference)
|
||||
.AsIChatClient())
|
||||
{
|
||||
this._agentClient = aiProjectClient;
|
||||
@@ -132,13 +127,15 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
|
||||
agentEnabledChatOptions.RawRepresentationFactory = (client) =>
|
||||
{
|
||||
if (originalFactory?.Invoke(this) is not ResponseCreationOptions responseCreationOptions)
|
||||
if (originalFactory?.Invoke(this) is not CreateResponseOptions responseCreationOptions)
|
||||
{
|
||||
responseCreationOptions = new ResponseCreationOptions();
|
||||
responseCreationOptions = new CreateResponseOptions();
|
||||
}
|
||||
|
||||
ResponseCreationOptionsExtensions.set_Agent(responseCreationOptions, this._agentReference);
|
||||
ResponseCreationOptionsExtensions.set_Model(responseCreationOptions, null);
|
||||
responseCreationOptions.Agent = this._agentReference;
|
||||
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
responseCreationOptions.Patch.Remove("$.model"u8);
|
||||
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
return responseCreationOptions;
|
||||
};
|
||||
|
||||
@@ -400,7 +400,7 @@ public static partial class AzureAIProjectChatClientExtensions
|
||||
};
|
||||
|
||||
// Attempt to capture breaking glass options from the raw representation factory that match the agent definition.
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is ResponseCreationOptions respCreationOptions)
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions)
|
||||
{
|
||||
agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions;
|
||||
}
|
||||
@@ -466,7 +466,7 @@ public static partial class AzureAIProjectChatClientExtensions
|
||||
};
|
||||
|
||||
// Attempt to capture breaking glass options from the raw representation factory that match the agent definition.
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is ResponseCreationOptions respCreationOptions)
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions)
|
||||
{
|
||||
agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -217,9 +217,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a checkpoint document stored in Cosmos DB.
|
||||
/// </summary>
|
||||
/// <summary>Represents a checkpoint document stored in Cosmos DB.</summary>
|
||||
internal sealed class CosmosCheckpointDocument
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,29 +16,34 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
|
||||
private readonly DurableTaskClient _client = services.GetRequiredService<DurableTaskClient>();
|
||||
private readonly ILoggerFactory _loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
private readonly IAgentResponseHandler? _messageHandler = services.GetService<IAgentResponseHandler>();
|
||||
private readonly DurableAgentsOptions _options = services.GetRequiredService<DurableAgentsOptions>();
|
||||
private readonly CancellationToken _cancellationToken = cancellationToken != default
|
||||
? cancellationToken
|
||||
: services.GetService<IHostApplicationLifetime>()?.ApplicationStopping ?? CancellationToken.None;
|
||||
|
||||
public async Task<AgentRunResponse> RunAgentAsync(RunRequest request)
|
||||
public Task<AgentRunResponse> RunAgentAsync(RunRequest request)
|
||||
{
|
||||
return this.Run(request);
|
||||
}
|
||||
|
||||
// IDE1006 and VSTHRD200 disabled to allow method name to match the common cross-platform entity operation name.
|
||||
#pragma warning disable IDE1006
|
||||
#pragma warning disable VSTHRD200
|
||||
public async Task<AgentRunResponse> Run(RunRequest request)
|
||||
#pragma warning restore VSTHRD200
|
||||
#pragma warning restore IDE1006
|
||||
{
|
||||
AgentSessionId sessionId = this.Context.Id;
|
||||
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents =
|
||||
this._services.GetRequiredService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>();
|
||||
if (!agents.TryGetValue(sessionId.Name, out Func<IServiceProvider, AIAgent>? agentFactory))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
|
||||
}
|
||||
|
||||
AIAgent agent = agentFactory(this._services);
|
||||
AIAgent agent = this.GetAgent(sessionId);
|
||||
EntityAgentWrapper agentWrapper = new(agent, this.Context, request, this._services);
|
||||
|
||||
// Logger category is Microsoft.DurableTask.Agents.{agentName}.{sessionId}
|
||||
ILogger logger = this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agent.Name}.{sessionId.Key}");
|
||||
ILogger logger = this.GetLogger(agent.Name!, sessionId.Key);
|
||||
|
||||
if (request.Messages.Count == 0)
|
||||
{
|
||||
logger.LogInformation("Ignoring empty request");
|
||||
return new AgentRunResponse();
|
||||
}
|
||||
|
||||
this.State.Data.ConversationHistory.Add(DurableAgentStateRequest.FromRunRequest(request));
|
||||
@@ -113,6 +118,36 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
|
||||
response.Usage?.TotalTokenCount);
|
||||
}
|
||||
|
||||
// Update TTL expiration time. Only schedule deletion check on first interaction.
|
||||
// Subsequent interactions just update the expiration time; CheckAndDeleteIfExpiredAsync
|
||||
// will reschedule the deletion check when it runs.
|
||||
TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
DateTime newExpirationTime = DateTime.UtcNow.Add(timeToLive.Value);
|
||||
bool isFirstInteraction = this.State.Data.ExpirationTimeUtc is null;
|
||||
|
||||
this.State.Data.ExpirationTimeUtc = newExpirationTime;
|
||||
logger.LogTTLExpirationTimeUpdated(sessionId, newExpirationTime);
|
||||
|
||||
// Only schedule deletion check on the first interaction when entity is created.
|
||||
// On subsequent interactions, we just update the expiration time. The scheduled
|
||||
// CheckAndDeleteIfExpiredAsync will reschedule itself if the entity hasn't expired.
|
||||
if (isFirstInteraction)
|
||||
{
|
||||
this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TTL is disabled. Clear the expiration time if it was previously set.
|
||||
if (this.State.Data.ExpirationTimeUtc.HasValue)
|
||||
{
|
||||
logger.LogTTLExpirationTimeCleared(sessionId);
|
||||
this.State.Data.ExpirationTimeUtc = null;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
finally
|
||||
@@ -121,4 +156,78 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
|
||||
DurableAgentContext.ClearCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the entity has expired and deletes it if so, otherwise reschedules the deletion check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is called by the durable task runtime when a <c>CheckAndDeleteIfExpired</c> signal is received.
|
||||
/// </remarks>
|
||||
public void CheckAndDeleteIfExpired()
|
||||
{
|
||||
AgentSessionId sessionId = this.Context.Id;
|
||||
AIAgent agent = this.GetAgent(sessionId);
|
||||
ILogger logger = this.GetLogger(agent.Name!, sessionId.Key);
|
||||
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
DateTime? expirationTime = this.State.Data.ExpirationTimeUtc;
|
||||
|
||||
logger.LogTTLDeletionCheck(sessionId, expirationTime, currentTime);
|
||||
|
||||
if (expirationTime.HasValue)
|
||||
{
|
||||
if (currentTime >= expirationTime.Value)
|
||||
{
|
||||
// Entity has expired, delete it
|
||||
logger.LogTTLEntityExpired(sessionId, expirationTime.Value);
|
||||
this.State = null!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Entity hasn't expired yet, reschedule the deletion check
|
||||
TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleDeletionCheck(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive)
|
||||
{
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
DateTime expirationTime = this.State.Data.ExpirationTimeUtc ?? currentTime.Add(timeToLive);
|
||||
TimeSpan minimumDelay = this._options.MinimumTimeToLiveSignalDelay;
|
||||
|
||||
// To avoid excessive scheduling, we schedule the deletion check for no less than the minimum delay.
|
||||
DateTime scheduledTime = expirationTime > currentTime.Add(minimumDelay)
|
||||
? expirationTime
|
||||
: currentTime.Add(minimumDelay);
|
||||
|
||||
logger.LogTTLDeletionScheduled(sessionId, scheduledTime);
|
||||
|
||||
// Schedule a signal to self to check for expiration
|
||||
this.Context.SignalEntity(
|
||||
this.Context.Id,
|
||||
nameof(CheckAndDeleteIfExpired), // self-signal
|
||||
options: new SignalEntityOptions { SignalTime = scheduledTime });
|
||||
}
|
||||
|
||||
private AIAgent GetAgent(AgentSessionId sessionId)
|
||||
{
|
||||
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents =
|
||||
this._services.GetRequiredService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>();
|
||||
if (!agents.TryGetValue(sessionId.Name, out Func<IServiceProvider, AIAgent>? agentFactory))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
|
||||
}
|
||||
|
||||
return agentFactory(this._services);
|
||||
}
|
||||
|
||||
private ILogger GetLogger(string agentName, string sessionKey)
|
||||
{
|
||||
return this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agentName}.{sessionKey}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Release History
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
|
||||
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
|
||||
|
||||
## v1.0.0-preview.251204.1
|
||||
|
||||
- Added orchestration ID to durable agent entity state ([#2137](https://github.com/microsoft/agent-framework/pull/2137))
|
||||
|
||||
@@ -22,7 +22,7 @@ internal class DefaultDurableAgentClient(DurableTaskClient client, ILoggerFactor
|
||||
|
||||
await this._client.Entities.SignalEntityAsync(
|
||||
sessionId,
|
||||
nameof(AgentEntity.RunAgentAsync),
|
||||
nameof(AgentEntity.Run),
|
||||
request,
|
||||
cancellation: cancellationToken);
|
||||
|
||||
|
||||
@@ -98,14 +98,16 @@ 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
|
||||
{
|
||||
return await this._context.Entities.CallEntityAsync<AgentRunResponse>(
|
||||
durableThread.SessionId,
|
||||
nameof(AgentEntity.RunAgentAsync),
|
||||
nameof(AgentEntity.Run),
|
||||
request);
|
||||
}
|
||||
catch (EntityOperationFailedException e) when (e.FailureDetails.ErrorType == "EntityTaskNotFound")
|
||||
|
||||
@@ -9,23 +9,67 @@ public sealed class DurableAgentsOptions
|
||||
{
|
||||
// Agent names are case-insensitive
|
||||
private readonly Dictionary<string, Func<IServiceProvider, AIAgent>> _agentFactories = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, TimeSpan?> _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal DurableAgentsOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default time-to-live (TTL) for agent entities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If an agent entity is idle for this duration, it will be automatically deleted.
|
||||
/// Defaults to 14 days. Set to <see langword="null"/> to disable TTL for agents without explicit TTL configuration.
|
||||
/// </remarks>
|
||||
public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum delay for scheduling TTL deletion signals. Defaults to 5 minutes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is primarily useful for testing (where shorter delays are needed) or for
|
||||
/// shorter-lived agents in workflows that need more rapid cleanup. The maximum allowed value is 5 minutes.
|
||||
/// Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions.
|
||||
/// However, this can also increase the load on the system and should be used with caution.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value exceeds 5 minutes.</exception>
|
||||
public TimeSpan MinimumTimeToLiveSignalDelay
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
const int MaximumDelayMinutes = 5;
|
||||
if (value > TimeSpan.FromMinutes(MaximumDelayMinutes))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
value,
|
||||
$"The minimum time-to-live signal delay cannot exceed {MaximumDelayMinutes} minutes.");
|
||||
}
|
||||
|
||||
field = value;
|
||||
}
|
||||
} = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an AI agent factory to the options.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="factory">The factory function to create the agent.</param>
|
||||
/// <param name="timeToLive">Optional time-to-live for this agent's entities. If not specified, uses <see cref="DefaultTimeToLive"/>.</param>
|
||||
/// <returns>The options instance.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="factory"/> is null.</exception>
|
||||
public DurableAgentsOptions AddAIAgentFactory(string name, Func<IServiceProvider, AIAgent> factory)
|
||||
public DurableAgentsOptions AddAIAgentFactory(string name, Func<IServiceProvider, AIAgent> factory, TimeSpan? timeToLive = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
this._agentFactories.Add(name, factory);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
this._agentTimeToLive[name] = timeToLive;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -50,12 +94,13 @@ public sealed class DurableAgentsOptions
|
||||
/// Adds an AI agent to the options.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent to add.</param>
|
||||
/// <param name="timeToLive">Optional time-to-live for this agent's entities. If not specified, uses <see cref="DefaultTimeToLive"/>.</param>
|
||||
/// <returns>The options instance.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="agent.Name"/> is null or whitespace or when an agent with the same name has already been registered.
|
||||
/// </exception>
|
||||
public DurableAgentsOptions AddAIAgent(AIAgent agent)
|
||||
public DurableAgentsOptions AddAIAgent(AIAgent agent, TimeSpan? timeToLive = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
@@ -70,6 +115,11 @@ public sealed class DurableAgentsOptions
|
||||
}
|
||||
|
||||
this._agentFactories.Add(agent.Name, sp => agent);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
this._agentTimeToLive[agent.Name] = timeToLive;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -81,4 +131,14 @@ public sealed class DurableAgentsOptions
|
||||
{
|
||||
return this._agentFactories.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time-to-live for a specific agent, or the default TTL if not specified.
|
||||
/// </summary>
|
||||
/// <param name="agentName">The name of the agent.</param>
|
||||
/// <returns>The time-to-live for the agent, or the default TTL if not specified.</returns>
|
||||
internal TimeSpan? GetTimeToLive(string agentName)
|
||||
{
|
||||
return this._agentTimeToLive.TryGetValue(agentName, out TimeSpan? ttl) ? ttl : this.DefaultTimeToLive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed class EntityAgentWrapper(
|
||||
private readonly IServiceProvider? _entityScopedServices = entityScopedServices;
|
||||
|
||||
// The ID of the agent is always the entity ID.
|
||||
public override string Id => this._entityContext.Id.ToString();
|
||||
protected override string? IdCore => this._entityContext.Id.ToString();
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
|
||||
@@ -46,4 +46,58 @@ internal static partial class Logs
|
||||
Level = LogLevel.Information,
|
||||
Message = "Found response for agent with session ID '{SessionId}' with correlation ID '{CorrelationId}'")]
|
||||
public static partial void LogDonePollingForResponse(this ILogger logger, AgentSessionId sessionId, string correlationId);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 6,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL expiration time updated to {ExpirationTime:O}")]
|
||||
public static partial void LogTTLExpirationTimeUpdated(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime expirationTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL deletion signal scheduled for {ScheduledTime:O}")]
|
||||
public static partial void LogTTLDeletionScheduled(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime scheduledTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 8,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL deletion check running. Expiration time: {ExpirationTime:O}, Current time: {CurrentTime:O}")]
|
||||
public static partial void LogTTLDeletionCheck(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime? expirationTime,
|
||||
DateTime currentTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 9,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] Entity expired and deleted due to TTL. Expiration time: {ExpirationTime:O}")]
|
||||
public static partial void LogTTLEntityExpired(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime expirationTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 10,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL deletion signal rescheduled for {ScheduledTime:O}")]
|
||||
public static partial void LogTTLRescheduled(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime scheduledTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 11,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL expiration time cleared (TTL disabled)")]
|
||||
public static partial void LogTTLExpirationTimeCleared(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ public static class ServiceCollectionExtensions
|
||||
// The agent dictionary contains the real agent factories, which is used by the agent entities.
|
||||
services.AddSingleton(agents);
|
||||
|
||||
// Register the options so AgentEntity can access TTL configuration
|
||||
services.AddSingleton(options);
|
||||
|
||||
// The keyed services are used to resolve durable agent *proxy* instances for external clients.
|
||||
foreach (var factory in agents)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,13 @@ internal sealed class DurableAgentStateData
|
||||
[JsonPropertyName("conversationHistory")]
|
||||
public IList<DurableAgentStateEntry> ConversationHistory { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expiration time (UTC) for this agent entity.
|
||||
/// If the entity is idle beyond this time, it will be automatically deleted.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expirationTimeUtc")]
|
||||
public DateTime? ExpirationTimeUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any additional data found during deserialization that does not map to known properties.
|
||||
/// </summary>
|
||||
|
||||
+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",
|
||||
|
||||
+11
-15
@@ -84,22 +84,18 @@ internal sealed class ConversationReferenceJsonConverter : JsonConverter<Convers
|
||||
return;
|
||||
}
|
||||
|
||||
// If only ID is present and no metadata, serialize as a simple string
|
||||
if (value.Metadata is null || value.Metadata.Count == 0)
|
||||
// Ideally if only ID is present and no metadata, we would serialize as a simple string.
|
||||
// However, while a request's "conversation" property can be either a string or an object
|
||||
// containing a string, a response's "conversation" property is always an object. Since
|
||||
// here we don't know which scenario we're in, we always serialize as an object, which works
|
||||
// in any scenario.
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("id", value.Id);
|
||||
if (value.Metadata is not null)
|
||||
{
|
||||
writer.WriteStringValue(value.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, serialize as an object
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("id", value.Id);
|
||||
if (value.Metadata is not null)
|
||||
{
|
||||
writer.WritePropertyName("metadata");
|
||||
JsonSerializer.Serialize(writer, value.Metadata, OpenAIHostingJsonContext.Default.DictionaryStringString);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
writer.WritePropertyName("metadata");
|
||||
JsonSerializer.Serialize(writer, value.Metadata, OpenAIHostingJsonContext.Default.DictionaryStringString);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,22 +73,22 @@ public static class AIAgentWithOpenAIExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the AI agent with a collection of OpenAI response items and returns the response as a native OpenAI <see cref="OpenAIResponse"/>.
|
||||
/// Runs the AI agent with a collection of OpenAI response items and returns the response as a native OpenAI <see cref="ResponseResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="agent">The AI agent to run.</param>
|
||||
/// <param name="messages">The collection of OpenAI response items to send to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="Task{OpenAIResponse}"/> representing the asynchronous operation that returns a native OpenAI <see cref="OpenAIResponse"/> response.</returns>
|
||||
/// <returns>A <see cref="Task{ResponseResult}"/> representing the asynchronous operation that returns a native OpenAI <see cref="ResponseResult"/> response.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> or <paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to an <see cref="OpenAIResponse"/>, typically when the underlying representation is not an OpenAI response.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to an <see cref="ResponseResult"/>, typically when the underlying representation is not an OpenAI response.</exception>
|
||||
/// <exception cref="NotSupportedException">Thrown when any message in <paramref name="messages"/> has a type that is not supported by the message conversion method.</exception>
|
||||
/// <remarks>
|
||||
/// This method converts the OpenAI response items to the Microsoft Extensions AI format using the appropriate conversion method,
|
||||
/// runs the agent with the converted message collection, and then extracts the native OpenAI <see cref="OpenAIResponse"/> from the response using <see cref="AgentRunResponseExtensions.AsOpenAIResponse"/>.
|
||||
/// runs the agent with the converted message collection, and then extracts the native OpenAI <see cref="ResponseResult"/> from the response using <see cref="AgentRunResponseExtensions.AsOpenAIResponse"/>.
|
||||
/// </remarks>
|
||||
public static async Task<OpenAIResponse> RunAsync(this AIAgent agent, IEnumerable<ResponseItem> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public static async Task<ResponseResult> RunAsync(this AIAgent agent, IEnumerable<ResponseItem> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(messages);
|
||||
|
||||
@@ -29,17 +29,17 @@ public static class AgentRunResponseExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or extracts a native OpenAI <see cref="OpenAIResponse"/> object from an <see cref="AgentRunResponse"/>.
|
||||
/// Creates or extracts a native OpenAI <see cref="ResponseResult"/> object from an <see cref="AgentRunResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The agent response.</param>
|
||||
/// <returns>The OpenAI <see cref="OpenAIResponse"/> object.</returns>
|
||||
/// <returns>The OpenAI <see cref="ResponseResult"/> object.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
|
||||
public static OpenAIResponse AsOpenAIResponse(this AgentRunResponse response)
|
||||
public static ResponseResult AsOpenAIResponse(this AgentRunResponse response)
|
||||
{
|
||||
Throw.IfNull(response);
|
||||
|
||||
return
|
||||
response.RawRepresentation as OpenAIResponse ??
|
||||
response.AsChatResponse().AsOpenAIResponse();
|
||||
response.RawRepresentation as ResponseResult ??
|
||||
response.AsChatResponse().AsOpenAIResponseResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace OpenAI.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="OpenAIResponseClient"/>
|
||||
/// Provides extension methods for <see cref="ResponsesClient"/>
|
||||
/// to simplify the creation of AI agents that work with OpenAI services.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -20,9 +20,9 @@ namespace OpenAI.Responses;
|
||||
public static class OpenAIResponseClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="OpenAIResponseClient"/> using the OpenAI Response API.
|
||||
/// Creates an AI agent from an <see cref="ResponsesClient"/> using the OpenAI Response API.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="OpenAIResponseClient" /> to use for the agent.</param>
|
||||
/// <param name="client">The <see cref="ResponsesClient" /> to use for the agent.</param>
|
||||
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
|
||||
/// <param name="name">Optional name for the agent for identification purposes.</param>
|
||||
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
|
||||
@@ -33,7 +33,7 @@ public static class OpenAIResponseClientExtensions
|
||||
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the OpenAI Response service.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static ChatClientAgent CreateAIAgent(
|
||||
this OpenAIResponseClient client,
|
||||
this ResponsesClient client,
|
||||
string? instructions = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
@@ -61,9 +61,9 @@ public static class OpenAIResponseClientExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="OpenAIResponseClient"/> using the OpenAI Response API.
|
||||
/// Creates an AI agent from an <see cref="ResponsesClient"/> using the OpenAI Response API.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="OpenAIResponseClient" /> to use for the agent.</param>
|
||||
/// <param name="client">The <see cref="ResponsesClient" /> to use for the agent.</param>
|
||||
/// <param name="options">Full set of options to configure the agent.</param>
|
||||
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
|
||||
@@ -71,7 +71,7 @@ public static class OpenAIResponseClientExtensions
|
||||
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the OpenAI Response service.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
|
||||
public static ChatClientAgent CreateAIAgent(
|
||||
this OpenAIResponseClient client,
|
||||
this ResponsesClient client,
|
||||
ChatClientAgentOptions options,
|
||||
Func<IChatClient, IChatClient>? clientFactory = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
if (inputArguments is not null)
|
||||
{
|
||||
JsonNode jsonNode = ConvertDictionaryToJson(inputArguments);
|
||||
ResponseCreationOptions responseCreationOptions = new();
|
||||
CreateResponseOptions responseCreationOptions = new();
|
||||
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
responseCreationOptions.Patch.Set("$.structured_inputs"u8, BinaryData.FromString(jsonNode.ToJsonString()));
|
||||
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
@@ -206,7 +206,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentResponseItem responseItem = await this.GetConversationClient().GetProjectConversationItemAsync(conversationId, messageId, include: null, cancellationToken).ConfigureAwait(false);
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
ResponseItem[] items = [responseItem.AsResponseResultItem()];
|
||||
return items.AsChatMessages().Single();
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
|
||||
await foreach (AgentResponseItem responseItem in this.GetConversationClient().GetProjectConversationItemsAsync(conversationId, null, limit, order.ToString(), after, before, include: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
ResponseItem[] items = [responseItem.AsResponseResultItem()];
|
||||
foreach (ChatMessage message in items.AsChatMessages())
|
||||
{
|
||||
yield return message;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// </summary>
|
||||
public sealed class DirectEdgeData : EdgeData
|
||||
{
|
||||
internal DirectEdgeData(string sourceId, string sinkId, EdgeId id, PredicateT? condition = null) : base(id)
|
||||
internal DirectEdgeData(string sourceId, string sinkId, EdgeId id, PredicateT? condition = null, string? label = null) : base(id, label)
|
||||
{
|
||||
this.SourceId = sourceId;
|
||||
this.SinkId = sinkId;
|
||||
|
||||
@@ -14,10 +14,16 @@ public abstract class EdgeData
|
||||
/// </summary>
|
||||
internal abstract EdgeConnection Connection { get; }
|
||||
|
||||
internal EdgeData(EdgeId id)
|
||||
internal EdgeData(EdgeId id, string? label = null)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Label = label;
|
||||
}
|
||||
|
||||
internal EdgeId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional label for the edge, allowing for arbitrary metadata to be associated with it.
|
||||
/// </summary>
|
||||
public string? Label { get; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// </summary>
|
||||
internal sealed class FanInEdgeData : EdgeData
|
||||
{
|
||||
internal FanInEdgeData(List<string> sourceIds, string sinkId, EdgeId id) : base(id)
|
||||
internal FanInEdgeData(List<string> sourceIds, string sinkId, EdgeId id, string? label) : base(id, label)
|
||||
{
|
||||
this.SourceIds = sourceIds;
|
||||
this.SinkId = sinkId;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// </summary>
|
||||
internal sealed class FanOutEdgeData : EdgeData
|
||||
{
|
||||
internal FanOutEdgeData(string sourceId, List<string> sinkIds, EdgeId edgeId, AssignerF? assigner = null) : base(edgeId)
|
||||
internal FanOutEdgeData(string sourceId, List<string> sinkIds, EdgeId edgeId, AssignerF? assigner = null, string? label = null) : base(edgeId, label)
|
||||
{
|
||||
this.SourceId = sourceId;
|
||||
this.SinkIds = sinkIds;
|
||||
|
||||
@@ -99,10 +99,30 @@ public static class WorkflowVisualizer
|
||||
}
|
||||
|
||||
// Emit normal edges
|
||||
foreach (var (src, target, isConditional) in ComputeNormalEdges(workflow))
|
||||
foreach (var (src, target, isConditional, label) in ComputeNormalEdges(workflow))
|
||||
{
|
||||
var edgeAttr = isConditional ? " [style=dashed, label=\"conditional\"]" : "";
|
||||
lines.Add($"{indent}\"{MapId(src)}\" -> \"{MapId(target)}\"{edgeAttr};");
|
||||
// Build edge attributes
|
||||
var attributes = new List<string>();
|
||||
|
||||
// Add style for conditional edges
|
||||
if (isConditional)
|
||||
{
|
||||
attributes.Add("style=dashed");
|
||||
}
|
||||
|
||||
// Add label (custom label or default "conditional" for conditional edges)
|
||||
if (label != null)
|
||||
{
|
||||
attributes.Add($"label=\"{EscapeDotLabel(label)}\"");
|
||||
}
|
||||
else if (isConditional)
|
||||
{
|
||||
attributes.Add("label=\"conditional\"");
|
||||
}
|
||||
|
||||
// Combine attributes
|
||||
var attrString = attributes.Count > 0 ? $" [{string.Join(", ", attributes)}]" : "";
|
||||
lines.Add($"{indent}\"{MapId(src)}\" -> \"{MapId(target)}\"{attrString};");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +153,7 @@ public static class WorkflowVisualizer
|
||||
|
||||
private static void EmitWorkflowMermaid(Workflow workflow, List<string> lines, string indent, string? ns = null)
|
||||
{
|
||||
string sanitize(string input)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
string MapId(string id) => ns != null ? $"{sanitize(ns)}/{sanitize(id)}" : id;
|
||||
string MapId(string id) => ns != null ? $"{ns}/{id}" : id;
|
||||
|
||||
// Add start node
|
||||
var startExecutorId = workflow.StartExecutorId;
|
||||
@@ -175,14 +190,23 @@ public static class WorkflowVisualizer
|
||||
}
|
||||
|
||||
// Emit normal edges
|
||||
foreach (var (src, target, isConditional) in ComputeNormalEdges(workflow))
|
||||
foreach (var (src, target, isConditional, label) in ComputeNormalEdges(workflow))
|
||||
{
|
||||
if (isConditional)
|
||||
{
|
||||
lines.Add($"{indent}{MapId(src)} -. conditional .--> {MapId(target)};");
|
||||
string effectiveLabel = label != null ? EscapeMermaidLabel(label) : "conditional";
|
||||
|
||||
// Conditional edge, with user label or default
|
||||
lines.Add($"{indent}{MapId(src)} -. {effectiveLabel} .--> {MapId(target)};");
|
||||
}
|
||||
else if (label != null)
|
||||
{
|
||||
// Regular edge with label
|
||||
lines.Add($"{indent}{MapId(src)} -->|{EscapeMermaidLabel(label)}| {MapId(target)};");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular edge without label
|
||||
lines.Add($"{indent}{MapId(src)} --> {MapId(target)};");
|
||||
}
|
||||
}
|
||||
@@ -214,9 +238,9 @@ public static class WorkflowVisualizer
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<(string Source, string Target, bool IsConditional)> ComputeNormalEdges(Workflow workflow)
|
||||
private static List<(string Source, string Target, bool IsConditional, string? Label)> ComputeNormalEdges(Workflow workflow)
|
||||
{
|
||||
var edges = new List<(string, string, bool)>();
|
||||
var edges = new List<(string, string, bool, string?)>();
|
||||
foreach (var edgeGroup in workflow.Edges.Values.SelectMany(x => x))
|
||||
{
|
||||
if (edgeGroup.Kind == EdgeKind.FanIn)
|
||||
@@ -229,14 +253,15 @@ public static class WorkflowVisualizer
|
||||
case EdgeKind.Direct when edgeGroup.DirectEdgeData != null:
|
||||
var directData = edgeGroup.DirectEdgeData;
|
||||
var isConditional = directData.Condition != null;
|
||||
edges.Add((directData.SourceId, directData.SinkId, isConditional));
|
||||
var label = directData.Label;
|
||||
edges.Add((directData.SourceId, directData.SinkId, isConditional, label));
|
||||
break;
|
||||
|
||||
case EdgeKind.FanOut when edgeGroup.FanOutEdgeData != null:
|
||||
var fanOutData = edgeGroup.FanOutEdgeData;
|
||||
foreach (var sinkId in fanOutData.SinkIds)
|
||||
{
|
||||
edges.Add((fanOutData.SourceId, sinkId, false));
|
||||
edges.Add((fanOutData.SourceId, sinkId, false, fanOutData.Label));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -276,5 +301,24 @@ public static class WorkflowVisualizer
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper method to escape special characters in DOT labels
|
||||
private static string EscapeDotLabel(string label)
|
||||
{
|
||||
return label.Replace("\"", "\\\"").Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
// Helper method to escape special characters in Mermaid labels
|
||||
private static string EscapeMermaidLabel(string label)
|
||||
{
|
||||
return label
|
||||
.Replace("&", "&") // Must be first to avoid double-escaping
|
||||
.Replace("|", "|") // Pipe breaks Mermaid delimiter syntax
|
||||
.Replace("\"", """) // Quote character
|
||||
.Replace("<", "<") // Less than
|
||||
.Replace(">", ">") // Greater than
|
||||
.Replace("\n", "<br/>") // Newline to HTML break
|
||||
.Replace("\r", ""); // Remove carriage return
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -168,6 +168,18 @@ public class WorkflowBuilder
|
||||
return edges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directed edge from the specified source executor to the target executor, optionally guarded by a
|
||||
/// condition.
|
||||
/// </summary>
|
||||
/// <param name="source">The executor that acts as the source node of the edge. Cannot be null.</param>
|
||||
/// <param name="target">The executor that acts as the target node of the edge. Cannot be null.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge(ExecutorBinding source, ExecutorBinding target)
|
||||
=> this.AddEdge<object>(source, target, null, false);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directed edge from the specified source executor to the target executor, optionally guarded by a
|
||||
/// condition.
|
||||
@@ -182,6 +194,20 @@ public class WorkflowBuilder
|
||||
public WorkflowBuilder AddEdge(ExecutorBinding source, ExecutorBinding target, bool idempotent = false)
|
||||
=> this.AddEdge<object>(source, target, null, idempotent);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directed edge from the specified source executor to the target executor.
|
||||
/// </summary>
|
||||
/// <param name="source">The executor that acts as the source node of the edge. Cannot be null.</param>
|
||||
/// <param name="target">The executor that acts as the target node of the edge. Cannot be null.</param>
|
||||
/// <param name="label">An optional label for the edge. Will be used in visualizations.</param>
|
||||
/// <param name="idempotent">If set to <see langword="true"/>, adding the same edge multiple times will be a NoOp,
|
||||
/// rather than an error.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge(ExecutorBinding source, ExecutorBinding target, string? label = null, bool idempotent = false)
|
||||
=> this.AddEdge<object>(source, target, null, label, idempotent);
|
||||
|
||||
internal static Func<object?, bool>? CreateConditionFunc<T>(Func<T?, bool>? condition)
|
||||
{
|
||||
if (condition is null)
|
||||
@@ -222,6 +248,20 @@ public class WorkflowBuilder
|
||||
|
||||
private EdgeId TakeEdgeId() => new(Interlocked.Increment(ref this._edgeCount));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directed edge from the specified source executor to the target executor, optionally guarded by a
|
||||
/// condition.
|
||||
/// </summary>
|
||||
/// <param name="source">The executor that acts as the source node of the edge. Cannot be null.</param>
|
||||
/// <param name="target">The executor that acts as the target node of the edge. Cannot be null.</param>
|
||||
/// <param name="condition">An optional predicate that determines whether the edge should be followed based on the input.
|
||||
/// If null, the edge is always activated when the source sends a message.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge<T>(ExecutorBinding source, ExecutorBinding target, Func<T?, bool>? condition = null)
|
||||
=> this.AddEdge(source, target, condition, label: null, false);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directed edge from the specified source executor to the target executor, optionally guarded by a
|
||||
/// condition.
|
||||
@@ -236,6 +276,23 @@ public class WorkflowBuilder
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge<T>(ExecutorBinding source, ExecutorBinding target, Func<T?, bool>? condition = null, bool idempotent = false)
|
||||
=> this.AddEdge(source, target, condition, label: null, idempotent);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directed edge from the specified source executor to the target executor, optionally guarded by a
|
||||
/// condition.
|
||||
/// </summary>
|
||||
/// <param name="source">The executor that acts as the source node of the edge. Cannot be null.</param>
|
||||
/// <param name="target">The executor that acts as the target node of the edge. Cannot be null.</param>
|
||||
/// <param name="condition">An optional predicate that determines whether the edge should be followed based on the input.
|
||||
/// <param name="label">An optional label for the edge. Will be used in visualizations.</param>
|
||||
/// <param name="idempotent">If set to <see langword="true"/>, adding the same edge multiple times will be a NoOp,
|
||||
/// rather than an error.</param>
|
||||
/// If null, the edge is always activated when the source sends a message.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if an unconditional edge between the specified source and target
|
||||
/// executors already exists.</exception>
|
||||
public WorkflowBuilder AddEdge<T>(ExecutorBinding source, ExecutorBinding target, Func<T?, bool>? condition = null, string? label = null, bool idempotent = false)
|
||||
{
|
||||
// Add an edge from source to target with an optional condition.
|
||||
// This is a low-level builder method that does not enforce any specific executor type.
|
||||
@@ -256,7 +313,7 @@ public class WorkflowBuilder
|
||||
"You cannot add another edge without a condition for the same source and target.");
|
||||
}
|
||||
|
||||
DirectEdgeData directEdge = new(this.Track(source).Id, this.Track(target).Id, this.TakeEdgeId(), CreateConditionFunc(condition));
|
||||
DirectEdgeData directEdge = new(this.Track(source).Id, this.Track(target).Id, this.TakeEdgeId(), CreateConditionFunc(condition), label);
|
||||
|
||||
this.EnsureEdgesFor(source.Id).Add(new(directEdge));
|
||||
|
||||
@@ -275,6 +332,19 @@ public class WorkflowBuilder
|
||||
public WorkflowBuilder AddFanOutEdge(ExecutorBinding source, IEnumerable<ExecutorBinding> targets)
|
||||
=> this.AddFanOutEdge<object>(source, targets, null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a fan-out edge from the specified source executor to one or more target executors, optionally using a
|
||||
/// custom partitioning function.
|
||||
/// </summary>
|
||||
/// <remarks>If a partitioner function is provided, it will be used to distribute input across the target
|
||||
/// executors. The order of targets determines their mapping in the partitioning process.</remarks>
|
||||
/// <param name="source">The source executor from which the fan-out edge originates. Cannot be null.</param>
|
||||
/// <param name="targets">One or more target executors that will receive the fan-out edge. Cannot be null or empty.</param>
|
||||
/// <param name="label">A label for the edge. Will be used in visualization.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
public WorkflowBuilder AddFanOutEdge(ExecutorBinding source, IEnumerable<ExecutorBinding> targets, string label)
|
||||
=> this.AddFanOutEdge<object>(source, targets, null, label);
|
||||
|
||||
internal static Func<object?, int, IEnumerable<int>>? CreateTargetAssignerFunc<T>(Func<T?, int, IEnumerable<int>>? targetAssigner)
|
||||
{
|
||||
if (targetAssigner is null)
|
||||
@@ -305,6 +375,21 @@ public class WorkflowBuilder
|
||||
/// <param name="targetSelector">An optional function that determines how input is assigned among the target executors.
|
||||
/// If null, messages will route to all targets.</param>
|
||||
public WorkflowBuilder AddFanOutEdge<T>(ExecutorBinding source, IEnumerable<ExecutorBinding> targets, Func<T?, int, IEnumerable<int>>? targetSelector = null)
|
||||
=> this.AddFanOutEdge(source, targets, targetSelector, label: null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a fan-out edge from the specified source executor to one or more target executors, optionally using a
|
||||
/// custom partitioning function.
|
||||
/// </summary>
|
||||
/// <remarks>If a partitioner function is provided, it will be used to distribute input across the target
|
||||
/// executors. The order of targets determines their mapping in the partitioning process.</remarks>
|
||||
/// <param name="source">The source executor from which the fan-out edge originates. Cannot be null.</param>
|
||||
/// <param name="targets">One or more target executors that will receive the fan-out edge. Cannot be null or empty.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
/// <param name="targetSelector">An optional function that determines how input is assigned among the target executors.
|
||||
/// If null, messages will route to all targets.</param>
|
||||
/// <param name="label">An optional label for the edge. Will be used in visualizations.</param>
|
||||
public WorkflowBuilder AddFanOutEdge<T>(ExecutorBinding source, IEnumerable<ExecutorBinding> targets, Func<T?, int, IEnumerable<int>>? targetSelector = null, string? label = null)
|
||||
{
|
||||
Throw.IfNull(source);
|
||||
Throw.IfNull(targets);
|
||||
@@ -321,7 +406,8 @@ public class WorkflowBuilder
|
||||
this.Track(source).Id,
|
||||
sinkIds,
|
||||
this.TakeEdgeId(),
|
||||
CreateTargetAssignerFunc(targetSelector));
|
||||
CreateTargetAssignerFunc(targetSelector),
|
||||
label);
|
||||
|
||||
this.EnsureEdgesFor(source.Id).Add(new(fanOutEdge));
|
||||
|
||||
@@ -339,6 +425,20 @@ public class WorkflowBuilder
|
||||
/// <param name="target">The target executor that receives input from the specified source executors. Cannot be null.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
public WorkflowBuilder AddFanInEdge(IEnumerable<ExecutorBinding> sources, ExecutorBinding target)
|
||||
=> this.AddFanInEdge(sources, target, label: null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a fan-in edge to the workflow, connecting multiple source executors to a single target executor with an
|
||||
/// optional trigger condition.
|
||||
/// </summary>
|
||||
/// <remarks>This method establishes a fan-in relationship, allowing the target executor to be activated
|
||||
/// based on the completion or state of multiple sources. The trigger parameter can be used to customize activation
|
||||
/// behavior.</remarks>
|
||||
/// <param name="sources">One or more source executors that provide input to the target. Cannot be null or empty.</param>
|
||||
/// <param name="target">The target executor that receives input from the specified source executors. Cannot be null.</param>
|
||||
/// <param name="label">An optional label for the edge. Will be used in visualizations.</param>
|
||||
/// <returns>The current instance of <see cref="WorkflowBuilder"/>.</returns>
|
||||
public WorkflowBuilder AddFanInEdge(IEnumerable<ExecutorBinding> sources, ExecutorBinding target, string? label = null)
|
||||
{
|
||||
Throw.IfNull(target);
|
||||
Throw.IfNull(sources);
|
||||
@@ -354,7 +454,8 @@ public class WorkflowBuilder
|
||||
FanInEdgeData edgeData = new(
|
||||
sourceIds,
|
||||
this.Track(target).Id,
|
||||
this.TakeEdgeId());
|
||||
this.TakeEdgeId(),
|
||||
label);
|
||||
|
||||
foreach (string sourceId in edgeData.SourceIds)
|
||||
{
|
||||
@@ -380,7 +481,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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user