mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://aka.ms/agent-framework
|
||||
about: Check out the official documentation for guides and API reference.
|
||||
- name: Discussions
|
||||
url: https://github.com/microsoft/agent-framework/discussions
|
||||
about: Ask questions about Agent Framework.
|
||||
@@ -0,0 +1,70 @@
|
||||
name: .NET Bug Report
|
||||
description: Report a bug in the Agent Framework .NET SDK
|
||||
title: ".NET: [Bug]: "
|
||||
labels: ["bug", ".NET"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the bug.
|
||||
placeholder: |
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue.
|
||||
placeholder: |
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: error-messages
|
||||
attributes:
|
||||
label: Error Messages / Stack Traces
|
||||
description: Include any error messages or stack traces you received.
|
||||
placeholder: |
|
||||
```
|
||||
Paste error messages or stack traces here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-packages
|
||||
attributes:
|
||||
label: Package Versions
|
||||
description: List the Microsoft.Agents.* packages and versions you are using
|
||||
placeholder: "e.g., Microsoft.Agents.AI.Abstractions: 1.0.0, Microsoft.Agents.AI.OpenAI: 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: dotnet-version
|
||||
attributes:
|
||||
label: .NET Version
|
||||
description: What version of .NET are you using?
|
||||
placeholder: "e.g., .NET 8.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots that might be helpful.
|
||||
placeholder: "Any additional information..."
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Feature Request
|
||||
description: Request a new feature for Microsoft Agent Framework
|
||||
title: "[Feature]: "
|
||||
type: feature
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please describe the feature you'd like and why it would be useful.
|
||||
placeholder: |
|
||||
Describe the feature you're requesting:
|
||||
- What problem does it solve?
|
||||
- What would the expected behavior be?
|
||||
- Are there any alternatives you've considered?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a code sample showing how you'd like to use this feature.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: language
|
||||
attributes:
|
||||
label: Language/SDK
|
||||
description: Which language/SDK does this feature apply to?
|
||||
options:
|
||||
- Both
|
||||
- .NET
|
||||
- Python
|
||||
- Other / Not Applicable
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Python Bug Report
|
||||
description: Report a bug in the Agent Framework Python SDK
|
||||
title: "Python: [Bug]: "
|
||||
labels: ["bug", "Python"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the bug.
|
||||
placeholder: |
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: error-messages
|
||||
attributes:
|
||||
label: Error Messages / Stack Traces
|
||||
description: Include any error messages or stack traces you received.
|
||||
placeholder: |
|
||||
```
|
||||
Paste error messages or stack traces here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-packages
|
||||
attributes:
|
||||
label: Package Versions
|
||||
description: List the agent-framework-* packages and versions you are using
|
||||
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: python-version
|
||||
attributes:
|
||||
label: Python Version
|
||||
description: What version of Python are you using?
|
||||
placeholder: "e.g., Python 3.11"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots that might be helpful.
|
||||
placeholder: "Any additional information..."
|
||||
validations:
|
||||
required: false
|
||||
@@ -12,7 +12,7 @@ runs:
|
||||
docker rm -f dts-emulator
|
||||
fi
|
||||
echo "Starting Durable Task Scheduler Emulator"
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 -e DTS_USE_DYNAMIC_TASK_HUBS=true mcr.microsoft.com/dts/dts-emulator:latest
|
||||
echo "Waiting for Durable Task Scheduler Emulator to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
|
||||
echo "Durable Task Scheduler Emulator is ready"
|
||||
@@ -28,6 +28,18 @@ runs:
|
||||
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
|
||||
echo "Azurite (Azure Storage emulator) is ready"
|
||||
- name: Start Redis
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=redis)" ]; then
|
||||
echo "Stopping and removing existing Redis"
|
||||
docker rm -f redis
|
||||
fi
|
||||
echo "Starting Redis"
|
||||
docker run -d --name redis -p 6379:6379 redis:latest
|
||||
echo "Waiting for Redis to be ready"
|
||||
timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
|
||||
echo "Redis is ready"
|
||||
- name: Install Azure Functions Core Tools
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -14,6 +14,8 @@ Here are some general guidelines that apply to all code.
|
||||
|
||||
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
- All public methods and classes should have XML documentation comments.
|
||||
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
|
||||
- After adding or modifying code, run `dotnet format` to automatically fix any formatting errors.
|
||||
|
||||
### C# Sample Code Guidelines
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ After completing migration, verify these specific items:
|
||||
1. **Compilation**: Execute `dotnet build` on all modified projects - zero errors required
|
||||
2. **Namespace Updates**: Confirm all `using Microsoft.SemanticKernel.Agents` statements are replaced
|
||||
3. **Method Calls**: Verify all `InvokeAsync` calls are changed to `RunAsync`
|
||||
4. **Return Types**: Confirm handling of `AgentRunResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
4. **Return Types**: Confirm handling of `AgentResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
5. **Thread Creation**: Validate all thread creation uses `agent.GetNewThread()` pattern
|
||||
6. **Tool Registration**: Ensure `[KernelFunction]` attributes are removed and `AIFunctionFactory.Create()` is used
|
||||
7. **Options Configuration**: Verify `AgentRunOptions` or `ChatClientAgentRunOptions` replaces `AgentInvokeOptions`
|
||||
@@ -119,7 +119,7 @@ Agent Framework provides functionality for creating and managing AI agents throu
|
||||
Key API differences:
|
||||
- Agent creation: Remove Kernel dependency, use direct client-based creation
|
||||
- Method names: `InvokeAsync` → `RunAsync`, `InvokeStreamingAsync` → `RunStreamingAsync`
|
||||
- Return types: `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentRunResponse`
|
||||
- Return types: `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentResponse`
|
||||
- Thread creation: Provider-specific constructors → `agent.GetNewThread()`
|
||||
- Tool registration: `KernelPlugin` system → Direct `AIFunction` registration
|
||||
- Options: `AgentInvokeOptions` → Provider-specific run options (e.g., `ChatClientAgentRunOptions`)
|
||||
@@ -166,8 +166,8 @@ Replace these method calls:
|
||||
| `thread.DeleteAsync()` | Provider-specific cleanup | Use provider client directly |
|
||||
|
||||
Return type changes:
|
||||
- `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentRunResponse`
|
||||
- `IAsyncEnumerable<StreamingChatMessageContent>` → `IAsyncEnumerable<AgentRunResponseUpdate>`
|
||||
- `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentResponse`
|
||||
- `IAsyncEnumerable<StreamingChatMessageContent>` → `IAsyncEnumerable<AgentResponseUpdate>`
|
||||
</api_changes>
|
||||
|
||||
<configuration_changes>
|
||||
@@ -191,8 +191,8 @@ Agent Framework changes these behaviors compared to Semantic Kernel Agents:
|
||||
1. **Thread Management**: Agent Framework automatically manages thread state. Semantic Kernel required manual thread updates in some scenarios (e.g., OpenAI Responses).
|
||||
|
||||
2. **Return Types**:
|
||||
- Non-streaming: Returns single `AgentRunResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
- Streaming: Returns `IAsyncEnumerable<AgentRunResponseUpdate>` instead of `IAsyncEnumerable<StreamingChatMessageContent>`
|
||||
- Non-streaming: Returns single `AgentResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
- Streaming: Returns `IAsyncEnumerable<AgentResponseUpdate>` instead of `IAsyncEnumerable<StreamingChatMessageContent>`
|
||||
|
||||
3. **Tool Registration**: Agent Framework uses direct function registration without requiring `[KernelFunction]` attributes.
|
||||
|
||||
@@ -397,7 +397,7 @@ await foreach (AgentResponseItem<ChatMessageContent> item in agent.InvokeAsync(u
|
||||
|
||||
**With this Agent Framework non-streaming pattern:**
|
||||
```csharp
|
||||
AgentRunResponse result = await agent.RunAsync(userInput, thread, options);
|
||||
AgentResponse result = await agent.RunAsync(userInput, thread, options);
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
|
||||
@@ -411,7 +411,7 @@ await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync(
|
||||
|
||||
**With this Agent Framework streaming pattern:**
|
||||
```csharp
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
@@ -420,8 +420,8 @@ await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInpu
|
||||
**Required changes:**
|
||||
1. Replace `agent.InvokeAsync()` with `agent.RunAsync()`
|
||||
2. Replace `agent.InvokeStreamingAsync()` with `agent.RunStreamingAsync()`
|
||||
3. Change return type handling from `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` to `AgentRunResponse`
|
||||
4. Change streaming type from `StreamingChatMessageContent` to `AgentRunResponseUpdate`
|
||||
3. Change return type handling from `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` to `AgentResponse`
|
||||
4. Change streaming type from `StreamingChatMessageContent` to `AgentResponseUpdate`
|
||||
5. Remove `await foreach` for non-streaming calls
|
||||
6. Access message content directly from result object instead of iterating
|
||||
</api_changes>
|
||||
@@ -661,7 +661,7 @@ await foreach (var result in agent.InvokeAsync(input, thread, options))
|
||||
```csharp
|
||||
ChatClientAgentRunOptions options = new(new ChatOptions { MaxOutputTokens = 1000 });
|
||||
|
||||
AgentRunResponse result = await agent.RunAsync(input, thread, options);
|
||||
AgentResponse result = await agent.RunAsync(input, thread, options);
|
||||
Console.WriteLine(result);
|
||||
|
||||
// Access underlying content when needed:
|
||||
@@ -689,7 +689,7 @@ await foreach (var result in agent.InvokeAsync(input, thread, options))
|
||||
|
||||
**With this Agent Framework non-streaming usage pattern:**
|
||||
```csharp
|
||||
AgentRunResponse result = await agent.RunAsync(input, thread, options);
|
||||
AgentResponse result = await agent.RunAsync(input, thread, options);
|
||||
Console.WriteLine($"Tokens: {result.Usage.TotalTokenCount}");
|
||||
```
|
||||
|
||||
@@ -709,7 +709,7 @@ await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsyn
|
||||
|
||||
**With this Agent Framework streaming usage pattern:**
|
||||
```csharp
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
|
||||
{
|
||||
if (update.Contents.OfType<UsageContent>().FirstOrDefault() is { } usageContent)
|
||||
{
|
||||
@@ -839,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()
|
||||
{
|
||||
|
||||
@@ -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,9 +83,19 @@ 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
|
||||
uses: actions/setup-dotnet@v5.1.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
- name: Build dotnet solutions
|
||||
@@ -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
|
||||
|
||||
@@ -45,19 +45,58 @@ jobs:
|
||||
labels.push("triage")
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the word 'python' (case-insensitive)
|
||||
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
|
||||
// Add the 'python' label to the array
|
||||
labels.push("python")
|
||||
// Helper function to extract field value from issue form body
|
||||
// Issue forms format fields as: ### Field Name\n\nValue
|
||||
function getFormFieldValue(body, fieldName) {
|
||||
if (!body) return null
|
||||
const regex = new RegExp(`###\\s*${fieldName}\\s*\\n\\n([^\\n#]+)`, 'i')
|
||||
const match = body.match(regex)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
|
||||
if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) ||
|
||||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
|
||||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
|
||||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
|
||||
// Add the '.NET' label to the array
|
||||
labels.push(".NET")
|
||||
// Check for language from issue form dropdown first
|
||||
const languageField = getFormFieldValue(body, 'Language')
|
||||
let languageLabelAdded = false
|
||||
|
||||
if (languageField) {
|
||||
if (languageField === 'Python') {
|
||||
labels.push("python")
|
||||
languageLabelAdded = true
|
||||
} else if (languageField === '.NET') {
|
||||
labels.push(".NET")
|
||||
languageLabelAdded = true
|
||||
}
|
||||
// 'None / Not Applicable' - don't add any language label
|
||||
}
|
||||
|
||||
// Fallback: Check if the body or the title contains the word 'python' (case-insensitive)
|
||||
// Only if language wasn't already determined from the form field
|
||||
if (!languageLabelAdded) {
|
||||
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
|
||||
// Add the 'python' label to the array
|
||||
labels.push("python")
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
|
||||
if ((body != null && body.match(/\.net/i)) || (title != null && title.match(/\.net/i)) ||
|
||||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
|
||||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
|
||||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
|
||||
// Add the '.NET' label to the array
|
||||
labels.push(".NET")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue type from issue form dropdown
|
||||
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
|
||||
if (issueTypeField) {
|
||||
if (issueTypeField === 'Bug') {
|
||||
labels.push("bug")
|
||||
} else if (issueTypeField === 'Feature Request') {
|
||||
labels.push("enhancement")
|
||||
} else if (issueTypeField === 'Question') {
|
||||
labels.push("question")
|
||||
}
|
||||
}
|
||||
|
||||
// Add the labels to the issue (only if there are labels to add)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -29,3 +29,4 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout: 3600
|
||||
interval: 30
|
||||
ignored: CodeQL,CodeQL analysis (csharp)
|
||||
|
||||
@@ -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
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
id: azure-functions-setup
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 600 --retries 3 --retry-delay 10
|
||||
working-directory: ./python
|
||||
- name: Test core samples
|
||||
timeout-minutes: 10
|
||||
@@ -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
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe azure-ai-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
working-directory: ./python
|
||||
- name: Test Azure AI samples
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -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 }}
|
||||
@@ -34,9 +34,16 @@ jobs:
|
||||
# because the workflow_run event does not have access to the PR number
|
||||
# The PR number is needed to post the comment on the PR
|
||||
run: |
|
||||
PR_NUMBER=$(cat pr_number)
|
||||
echo "PR number: $PR_NUMBER"
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
if [ ! -s pr_number ]; then
|
||||
echo "PR number file 'pr_number' is missing or empty"
|
||||
exit 1
|
||||
fi
|
||||
PR_NUMBER=$(head -1 pr_number | tr -dc '0-9')
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "PR number file 'pr_number' does not contain a valid PR number"
|
||||
exit 1
|
||||
fi
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@v1.2.0
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-5
@@ -206,15 +206,17 @@ agents.md
|
||||
WARP.md
|
||||
**/memory-bank/
|
||||
**/projectBrief.md
|
||||
**/tmpclaude*
|
||||
|
||||
# Azurite storage emulator files
|
||||
*/__azurite_db_blob__.json
|
||||
*/__azurite_db_blob_extent__.json
|
||||
*/__azurite_db_queue__.json
|
||||
*/__azurite_db_queue_extent__.json
|
||||
*/__azurite_db_table__.json
|
||||
*/__azurite_db_blob__.json*
|
||||
*/__azurite_db_blob_extent__.json*
|
||||
*/__azurite_db_queue__.json*
|
||||
*/__azurite_db_queue_extent__.json*
|
||||
*/__azurite_db_table__.json*
|
||||
*/__blobstorage__/
|
||||
*/__queuestorage__/
|
||||
*/AzuriteConfig
|
||||
|
||||
# Azure Functions local settings
|
||||
local.settings.json
|
||||
@@ -226,3 +228,4 @@ local.settings.json
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
python/dotnet-ref
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Declarative Agents
|
||||
|
||||
This folder contains sample agent definitions than be ran using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
|
||||
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
|
||||
|
||||
@@ -10,19 +10,19 @@ model:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
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.
|
||||
|
||||
@@ -64,7 +64,7 @@ Approaches observed from the compared SDKs:
|
||||
| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
|
||||
| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
|
||||
| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
|
||||
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
|
||||
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
|
||||
| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
|
||||
| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
|
||||
| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
|
||||
@@ -163,8 +163,8 @@ foreach (var update in response.Messages)
|
||||
### Option 2 Run: Container with Primary and Secondary Properties, RunStreaming: Stream of Primary + Secondary
|
||||
|
||||
Run returns a new response type that has separate properties for the Primary Content and the Secondary Updates leading up to it.
|
||||
The Primary content is available in the `AgentRunResponse.Messages` property while Secondary updates are in a new `AgentRunResponse.Updates` property.
|
||||
`AgentRunResponse.Text` returns the Primary content text.
|
||||
The Primary content is available in the `AgentResponse.Messages` property while Secondary updates are in a new `AgentResponse.Updates` property.
|
||||
`AgentResponse.Text` returns the Primary content text.
|
||||
|
||||
Since streaming would still need to return an `IAsyncEnumerable` of updates, the design would differ from non-streaming.
|
||||
With non-streaming Primary and Secondary content is split into separate lists, while with streaming it's combined in one stream.
|
||||
@@ -232,24 +232,24 @@ await foreach (var update in responses)
|
||||
```csharp
|
||||
class Agent
|
||||
{
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
public abstract Task<AgentResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
class AgentRunResponse : ChatResponse
|
||||
class AgentResponse : ChatResponse
|
||||
{
|
||||
}
|
||||
|
||||
public class AgentRunResponseUpdate : ChatResponseUpdate
|
||||
public class AgentResponseUpdate : ChatResponseUpdate
|
||||
{
|
||||
}
|
||||
```
|
||||
@@ -265,20 +265,20 @@ The new types could also exclude properties that make less sense for agents, lik
|
||||
```csharp
|
||||
class Agent
|
||||
{
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
public abstract Task<AgentResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
class AgentRunResponse // Compare with ChatResponse
|
||||
class AgentResponse // Compare with ChatResponse
|
||||
{
|
||||
public string Text { get; } // Aggregation of TextContent from messages.
|
||||
|
||||
@@ -294,12 +294,12 @@ class AgentRunResponse // Compare with ChatResponse
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
// Not Included in AgentRunResponse compared to ChatResponse
|
||||
// Not Included in AgentResponse compared to ChatResponse
|
||||
public ChatFinishReason? FinishReason { get; set; }
|
||||
public string? ConversationId { get; set; }
|
||||
public string? ModelId { get; set; }
|
||||
|
||||
public class AgentRunResponseUpdate // Compare with ChatResponseUpdate
|
||||
public class AgentResponseUpdate // Compare with ChatResponseUpdate
|
||||
{
|
||||
public string Text { get; } // Aggregation of TextContent from Contents.
|
||||
|
||||
@@ -317,7 +317,7 @@ public class AgentRunResponseUpdate // Compare with ChatResponseUpdate
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
// Not Included in AgentRunResponseUpdate compared to ChatResponseUpdate
|
||||
// Not Included in AgentResponseUpdate compared to ChatResponseUpdate
|
||||
public ChatFinishReason? FinishReason { get; set; }
|
||||
public string? ConversationId { get; set; }
|
||||
public string? ModelId { get; set; }
|
||||
@@ -360,7 +360,7 @@ public class ChatFinishReason
|
||||
### Option 2: Add another property on responses for AgentRun
|
||||
|
||||
```csharp
|
||||
class AgentRunResponse
|
||||
class AgentResponse
|
||||
{
|
||||
...
|
||||
public AgentRun RunReference { get; set; } // Reference to long running process
|
||||
@@ -368,7 +368,7 @@ class AgentRunResponse
|
||||
}
|
||||
|
||||
|
||||
public class AgentRunResponseUpdate
|
||||
public class AgentResponseUpdate
|
||||
{
|
||||
...
|
||||
public AgentRun RunReference { get; set; } // Reference to long running process
|
||||
@@ -424,7 +424,7 @@ Note that where an agent doesn't support structured output, it may also be possi
|
||||
See [Structured Outputs Support](#structured-outputs-support) for a comparison on what other agent frameworks and protocols support.
|
||||
|
||||
To support a good user experience for structured outputs, I'm proposing that we follow the pattern used by MEAI.
|
||||
We would add a generic version of `AgentRunResponse<T>`, that allows us to get the agent result already deserialized into our preferred type.
|
||||
We would add a generic version of `AgentResponse<T>`, that allows us to get the agent result already deserialized into our preferred type.
|
||||
This would be coupled with generic overload extension methods for Run that automatically builds a schema from the supplied type and updates
|
||||
the run options.
|
||||
|
||||
@@ -438,14 +438,14 @@ class Movie
|
||||
public int ReleaseYear { get; set; }
|
||||
}
|
||||
|
||||
AgentRunResponse<Movie[]> response = agent.RunAsync<Movie[]>("What are the top 3 children's movies of the 80s.");
|
||||
AgentResponse<Movie[]> response = agent.RunAsync<Movie[]>("What are the top 3 children's movies of the 80s.");
|
||||
Movie[] movies = response.Result
|
||||
```
|
||||
|
||||
If we only support requesting a schema at agent creation time or where an agent has a built in schema, the following would be the preferred approach:
|
||||
|
||||
```csharp
|
||||
AgentRunResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
|
||||
AgentResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
|
||||
Movie[] movies = response.TryParseStructuredOutput<Movie[]>();
|
||||
```
|
||||
|
||||
@@ -463,7 +463,7 @@ Option 2 chosen so that we can vary Agent responses independently of Chat Client
|
||||
### StructuredOutputs Decision
|
||||
|
||||
We will not support structured output per run request, but individual agents are free to allow this on the concrete implementation or at construction time.
|
||||
We will however add support for easily extracting a structured output type from the `AgentRunResponse`.
|
||||
We will however add support for easily extracting a structured output type from the `AgentResponse`.
|
||||
|
||||
## Addendum 1: AIContext Derived Types for different response types / Gap Analysis (Work in progress)
|
||||
|
||||
@@ -495,8 +495,8 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
| SDK | Structured Outputs support |
|
||||
|-|-|
|
||||
| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
|
||||
| Google ADK | **Approach 1** Both [input and output shemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
|
||||
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.structured_output) |
|
||||
| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
|
||||
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
|
||||
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
|
||||
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
|
||||
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
|
||||
@@ -508,7 +508,7 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
|-|-|
|
||||
| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
|
||||
| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
|
||||
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/api-reference/types/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) class with options that are tied closely to LLM operations. |
|
||||
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
|
||||
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
|
||||
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
|
||||
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
|
||||
|
||||
@@ -54,7 +54,7 @@ The table below represents the majority of the naming changes discussed in issue
|
||||
| *Mcp* & *Http* | *MCP* & *HTTP* | accepted | Acronyms should be uppercased in class names, according to PEP 8. | None |
|
||||
| `agent.run_streaming` | `agent.run_stream` | accepted | Shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
|
||||
| `workflow.run_streaming` | `workflow.run_stream` | accepted | In sync with `agent.run_stream` and shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
|
||||
| AgentRunResponse & AgentRunResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
|
||||
| AgentResponse & AgentResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
|
||||
| *Content | * | rejected | Rejected other content type renames (removing `Content` suffix) because it would reduce clarity and discoverability. | Item was also considered, but rejected as it is very similar to Content, but would be inconsistent with dotnet. |
|
||||
| ChatResponse & ChatResponseUpdate | Response & ResponseUpdate | rejected | Rejected, because Response is too generic. | None |
|
||||
|
||||
|
||||
@@ -161,11 +161,11 @@ while (response.ApprovalRequests.Count > 0)
|
||||
response = await agent.RunAsync(messages, thread);
|
||||
}
|
||||
|
||||
class AgentRunResponse
|
||||
class AgentResponse
|
||||
{
|
||||
...
|
||||
|
||||
// A new property on AgentRunResponse to aggregate the ApprovalRequestContent items from
|
||||
// A new property on AgentResponse to aggregate the ApprovalRequestContent items from
|
||||
// the response messages (Similar to the Text property).
|
||||
public IEnumerable<ApprovalRequestContent> ApprovalRequests { get; set; }
|
||||
|
||||
@@ -251,11 +251,11 @@ while (response.UserInputRequests.Any())
|
||||
response = await agent.RunAsync(messages, thread);
|
||||
}
|
||||
|
||||
class AgentRunResponse
|
||||
class AgentResponse
|
||||
{
|
||||
...
|
||||
|
||||
// A new property on AgentRunResponse to aggregate the UserInputRequestContent items from
|
||||
// A new property on AgentResponse to aggregate the UserInputRequestContent items from
|
||||
// the response messages (Similar to the Text property).
|
||||
public IReadOnlyList<UserInputRequestContent> UserInputRequests { get; set; }
|
||||
|
||||
@@ -366,11 +366,11 @@ while (response.UserInputRequests.Any())
|
||||
response = await agent.RunAsync(messages, thread);
|
||||
}
|
||||
|
||||
class AgentRunResponse
|
||||
class AgentResponse
|
||||
{
|
||||
...
|
||||
|
||||
// A new property on AgentRunResponse to aggregate the UserInputRequestContent items from
|
||||
// A new property on AgentResponse to aggregate the UserInputRequestContent items from
|
||||
// the response messages (Similar to the Text property).
|
||||
public IEnumerable<UserInputRequestContent> UserInputRequests { get; set; }
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ public class AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AgentRunResponse> RunAsync(
|
||||
public async Task<AgentResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -135,7 +135,7 @@ public class AIAgent
|
||||
return context.Response ?? throw new InvalidOperationException("Agent execution did not produce a response");
|
||||
}
|
||||
|
||||
protected abstract Task<AgentRunResponse> ExecuteCoreLogicAsync(
|
||||
protected abstract Task<AgentResponse> ExecuteCoreLogicAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread,
|
||||
AgentRunOptions? options,
|
||||
@@ -190,7 +190,7 @@ internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
|
||||
|
||||
public GuardrailCallbackAgent(AIAgent innerAgent) : base(innerAgent) { }
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filteredMessages = this.FilterMessages(messages);
|
||||
Console.WriteLine($"Guardrail Middleware - Filtered messages: {new ChatResponse(filteredMessages).Text}");
|
||||
@@ -202,14 +202,14 @@ internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filteredMessages = this.FilterMessages(messages);
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(filteredMessages, thread, options, cancellationToken))
|
||||
{
|
||||
if (update.Text != null)
|
||||
{
|
||||
yield return new AgentRunResponseUpdate(update.Role, this.FilterContent(update.Text));
|
||||
yield return new AgentResponseUpdate(update.Role, this.FilterContent(update.Text));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -252,7 +252,7 @@ internal sealed class RunningCallbackHandlerAgent : DelegatingAIAgent
|
||||
this._func = func;
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = new AgentInvokeCallbackContext(this, messages, thread, options, isStreaming: false, cancellationToken);
|
||||
|
||||
@@ -469,7 +469,7 @@ public sealed class CallbackEnabledAgent : DelegatingAIAgent
|
||||
this._callbacksProcessor = callbackMiddlewareProcessor ?? new();
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
public override async Task<AgentResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -541,7 +541,7 @@ public abstract class AgentContext
|
||||
public class AgentRunContext : AgentContext
|
||||
{
|
||||
public IList<ChatMessage> Messages { get; set; }
|
||||
public AgentRunResponse? Response { get; set; }
|
||||
public AgentResponse? Response { get; set; }
|
||||
public AgentThread? Thread { get; }
|
||||
|
||||
public AgentRunContext(AIAgent agent, IList<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options)
|
||||
|
||||
@@ -687,7 +687,7 @@ This section considers different options for exposing the `RunId`, `Status`, and
|
||||
#### 4.1. As AIContent
|
||||
|
||||
The `AsyncRunContent` class will represent a long-running operation initiated and managed by an agent/LLM.
|
||||
Items of this content type will be returned in a chat message as part of the `AgentRunResponse` or `ChatResponse`
|
||||
Items of this content type will be returned in a chat message as part of the `AgentResponse` or `ChatResponse`
|
||||
response to represent the long-running operation.
|
||||
|
||||
The `AsyncRunContent` class has two properties: `RunId` and `Status`. The `RunId` identifies the
|
||||
@@ -1162,29 +1162,29 @@ For cancellation and deletion of long-running operations, new methods will be ad
|
||||
public abstract class AIAgent
|
||||
{
|
||||
// Existing methods...
|
||||
public Task<AgentRunResponse> RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
public Task<AgentResponse> RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
|
||||
// New methods for uncommon operations
|
||||
public virtual Task<AgentRunResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public virtual Task<AgentResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<AgentRunResponse?>(null);
|
||||
return Task.FromResult<AgentResponse?>(null);
|
||||
}
|
||||
|
||||
public virtual Task<AgentRunResponse?> DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public virtual Task<AgentResponse?> DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<AgentRunResponse?>(null);
|
||||
return Task.FromResult<AgentResponse?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Agent that supports update and cancellation
|
||||
public class CustomAgent : AIAgent
|
||||
{
|
||||
public override async Task<AgentRunResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<AgentResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await this._client.CancelRunAsync(id, options?.Thread?.ConversationId);
|
||||
|
||||
return ConvertToAgentRunResponse(response);
|
||||
return ConvertToAgentResponse(response);
|
||||
}
|
||||
|
||||
// No overload for DeleteRunAsync as it's not supported by the underlying API
|
||||
@@ -1195,7 +1195,7 @@ AIAgent agent = new CustomAgent();
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
AgentRunResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
AgentResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
|
||||
response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOptions { Thread = thread });
|
||||
```
|
||||
@@ -1251,10 +1251,10 @@ public class AgentRunOptions
|
||||
AIAgent agent = ...; // Get an instance of an AIAgent
|
||||
|
||||
// Start a long-running execution for the prompt if supported by the underlying API
|
||||
AgentRunResponse response = await agent.RunAsync("<prompt>", new AgentRunOptions { AllowLongRunningResponses = true });
|
||||
AgentResponse response = await agent.RunAsync("<prompt>", new AgentRunOptions { AllowLongRunningResponses = true });
|
||||
|
||||
// Start a quick prompt
|
||||
AgentRunResponse response = await agent.RunAsync("<prompt>");
|
||||
AgentResponse response = await agent.RunAsync("<prompt>");
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
@@ -1279,7 +1279,7 @@ Below are the details of the option selected for chat clients that is also selec
|
||||
#### 3.1 Continuation Token of a Custom Type
|
||||
|
||||
This option suggests using `ContinuationToken` to encapsulate all properties representing a long-running operation. The continuation token will be returned by agents in the
|
||||
`ContinuationToken` property of the `AgentRunResponse` and `AgentRunResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
|
||||
`ContinuationToken` property of the `AgentResponse` and `AgentResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
|
||||
of the property will indicate that the response is not part of a long-running operation or the long-running operation has been completed. Callers will set the token in the
|
||||
`ContinuationToken` property of the `AgentRunOptions` class in follow-up calls to the `Run{Streaming}Async` methods to indicate that they want to "continue" the long-running
|
||||
operation identified by the token.
|
||||
@@ -1313,18 +1313,18 @@ public class AgentRunOptions
|
||||
public ResponseContinuationToken? ContinuationToken { get; set; }
|
||||
}
|
||||
|
||||
public class AgentRunResponse
|
||||
public class AgentResponse
|
||||
{
|
||||
public ResponseContinuationToken? ContinuationToken { get; }
|
||||
}
|
||||
|
||||
public class AgentRunResponseUpdate
|
||||
public class AgentResponseUpdate
|
||||
{
|
||||
public ResponseContinuationToken? ContinuationToken { get; }
|
||||
}
|
||||
|
||||
// Usage example
|
||||
AgentRunResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
AgentResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
|
||||
AgentRunOptions options = new() { ContinuationToken = response.ContinuationToken };
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Chosen option: "Current approach with internal event types and framework-native
|
||||
|
||||
- Protects consumers from protocol changes by keeping AG-UI events internal
|
||||
- Maintains framework abstractions through conversion at boundaries
|
||||
- Uses existing framework types (AgentRunResponseUpdate, ChatMessage) for public API
|
||||
- Uses existing framework types (AgentResponseUpdate, ChatMessage) for public API
|
||||
- Focuses on core text streaming functionality
|
||||
- Leverages existing properties (ConversationId, ResponseId, ErrorContent) instead of custom types
|
||||
- Provides bidirectional client and server support
|
||||
@@ -69,7 +69,7 @@ Chosen option: "Current approach with internal event types and framework-native
|
||||
|
||||
3. **Agent Factory Pattern** - `MapAGUIAgent` uses factory function `(messages) => AIAgent` to allow request-specific agent configuration supporting multi-tenancy
|
||||
|
||||
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentRunResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentRunResponseUpdate`)
|
||||
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentResponseUpdate`)
|
||||
|
||||
5. **Thread Management** - `AGUIAgentThread` stores only `ThreadId` with thread ID communicated via `ConversationId`; applications manage persistence for parity with other implementations and to be compliant with the protocol. Future extensions will support having the server manage the conversation.
|
||||
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: dmytrostruk
|
||||
date: 2025-12-12
|
||||
deciders: dmytrostruk, markwallace-microsoft, eavanvalkenburg, giles17
|
||||
---
|
||||
|
||||
# Create/Get Agent API
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
There is a misalignment between the create/get agent API in the .NET and Python implementations.
|
||||
|
||||
In .NET, the `CreateAIAgent` method can create either a local instance of an agent or a remote instance if the backend provider supports it. For remote agents, once the agent is created, you can retrieve an existing remote agent by using the `GetAIAgent` method. If a backend provider doesn't support remote agents, `CreateAIAgent` just initializes a new local agent instance and `GetAIAgent` is not available. There is also a `BuildAIAgent` method, which is an extension for the `ChatClientBuilder` class from `Microsoft.Extensions.AI`. It builds pipelines of `IChatClient` instances with an `IServiceProvider`. This functionality does not exist in Python, so `BuildAIAgent` is out of scope.
|
||||
|
||||
In Python, there is only one `create_agent` method, which always creates a local instance of the agent. If the backend provider supports remote agents, the remote agent is created only on the first `agent.run()` invocation.
|
||||
|
||||
Below is a short summary of different providers and their APIs in .NET:
|
||||
|
||||
| Package | Method | Behavior | Python support |
|
||||
|---|---|---|---|
|
||||
| Microsoft.Agents.AI | `CreateAIAgent` (based on `IChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.Anthropic | `CreateAIAgent` (based on `IBetaService` and `IAnthropicClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`AnthropicClient` inherits `BaseChatClient`, which exposes `create_agent`). |
|
||||
| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent` (based on `AIProjectClient` with `AgentReference`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent`/`GetAIAgentAsync` (with `Name`/`ChatClientAgentOptions`) | Fetches `AgentRecord` via HTTP, then creates a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.AzureAI (V2) | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AIProjectClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent` (based on `PersistentAgentsClient` with `PersistentAgent`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `PersistentAgent` via HTTP, then creates a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `CreateAIAgent`/`CreateAIAgentAsync` | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.OpenAI | `GetAIAgent` (based on `AssistantClient` with `Assistant`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.OpenAI | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `Assistant` via HTTP, then creates a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AssistantClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `ChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `OpenAIResponseClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
|
||||
|
||||
Another difference between Python and .NET implementation is that in .NET `CreateAIAgent`/`GetAIAgent` methods are implemented as extension methods based on underlying SDK client, like `AIProjectClient` from Azure AI or `AssistantClient` from OpenAI:
|
||||
|
||||
```csharp
|
||||
// Definition
|
||||
public static ChatClientAgent CreateAIAgent(
|
||||
this AIProjectClient aiProjectClient,
|
||||
string name,
|
||||
string model,
|
||||
string instructions,
|
||||
string? description = null,
|
||||
IList<AITool>? tools = null,
|
||||
Func<IChatClient, IChatClient>? clientFactory = null,
|
||||
IServiceProvider? services = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{ }
|
||||
|
||||
// Usage
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); // Initialization of underlying SDK client
|
||||
|
||||
var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AgentName, model: deploymentName, instructions: AgentInstructions, tools: [tool]); // ChatClientAgent creation from underlying SDK client
|
||||
|
||||
// Alternative usage (same as extension method, just explicit syntax)
|
||||
var newAgent = await AzureAIProjectChatClientExtensions.CreateAIAgentAsync(
|
||||
aiProjectClient,
|
||||
name: AgentName,
|
||||
model: deploymentName,
|
||||
instructions: AgentInstructions,
|
||||
tools: [tool]);
|
||||
```
|
||||
|
||||
Python doesn't support extension methods. Currently `create_agent` method is defined on `BaseChatClient`, but this method only creates a local instance of `ChatAgent` and it can't create remote agents for providers that support it for a couple of reasons:
|
||||
|
||||
- It's defined as non-async.
|
||||
- `BaseChatClient` implementation is stateful for providers like Azure AI or OpenAI Assistants. The implementation stores agent/assistant metadata like `AgentId` and `AgentName`, so currently it's not possible to create different instances of `ChatAgent` from a single `BaseChatClient` in case if the implementation is stateful.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- API should be aligned between .NET and Python.
|
||||
- API should be intuitive and consistent between backend providers in .NET and Python.
|
||||
|
||||
## Considered Options
|
||||
|
||||
Add missing implementations on the Python side. This should include the following:
|
||||
|
||||
### agent-framework-azure-ai (both V1 and V2)
|
||||
|
||||
- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
|
||||
- Add a `get_agent` method that accepts an agent identifier, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
|
||||
- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
|
||||
|
||||
.NET:
|
||||
|
||||
```csharp
|
||||
var agent1 = new AIProjectClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from Azure.AI.Projects.OpenAI.AgentReference
|
||||
var agent2 = new AIProjectClient(...).GetAIAgent(agentName); // Fetches agent data, creates a local ChatClientAgent instance
|
||||
var agent3 = new AIProjectClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
|
||||
```
|
||||
|
||||
### agent-framework-core (OpenAI Assistants)
|
||||
|
||||
- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
|
||||
- Add a `get_agent` method that accepts an agent name, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
|
||||
- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
|
||||
|
||||
.NET:
|
||||
|
||||
```csharp
|
||||
var agent1 = new AssistantClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from OpenAI.Assistants.Assistant
|
||||
var agent2 = new AssistantClient(...).GetAIAgent(agentId); // Fetches agent data, creates a local ChatClientAgent instance
|
||||
var agent3 = new AssistantClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
|
||||
```
|
||||
|
||||
### Possible Python implementations
|
||||
|
||||
Methods like `create_agent` and `get_agent` should be implemented separately or defined on some stateless component that will allow to create multiple agents from the same instance/place.
|
||||
|
||||
Possible options:
|
||||
|
||||
#### Option 1: Module-level functions
|
||||
|
||||
Implement free functions in the provider package that accept the underlying SDK client as the first argument (similar to .NET extension methods, but expressed in Python).
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from agent_framework.azure import create_agent, get_agent
|
||||
|
||||
ai_project_client = AIProjectClient(...)
|
||||
|
||||
# Creates a remote agent first, then returns a local ChatAgent wrapper
|
||||
created_agent = await create_agent(
|
||||
ai_project_client,
|
||||
name="",
|
||||
instructions="",
|
||||
tools=[tool],
|
||||
)
|
||||
|
||||
# Gets an existing remote agent and returns a local ChatAgent wrapper
|
||||
first_agent = await get_agent(ai_project_client, agent_id=agent_id)
|
||||
|
||||
# Wraps an SDK agent instance (no extra HTTP call)
|
||||
second_agent = get_agent(ai_project_client, agent_reference)
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- Naturally supports async `create_agent` / `get_agent`.
|
||||
- Supports multiple agents per SDK client.
|
||||
- Closest conceptual match to .NET extension methods while staying Pythonic.
|
||||
|
||||
Cons:
|
||||
|
||||
- Discoverability is lower (users need to know where the functions live).
|
||||
- Verbose when creating multiple agents (client must be passed every time):
|
||||
|
||||
```python
|
||||
agent1 = await azure_agents.create_agent(client, name="Agent1", ...)
|
||||
agent2 = await azure_agents.create_agent(client, name="Agent2", ...)
|
||||
```
|
||||
|
||||
#### Option 2: Provider object
|
||||
|
||||
Introduce a dedicated provider type that is constructed from the underlying SDK client, and exposes async `create_agent` / `get_agent` methods.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIAgentProvider
|
||||
|
||||
ai_project_client = AIProjectClient(...)
|
||||
provider = AzureAIAgentProvider(ai_project_client)
|
||||
|
||||
agent = await provider.create_agent(
|
||||
name="",
|
||||
instructions="",
|
||||
tools=[tool],
|
||||
)
|
||||
|
||||
agent = await provider.get_agent(agent_id=agent_id)
|
||||
agent = provider.get_agent(agent_reference=agent_reference)
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- High discoverability and clear grouping of related behavior.
|
||||
- Keeps SDK clients unchanged and supports multiple agents per SDK client.
|
||||
- Concise when creating multiple agents (client passed once):
|
||||
|
||||
```python
|
||||
provider = AzureAIAgentProvider(ai_project_client)
|
||||
agent1 = await provider.create_agent(name="Agent1", ...)
|
||||
agent2 = await provider.create_agent(name="Agent2", ...)
|
||||
```
|
||||
|
||||
Cons:
|
||||
|
||||
- Adds a new public concept/type for users to learn.
|
||||
|
||||
#### Option 3: Inheritance (SDK client subclass)
|
||||
|
||||
Create a subclass of the underlying SDK client and add `create_agent` / `get_agent` methods.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
class ExtendedAIProjectClient(AIProjectClient):
|
||||
async def create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
|
||||
...
|
||||
|
||||
async def get_agent(self, *, agent_id: str | None = None, sdk_agent=None, **kwargs) -> ChatAgent:
|
||||
...
|
||||
|
||||
client = ExtendedAIProjectClient(...)
|
||||
agent = await client.create_agent(name="", instructions="")
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- Discoverable and ergonomic call sites.
|
||||
- Mirrors the .NET “methods on the client” feeling.
|
||||
|
||||
Cons:
|
||||
|
||||
- Many SDK clients are not designed for inheritance; SDK upgrades can break subclasses.
|
||||
- Users must opt into subclass everywhere.
|
||||
- Typing/initialization can be tricky if the SDK client has non-trivial constructors.
|
||||
|
||||
#### Option 4: Monkey patching
|
||||
|
||||
Attach `create_agent` / `get_agent` methods to an SDK client class (or instance) at runtime.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
def _create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
|
||||
...
|
||||
|
||||
AIProjectClient.create_agent = _create_agent # monkey patch
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- Produces “extension method-like” call sites without wrappers or subclasses.
|
||||
|
||||
Cons:
|
||||
|
||||
- Fragile across SDK updates and difficult to type-check.
|
||||
- Surprising behavior (global side effects), potential conflicts across packages.
|
||||
- Harder to support/debug, especially in larger apps and test suites.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Implement `create_agent`/`get_agent`/`as_agent` API via **Option 2: Provider object**.
|
||||
|
||||
### Rationale
|
||||
|
||||
| Aspect | Option 1 (Functions) | Option 2 (Provider) |
|
||||
|--------|----------------------|---------------------|
|
||||
| Multiple implementations | One package may contain V1, V2, and other agent types. Function names like `create_agent` become ambiguous - which agent type does it create? | Each provider class is explicit: `AzureAIAgentsProvider` vs `AzureAIProjectAgentProvider` |
|
||||
| Discoverability | Users must know to import specific functions from the package | IDE autocomplete on provider instance shows all available methods |
|
||||
| Client reuse | SDK client must be passed to every function call: `create_agent(client, ...)`, `get_agent(client, ...)` | SDK client passed once at construction: `provider = Provider(client)` |
|
||||
|
||||
**Option 1 example:**
|
||||
```python
|
||||
from agent_framework.azure import create_agent, get_agent
|
||||
agent1 = await create_agent(client, name="Agent1", ...) # Which agent type, V1 or V2?
|
||||
agent2 = await create_agent(client, name="Agent2", ...) # Repetitive client passing
|
||||
```
|
||||
|
||||
**Option 2 example:**
|
||||
```python
|
||||
from agent_framework.azure import AzureAIProjectAgentProvider
|
||||
provider = AzureAIProjectAgentProvider(client) # Clear which service, client passed once
|
||||
agent1 = await provider.create_agent(name="Agent1", ...)
|
||||
agent2 = await provider.create_agent(name="Agent2", ...)
|
||||
```
|
||||
|
||||
### Method Naming
|
||||
|
||||
| Operation | Python | .NET | Async |
|
||||
|-----------|--------|------|-------|
|
||||
| Create on service | `create_agent()` | `CreateAIAgent()` | Yes |
|
||||
| Get from service | `get_agent(id=...)` | `GetAIAgent(agentId)` | Yes |
|
||||
| Wrap SDK object | `as_agent(reference)` | `AsAIAgent(agentInstance)` | No |
|
||||
|
||||
The method names (`create_agent`, `get_agent`) do not explicitly mention "service" or "remote" because:
|
||||
- In Python, the provider class name explicitly identifies the service (`AzureAIAgentsProvider`, `OpenAIAssistantProvider`), making additional qualifiers in method names redundant.
|
||||
- In .NET, these are extension methods on `AIProjectClient` or `AssistantClient`, which already imply service operations.
|
||||
|
||||
### Provider Class Naming
|
||||
|
||||
| Package | Provider Class | SDK Client | Service |
|
||||
|---------|---------------|------------|---------|
|
||||
| `agent_framework.azure` | `AzureAIProjectAgentProvider` | `AIProjectClient` | Azure AI Agent Service, based on Responses API (V2) |
|
||||
| `agent_framework.azure` | `AzureAIAgentsProvider` | `AgentsClient` | Azure AI Agent Service (V1) |
|
||||
| `agent_framework.openai` | `OpenAIAssistantProvider` | `AsyncOpenAI` | OpenAI Assistants API |
|
||||
|
||||
> **Note:** Azure AI naming is temporary. Final naming will be updated according to Azure AI / Microsoft Foundry renaming decisions.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Azure AI Agent Service V2 (based on Responses API)
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIProjectAgentProvider
|
||||
from azure.ai.projects import AIProjectClient
|
||||
|
||||
client = AIProjectClient(endpoint, credential)
|
||||
provider = AzureAIProjectAgentProvider(client)
|
||||
|
||||
# Create new agent on service
|
||||
agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
|
||||
|
||||
# Get existing agent by name
|
||||
agent = await provider.get_agent(agent_name="MyAgent")
|
||||
|
||||
# Wrap already-fetched SDK object (no HTTP calls)
|
||||
agent_ref = await client.agents.get("MyAgent")
|
||||
agent = provider.as_agent(agent_ref)
|
||||
```
|
||||
|
||||
#### Azure AI Persistent Agents V1
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIAgentsProvider
|
||||
from azure.ai.agents import AgentsClient
|
||||
|
||||
client = AgentsClient(endpoint, credential)
|
||||
provider = AzureAIAgentsProvider(client)
|
||||
|
||||
agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
|
||||
agent = await provider.get_agent(agent_id="persistent-agent-456")
|
||||
agent = provider.as_agent(persistent_agent)
|
||||
```
|
||||
|
||||
#### OpenAI Assistants
|
||||
|
||||
```python
|
||||
from agent_framework.openai import OpenAIAssistantProvider
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
provider = OpenAIAssistantProvider(client)
|
||||
|
||||
agent = await provider.create_agent(name="MyAssistant", model="gpt-4", instructions="...")
|
||||
agent = await provider.get_agent(assistant_id="asst_123")
|
||||
agent = provider.as_agent(assistant)
|
||||
```
|
||||
|
||||
#### Local-Only Agents (No Provider)
|
||||
|
||||
Current method `create_agent` (python) / `CreateAIAgent` (.NET) can be renamed to `as_agent` (python) / `AsAIAgent` (.NET) to emphasize the conversion logic rather than creation/initialization logic and to avoid collision with `create_agent` method for remote calls.
|
||||
|
||||
```python
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
# Convert chat client to ChatAgent (no remote service involved)
|
||||
client = OpenAIChatClient(model="gpt-4")
|
||||
agent = client.as_agent(name="LocalAgent", instructions="...") # instead of create_agent
|
||||
```
|
||||
|
||||
### Adding New Agent Types
|
||||
|
||||
Python:
|
||||
|
||||
1. Create provider class in appropriate package.
|
||||
2. Implement `create_agent`, `get_agent`, `as_agent` as applicable.
|
||||
|
||||
.NET:
|
||||
|
||||
1. Create static class for extension methods.
|
||||
2. Implement `CreateAIAgentAsync`, `GetAIAgentAsync`, `AsAIAgent` as applicable.
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: proposed
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-01-08
|
||||
deciders: eavanvalkenburg, markwallace-microsoft, sphenry, alliscode, johanst, brettcannon
|
||||
consulted: taochenosu, moonbox3, dmytrostruk, giles17
|
||||
---
|
||||
|
||||
# Leveraging TypedDict and Generic Options in Python Chat Clients
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The Agent Framework Python SDK provides multiple chat client implementations for different providers (OpenAI, Anthropic, Azure AI, Bedrock, Ollama, etc.). Each provider has unique configuration options beyond the common parameters defined in `ChatOptions`. Currently, developers using these clients lack type safety and IDE autocompletion for provider-specific options, leading to runtime errors and a poor developer experience.
|
||||
|
||||
How can we provide type-safe, discoverable options for each chat client while maintaining a consistent API across all implementations?
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Type Safety**: Developers should get compile-time/static analysis errors when using invalid options
|
||||
- **IDE Support**: Full autocompletion and inline documentation for all available options
|
||||
- **Extensibility**: Users should be able to define custom options that extend provider-specific options
|
||||
- **Consistency**: All chat clients should follow the same pattern for options handling
|
||||
- **Provider Flexibility**: Each provider can expose its unique options without affecting the common interface
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Option 1: Status Quo - Class `ChatOptions` with `**kwargs`**
|
||||
- **Option 2: TypedDict with Generic Type Parameters**
|
||||
|
||||
### Option 1: Status Quo - Class `ChatOptions` with `**kwargs`
|
||||
|
||||
The current approach uses a base `ChatOptions` Class with common parameters, and provider-specific options are passed via `**kwargs` or loosely typed dictionaries.
|
||||
|
||||
```python
|
||||
# Current usage - no type safety for provider-specific options
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
top_k=40,
|
||||
random=42, # No validation
|
||||
)
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Simple implementation
|
||||
- Maximum flexibility
|
||||
|
||||
**Cons:**
|
||||
- No type checking for provider-specific options
|
||||
- No IDE autocompletion for available options
|
||||
- Runtime errors for typos or invalid options
|
||||
- Documentation must be consulted for each provider
|
||||
|
||||
### Option 2: TypedDict with Generic Type Parameters (Chosen)
|
||||
|
||||
Each chat client is parameterized with a TypeVar bound to a provider-specific `TypedDict` that extends `ChatOptions`. This enables full type safety and IDE support.
|
||||
|
||||
```python
|
||||
# Provider-specific TypedDict
|
||||
class AnthropicChatOptions(ChatOptions, total=False):
|
||||
"""Anthropic-specific chat options."""
|
||||
top_k: int
|
||||
thinking: ThinkingConfig
|
||||
# ... other Anthropic-specific options
|
||||
|
||||
# Generic chat client
|
||||
class AnthropicChatClient(ChatClientBase[TAnthropicChatOptions]):
|
||||
...
|
||||
|
||||
client = AnthropicChatClient(...)
|
||||
|
||||
# Usage with full type safety
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"random": 42, # fails type checking and IDE would flag this
|
||||
}
|
||||
)
|
||||
|
||||
# Users can extend for custom options
|
||||
class MyAnthropicOptions(AnthropicChatOptions, total=False):
|
||||
custom_field: str
|
||||
|
||||
|
||||
client = AnthropicChatClient[MyAnthropicOptions](...)
|
||||
|
||||
# Usage of custom options with full type safety
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"custom_field": "value",
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Full type safety with static analysis
|
||||
- IDE autocompletion for all options
|
||||
- Compile-time error detection
|
||||
- Self-documenting through type hints
|
||||
- Users can extend options for their specific needs or advances in models
|
||||
|
||||
**Cons:**
|
||||
- More complex implementation
|
||||
- Some type: ignore comments needed for TypedDict field overrides
|
||||
- Minor: Requires TypeVar with default (Python 3.13+ or typing_extensions)
|
||||
|
||||
> [NOTE!]
|
||||
> In .NET this is already achieved through overloads on the `GetResponseAsync` method for each provider-specific options class, e.g., `AnthropicChatOptions`, `OpenAIChatOptions`, etc. So this does not apply to .NET.
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **Base Protocol**: `ChatClientProtocol[TOptions]` is generic over options type, with default set to `ChatOptions` (the new TypedDict)
|
||||
2. **Provider TypedDicts**: Each provider defines its options extending `ChatOptions`
|
||||
They can even override fields with type=None to indicate they are not supported.
|
||||
3. **TypeVar Pattern**: `TProviderOptions = TypeVar("TProviderOptions", bound=TypedDict, default=ProviderChatOptions, contravariant=True)`
|
||||
4. **Option Translation**: Common options are kept in place,and explicitly documented in the Options class how they are used. (e.g., `user` → `metadata.user_id`) in `_prepare_options` (for Anthropic) to preserve easy use of common options.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.
|
||||
|
||||
See [typed_options.py](../../python/samples/getting_started/chat_client/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
status: Accepted
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-01-06
|
||||
deciders: markwallace-microsoft, dmytrostruk, taochenosu, alliscode, moonbox3, sphenry
|
||||
consulted: sergeymenshykh, rbarreto, dmytrostruk, westey-m
|
||||
informed:
|
||||
---
|
||||
|
||||
# Simplify Python Get Response API into a single method
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Currently chat clients must implement two separate methods to get responses, one for streaming and one for non-streaming. This adds complexity to the client implementations and increases the maintenance burden. This was likely done because the .NET version cannot do proper typing with a single method, in Python this is possible and this for instance is also how the OpenAI python client works, this would then also make it simpler to work with the Python version because there is only one method to learn about instead of two.
|
||||
|
||||
## Implications of this change
|
||||
|
||||
### Current Architecture Overview
|
||||
|
||||
The current design has **two separate methods** at each layer:
|
||||
|
||||
| Layer | Non-streaming | Streaming |
|
||||
|-------|---------------|-----------|
|
||||
| **Protocol** | `get_response()` → `ChatResponse` | `get_streaming_response()` → `AsyncIterable[ChatResponseUpdate]` |
|
||||
| **BaseChatClient** | `get_response()` (public) | `get_streaming_response()` (public) |
|
||||
| **Implementation** | `_inner_get_response()` (private) | `_inner_get_streaming_response()` (private) |
|
||||
|
||||
### Key Usage Areas Identified
|
||||
|
||||
#### 1. **ChatAgent** (_agents.py)
|
||||
- `run()` → calls `self.chat_client.get_response()`
|
||||
- `run_stream()` → calls `self.chat_client.get_streaming_response()`
|
||||
|
||||
These are parallel methods on the agent, so consolidating the client methods would **not break** the agent API. You could keep `agent.run()` and `agent.run_stream()` unchanged while internally calling `get_response(stream=True/False)`.
|
||||
|
||||
#### 2. **Function Invocation Decorator** (_tools.py)
|
||||
This is **the most impacted area**. Currently:
|
||||
- `_handle_function_calls_response()` decorates `get_response`
|
||||
- `_handle_function_calls_streaming_response()` decorates `get_streaming_response`
|
||||
- The `use_function_invocation` class decorator wraps **both methods separately**
|
||||
|
||||
**Impact**: The decorator logic is almost identical (~200 lines each) with small differences:
|
||||
- Non-streaming collects response, returns it
|
||||
- Streaming yields updates, returns async iterable
|
||||
|
||||
With a unified method, you'd need **one decorator** that:
|
||||
- Checks the `stream` parameter
|
||||
- Uses `@overload` to determine return type
|
||||
- Handles both paths with conditional logic
|
||||
- The new decorator could be applied just on the method, instead of the whole class.
|
||||
|
||||
This would **reduce code duplication** but add complexity to a single function.
|
||||
|
||||
#### 3. **Observability/Instrumentation** (observability.py)
|
||||
Same pattern as function invocation:
|
||||
- `_trace_get_response()` wraps `get_response`
|
||||
- `_trace_get_streaming_response()` wraps `get_streaming_response`
|
||||
- `use_instrumentation` decorator applies both
|
||||
|
||||
**Impact**: Would need consolidation into a single tracing wrapper.
|
||||
|
||||
#### 4. **Chat Middleware** (_middleware.py)
|
||||
The `use_chat_middleware` decorator also wraps both methods separately with similar logic.
|
||||
|
||||
#### 5. **AG-UI Client** (_client.py)
|
||||
Wraps both methods to unwrap server function calls:
|
||||
```python
|
||||
original_get_streaming_response = chat_client.get_streaming_response
|
||||
original_get_response = chat_client.get_response
|
||||
```
|
||||
|
||||
#### 6. **Provider Implementations** (all subpackages)
|
||||
All subclasses implement both `_inner_*` methods, except:
|
||||
- OpenAI Assistants Client (and similar clients, such as Foundry Agents V1) - it implements `_inner_get_response` by calling `_inner_get_streaming_response`
|
||||
|
||||
### Implications of Consolidation
|
||||
|
||||
| Aspect | Impact |
|
||||
|--------|--------|
|
||||
| **Type Safety** | Overloads work well: `@overload` with `Literal[True]` → `AsyncIterable`, `Literal[False]` → `ChatResponse`. Runtime return type based on `stream` param. |
|
||||
| **Breaking Change** | **Major breaking change** for anyone implementing custom chat clients. They'd need to update from 2 methods to 1 (or 2 inner methods to 1). |
|
||||
| **Decorator Complexity** | All 3 decorator systems (function invocation, middleware, observability) would need refactoring to handle both paths in one wrapper. |
|
||||
| **Code Reduction** | Significant reduction in _tools.py (~200 lines of near-duplicate code) and other decorators. |
|
||||
| **Samples/Tests** | Many samples call `get_streaming_response()` directly - would need updates. |
|
||||
| **Protocol Simplification** | `ChatClientProtocol` goes from 2 methods + 1 property to 1 method + 1 property. |
|
||||
|
||||
### Recommendation
|
||||
|
||||
The consolidation makes sense architecturally, but consider:
|
||||
|
||||
1. **The overload pattern with `stream: bool`** works well in Python typing:
|
||||
```python
|
||||
@overload
|
||||
async def get_response(self, messages, *, stream: Literal[True] = True, ...) -> AsyncIterable[ChatResponseUpdate]: ...
|
||||
@overload
|
||||
async def get_response(self, messages, *, stream: Literal[False] = False, ...) -> ChatResponse: ...
|
||||
```
|
||||
|
||||
2. **The decorator complexity** is the biggest concern. The current approach of separate decorators for separate methods is cleaner than conditional logic inside one wrapper.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Reduce code needed to implement a Chat Client, simplify the public API for chat clients
|
||||
- Reduce code duplication in decorators and middleware
|
||||
- Maintain type safety and clarity in method signatures
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. Status quo: Keep separate methods for streaming and non-streaming
|
||||
2. Consolidate into a single `get_response` method with a `stream` parameter
|
||||
3. Option 2 plus merging `agent.run` and `agent.run_stream` into a single method with a `stream` parameter as well
|
||||
|
||||
## Option 1: Status Quo
|
||||
- Good: Clear separation of streaming vs non-streaming logic
|
||||
- Good: Aligned with .NET design, although it is already `run` for Python and `RunAsync` for .NET
|
||||
- Bad: Code duplication in decorators and middleware
|
||||
- Bad: More complex client implementations
|
||||
|
||||
## Option 2: Consolidate into Single Method
|
||||
- Good: Simplified public API for chat clients
|
||||
- Good: Reduced code duplication in decorators
|
||||
- Good: Smaller API footprint for users to get familiar with
|
||||
- Good: People using OpenAI directly already expect this pattern
|
||||
- Bad: Increased complexity in decorators and middleware
|
||||
- Bad: Less alignment with .NET design (`get_response(stream=True)` vs `GetStreamingResponseAsync`)
|
||||
|
||||
## Option 3: Consolidate + Merge Agent and Workflow Methods
|
||||
- Good: Further simplifies agent and workflow implementation
|
||||
- Good: Single method for all chat interactions
|
||||
- Good: Smaller API footprint for users to get familiar with
|
||||
- Good: People using OpenAI directly already expect this pattern
|
||||
- Good: Workflows internally already use a single method (_run_workflow_with_tracing), so would eliminate public API duplication as well, with hardly any code changes
|
||||
- Bad: More breaking changes for agent users
|
||||
- Bad: Increased complexity in agent implementation
|
||||
- Bad: More extensive misalignment with .NET design (`run(stream=True)` vs `RunStreamingAsync` in addition to `get_response` change)
|
||||
|
||||
## Misc
|
||||
|
||||
Smaller questions to consider:
|
||||
- Should default be `stream=False` or `stream=True`? (Current is False)
|
||||
- Default to `False` makes it simpler for new users, as non-streaming is easier to handle.
|
||||
- Default to `False` aligns with existing behavior.
|
||||
- Streaming tends to be faster, so defaulting to `True` could improve performance for common use cases.
|
||||
- Should this differ between ChatClient, Agent and Workflows? (e.g., Agent and Workflow defaults to streaming, ChatClient to non-streaming)
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen Option: **Option 3: Consolidate + Merge Agent and Workflow Methods**
|
||||
|
||||
Since this is the most pythonic option and it reduces the API surface and code duplication the most, we will go with this option.
|
||||
We will keep the default of `stream=False` for all methods to maintain backward compatibility and simplicity for new users.
|
||||
|
||||
# Appendix
|
||||
## Code Samples for Consolidated Method
|
||||
|
||||
### Python - Option 3: Direct ChatClient + Agent with Single Method
|
||||
|
||||
```python
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Example 1: Direct ChatClient usage with single method
|
||||
client = OpenAIChatClient()
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
|
||||
# Non-streaming usage
|
||||
print(f"User: {message}")
|
||||
response = await client.get_response(message, tools=get_weather)
|
||||
print(f"Assistant: {response.text}")
|
||||
|
||||
# Streaming usage - same method, different parameter
|
||||
print(f"\nUser: {message}")
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(message, tools=get_weather, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
|
||||
# Example 2: Agent usage with single method
|
||||
agent = ChatAgent(
|
||||
chat_client=client,
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# Non-streaming agent
|
||||
print(f"\nUser: {message}")
|
||||
result = await agent.run(message, thread=thread) # default would be stream=False
|
||||
print(f"{agent.name}: {result.text}")
|
||||
|
||||
# Streaming agent - same method, different parameter
|
||||
print(f"\nUser: {message}")
|
||||
print(f"{agent.name}: ", end="")
|
||||
async for update in agent.run(message, thread=thread, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### .NET - Current pattern for comparison
|
||||
|
||||
```csharp
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You are good at telling jokes about pirates.",
|
||||
name: "PirateJoker");
|
||||
|
||||
// Non-streaming: Returns a string directly
|
||||
Console.WriteLine("=== Non-streaming ===");
|
||||
string result = await agent.RunAsync("Tell me a joke about a pirate.");
|
||||
Console.WriteLine(result);
|
||||
|
||||
// Streaming: Returns IAsyncEnumerable<AgentUpdate>
|
||||
Console.WriteLine("\n=== Streaming ===");
|
||||
await foreach (AgentUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
Console.WriteLine();
|
||||
|
||||
```
|
||||
@@ -0,0 +1,423 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2025-01-21
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, westey-m, stephentoub
|
||||
consulted: reubenbond
|
||||
informed:
|
||||
---
|
||||
|
||||
# Feature Collections
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When using agents, we often have cases where we want to pass some arbitrary services or data to an agent or some component in the agent execution stack.
|
||||
These services or data are not necessarily known at compile time and can vary by the agent stack that the user has built.
|
||||
E.g., there may be an agent decorator or chat client decorator that was added to the stack by the user, and an arbitrary payload needs to be passed to that decorator.
|
||||
|
||||
Since these payloads are related to components that are not integral parts of the agent framework, they cannot be added as strongly typed settings to the agent run options.
|
||||
However, the payloads could be added to the agent run options as loosely typed 'features', that can be retrieved as needed.
|
||||
|
||||
In some cases certain classes of agents may support the same capability, but not all agents do.
|
||||
Having the configuration for such a capability on the main abstraction would advertise the functionality to all users, even if their chosen agent does not support it.
|
||||
The user may type test for certain agent types, and call overloads on the appropriate agent types, with the strongly typed configuration.
|
||||
Having a feature collection though, would be an alternative way of passing such configuration, without needing to type check the agent type.
|
||||
All agents that support the functionality would be able to check for the configuration and use it, simplifying the user code.
|
||||
If the agent does not support the capability, that configuration would be ignored.
|
||||
|
||||
### Sample Scenario 1 - Per Run ChatMessageStore Override for hosting Libraries
|
||||
|
||||
We are building an agent hosting library, that can host any agent built using the agent framework.
|
||||
Where an agent is not built on a service that uses in-service chat history storage, the hosting library wants to force the agent to use
|
||||
the hosting library's chat history storage implementation.
|
||||
This chat history storage implementation may be specifically tailored to the type of protocol that the hosting library uses, e.g. conversation id based storage or response id based storage.
|
||||
The hosting library does not know what type of agent it is hosting, so it cannot provide a strongly typed parameter on the agent.
|
||||
Instead, it adds the chat history storage implementation to a feature collection, and if the agent supports custom chat history storage, it retrieves the implementation from the feature collection and uses it.
|
||||
|
||||
```csharp
|
||||
// Pseudo-code for an agent hosting library that supports conversation id based hosting.
|
||||
public async Task<string> HandleConversationsBasedRequestAsync(AIAgent agent, string conversationId, string userInput)
|
||||
{
|
||||
var thread = await this._threadStore.GetOrCreateThread(conversationId);
|
||||
|
||||
// The hosting library can set a per-run chat message store via Features that only applies for that run.
|
||||
// This message store will load and save messages under the conversation id provided.
|
||||
ConversationsChatMessageStore messageStore = new(this._dbClient, conversationId);
|
||||
var response = await agent.RunAsync(
|
||||
userInput,
|
||||
thread,
|
||||
options: new AgentRunOptions()
|
||||
{
|
||||
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
|
||||
});
|
||||
|
||||
await this._threadStore.SaveThreadAsync(conversationId, thread);
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
// Pseudo-code for an agent hosting library that supports response id based hosting.
|
||||
public async Task<(string responseMessage, string responseId)> HandleResponseIdBasedRequestAsync(AIAgent agent, string previousResponseId, string userInput)
|
||||
{
|
||||
var thread = await this._threadStore.GetOrCreateThreadAsync(previousResponseId);
|
||||
|
||||
// The hosting library can set a per-run chat message store via Features that only applies for that run.
|
||||
// This message store will buffer newly added messages until explicitly saved after the run.
|
||||
ResponsesChatMessageStore messageStore = new(this._dbClient, previousResponseId);
|
||||
|
||||
var response = await agent.RunAsync(
|
||||
userInput,
|
||||
thread,
|
||||
options: new AgentRunOptions()
|
||||
{
|
||||
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
|
||||
});
|
||||
|
||||
// Since the message store may not actually have been used at all (if the agent's underlying chat client requires service-based chat history storage),
|
||||
// we may not have anything to save back to the database.
|
||||
// We still want to generate a new response id though, so that we can save the updated thread state under that id.
|
||||
// We should also use the same id to save any buffered messages in the message store if there are any.
|
||||
var newResponseId = this.GenerateResponseId();
|
||||
if (messageStore.HasBufferedMessages)
|
||||
{
|
||||
await messageStore.SaveBufferedMessagesAsync(newResponseId);
|
||||
}
|
||||
|
||||
// Save the updated thread state under the new response id that was generated by the store.
|
||||
await this._threadStore.SaveThreadAsync(newResponseId, thread);
|
||||
return (response.Text, newResponseId);
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Scenario 2 - Structured output
|
||||
|
||||
Currently our base abstraction does not support structured output, since the capability is not supported by all agents.
|
||||
For those agents that don't support structured output, we could add an agent decorator that takes the response from the underlying agent, and applies structured output parsing on top of it via an additional LLM call.
|
||||
|
||||
If we add structured output configuration as a feature, then any agent that supports structured output could retrieve the configuration from the feature collection and apply it, and where it is not supported, the configuration would simply be ignored.
|
||||
|
||||
We could add a simple StructuredOutputAgentFeature that can be added to the list of features and also be used to return the generated structured output.
|
||||
|
||||
```csharp
|
||||
internal class StructuredOutputAgentFeature
|
||||
{
|
||||
public Type? OutputType { get; set; }
|
||||
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
|
||||
public bool? UseJsonSchemaResponseFormat { get; set; }
|
||||
|
||||
// Contains the result of the structured output parsing request.
|
||||
public ChatResponse? ChatResponse { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
We can add a simple decorator class that does the chat client invocation.
|
||||
|
||||
```csharp
|
||||
public class StructuredOutputAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._chatClient = Throw.IfNull(chatClient);
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Run the inner agent first, to get back the text response we want to convert.
|
||||
var response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (options?.Features?.TryGet<StructuredOutputAgentFeature>(out var responseFormatFeature) is true
|
||||
&& responseFormatFeature.OutputType is not null)
|
||||
{
|
||||
// Create the chat options to request structured output.
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema(responseFormatFeature.OutputType, responseFormatFeature.SerializerOptions)
|
||||
};
|
||||
|
||||
// Invoke the chat client to transform the text output into structured data.
|
||||
// The feature is updated with the result.
|
||||
// The code can be simplified by adding a non-generic structured output GetResponseAsync
|
||||
// overload that takes Type as input.
|
||||
responseFormatFeature.ChatResponse = await this._chatClient.GetResponseAsync(
|
||||
messages: new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.System, "You are a json expert and when provided with any text, will convert it to the requested json format."),
|
||||
new ChatMessage(ChatRole.User, response.Text)
|
||||
},
|
||||
options: chatOptions,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we can add an extension method on `AIAgent` that can add the feature to the run options and check the feature for the structured output result and add the deserialized result to the response.
|
||||
|
||||
```csharp
|
||||
public static async Task<AgentRunResponse<T>> RunAsync<T>(
|
||||
this AIAgent agent,
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
bool? useJsonSchemaResponseFormat = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create the structured output feature.
|
||||
var structuredOutputFeature = new StructuredOutputAgentFeature();
|
||||
structuredOutputFeature.OutputType = typeof(T);
|
||||
structuredOutputFeature.UseJsonSchemaResponseFormat = useJsonSchemaResponseFormat;
|
||||
|
||||
// Run the agent.
|
||||
options ??= new AgentRunOptions();
|
||||
options.Features ??= new AgentFeatureCollection();
|
||||
options.Features.Set(structuredOutputFeature);
|
||||
|
||||
var response = await agent.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Deserialize the JSON output.
|
||||
if (structuredOutputFeature.ChatResponse is not null)
|
||||
{
|
||||
var typed = new ChatResponse<T>(structuredOutputFeature.ChatResponse, serializerOptions ?? AgentJsonUtilities.DefaultOptions);
|
||||
return new AgentRunResponse<T>(response, typed.Result);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No structured output response was generated by the agent.");
|
||||
}
|
||||
```
|
||||
|
||||
We can then use the extension method with any agent that supports structured output or that has
|
||||
been decorated with the `StructuredOutputAgent` decorator.
|
||||
|
||||
```csharp
|
||||
agent = new StructuredOutputAgent(agent, chatClient);
|
||||
|
||||
AgentRunResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>([new ChatMessage(
|
||||
ChatRole.User,
|
||||
"Please provide information about John Smith, who is a 35-year-old software engineer.")]);
|
||||
```
|
||||
|
||||
## Implementation Options
|
||||
|
||||
Three options were considered for implementing feature collections:
|
||||
|
||||
- **Option 1**: FeatureCollections similar to ASP.NET Core
|
||||
- **Option 2**: AdditionalProperties Dictionary
|
||||
- **Option 3**: IServiceProvider
|
||||
|
||||
Here are some comparisons about their suitability for our use case:
|
||||
|
||||
| Criteria | Feature Collection | Additional Properties | IServiceProvider |
|
||||
|------------------|--------------------|-----------------------|------------------|
|
||||
|Ease of use |✅ Good |❌ Bad |✅ Good |
|
||||
|User familiarity |❌ Bad |✅ Good |✅ Good |
|
||||
|Type safety |✅ Good |❌ Bad |✅ Good |
|
||||
|Ability to modify registered options when progressing down the stack|✅ Supported|✅ Supported|❌ Not-Supported (IServiceProvider is read-only)|
|
||||
|Already available in MEAI stack|❌ No|✅ Yes|❌ No|
|
||||
|Ambiguity with existing AdditionalProperties|❌ Yes|✅ No|❌ Yes|
|
||||
|
||||
## IServiceProvider
|
||||
|
||||
Service Collections and Service Providers provide a very popular way to register and retrieve services by type and could be used as a way to pass features to agents and chat clients.
|
||||
|
||||
However, since IServiceProvider is read-only, it is not possible to modify the registered services when progressing down the execution stack.
|
||||
E.g. an agent decorator cannot add additional services to the IServiceProvider passed to it when calling into the inner agent.
|
||||
|
||||
IServiceProvider also does not expose a way to list all services contained in it, making it difficult to copy services from one provider to another.
|
||||
|
||||
This lack of mutability makes IServiceProvider unsuitable for our use case, since we will not be able to use it to build sample scenario 2.
|
||||
|
||||
## AdditionalProperties dictionary
|
||||
|
||||
The AdditionalProperties dictionary is already available on various options classes in the agent framework as well as in the MEAI stack and
|
||||
allows storing arbitrary key/value pairs, where the key is a string and the value is an object.
|
||||
|
||||
While FeatureCollection uses Type as a key, AdditionalProperties uses string keys.
|
||||
This means that users need to agree on string keys to use for specific features, however it is also possible to use Type.FullName as a key by convention
|
||||
to avoid key collisions, which is an easy convention to follow.
|
||||
|
||||
Since the value of AdditionalProperties is of type object, users need to cast the value to the expected type when retrieving it, which is also
|
||||
a drawback, but when using the convention of using Type.FullName as a key, there is at least a clear expectation of what type to cast to.
|
||||
|
||||
```csharp
|
||||
// Setting a feature
|
||||
options.AdditionalProperties[typeof(MyFeature).FullName] = new MyFeature();
|
||||
|
||||
// Retrieving a feature
|
||||
if (options.AdditionalProperties.TryGetValue(typeof(MyFeature).FullName, out var featureObj)
|
||||
&& featureObj is MyFeature myFeature)
|
||||
{
|
||||
// Use myFeature
|
||||
}
|
||||
```
|
||||
|
||||
It would also be possible to add extension methods to simplify setting and getting features from AdditionalProperties.
|
||||
Having a base class for features should help make this more feature rich.
|
||||
|
||||
```csharp
|
||||
// Setting a feature, this can use Type.FullName as the key.
|
||||
options.AdditionalProperties
|
||||
.WithFeature(new MyFeature());
|
||||
|
||||
// Retrieving a feature, this can use Type.FullName as the key.
|
||||
if (options.AdditionalProperties.TryGetFeature<MyFeature>(out var myFeature))
|
||||
{
|
||||
// Use myFeature
|
||||
}
|
||||
```
|
||||
|
||||
It would also be possible to add extension methods for a feature to simplify setting and getting features from AdditionalProperties.
|
||||
|
||||
```csharp
|
||||
// Setting a feature
|
||||
options.AdditionalProperties
|
||||
.WithMyFeature(new MyFeature());
|
||||
// Retrieving a feature
|
||||
if (options.AdditionalProperties.TryGetMyFeature(out var myFeature))
|
||||
{
|
||||
// Use myFeature
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Collection
|
||||
|
||||
If we choose the feature collection option, we need to decide on the design of the feature collection itself.
|
||||
|
||||
### Feature Collections extension points
|
||||
|
||||
We need to decide the set of actions that feature collections would be supported for. Here is the suggested list of actions:
|
||||
|
||||
**MAAI.AIAgent:**
|
||||
|
||||
1. GetNewThread
|
||||
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
|
||||
1. DeserializeThread
|
||||
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
|
||||
1. Run / RunStreaming
|
||||
1. E.g. this would allow passing an override chat message store just for that run, or a desired schema for a structured output middleware component.
|
||||
|
||||
**MEAI.ChatClient:**
|
||||
|
||||
1. GetResponse / GetStreamingResponse
|
||||
|
||||
### Reconciling with existing AdditionalProperties
|
||||
|
||||
If we decide to add feature collections, separately from the existing AdditionalProperties dictionaries, we need to consider how to explain to users when to use each one.
|
||||
One possible approach though is to have the one use the other under the hood.
|
||||
AdditionalProperties could be stored as a feature in the feature collection.
|
||||
|
||||
Users would be able to retrieve additional properties from the feature collection, in addition to retrieving it via a dedicated AdditionalProperties property.
|
||||
E.g. `features.Get<AdditionalPropertiesDictionary>()`
|
||||
|
||||
One challenge with this approach is that when setting a value in the AdditionalProperties dictionary, the feature collection would need to be created first if it does not already exist.
|
||||
|
||||
```csharp
|
||||
public class AgentRunOptions
|
||||
{
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
public IAgentFeatureCollection? Features { get; set; }
|
||||
}
|
||||
|
||||
var options = new AgentRunOptions();
|
||||
// This would need to create the feature collection first, if it does not already exist.
|
||||
options.AdditionalProperties ??= new AdditionalPropertiesDictionary();
|
||||
```
|
||||
|
||||
Since IAgentFeatureCollection is an interface, AgentRunOptions would need to have a concrete implementation of the interface to create, meaning that the user cannot decide.
|
||||
It also means that if the user doesn't realise that AdditionalProperties is implemented using feature collections, they may set a value on AdditionalProperties, and then later overwrite the entire feature collection, losing the AdditionalProperties feature.
|
||||
|
||||
Options to avoid these issues:
|
||||
|
||||
1. Make `Features` readonly.
|
||||
1. This would prevent the user from overwriting the feature collection after setting AdditionalProperties.
|
||||
1. Since the user cannot set their own implementation of IAgentFeatureCollection, having an interface for it may not be necessary.
|
||||
|
||||
### Feature Collection Implementation
|
||||
|
||||
We have two options for implementing feature collections:
|
||||
|
||||
1. Create our own [IAgentFeatureCollection interface](https://github.com/microsoft/agent-framework/pull/2354/files#diff-9c42f3e60d70a791af9841d9214e038c6de3eebfc10e3997cb4cdffeb2f1246d) and [implementation](https://github.com/microsoft/agent-framework/pull/2354/files#diff-a435cc738baec500b8799f7f58c1538e3bb06c772a208afc2615ff90ada3f4ca).
|
||||
2. Reuse the asp.net [IFeatureCollection interface](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/IFeatureCollection.cs) and [implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs).
|
||||
|
||||
#### Roll our own
|
||||
|
||||
Advantages:
|
||||
|
||||
Creating our own IAgentFeatureCollection interface and implementation has the advantage of being more clearly associated with the agent framework and allows us to
|
||||
improve on some of the design decisions made in asp.net core's IFeatureCollection.
|
||||
|
||||
Drawbacks:
|
||||
|
||||
It would mean a different implementation to maintain and test.
|
||||
|
||||
#### Reuse asp.net IFeatureCollection
|
||||
|
||||
Advantages:
|
||||
|
||||
Reusing the asp.net IFeatureCollection has the advantage of being able to reuse the well-established and tested implementation from asp.net
|
||||
core. Users who are using agents in an asp.net core application may be able to pass feature collections from asp.net core to the agent framework directly.
|
||||
|
||||
Drawbacks:
|
||||
|
||||
While the package name is `Microsoft.Extensions.Features`, the namespaces of the types are `Microsoft.AspNetCore.Http.Features`, which may create confusion for users of agent framework who are not building web applications or services.
|
||||
Users may rightly ask: Why do I need to use a class from asp.net core when I'm not building a web application / service?
|
||||
|
||||
The current design has some design issues that would be good to avoid. E.g. it does not distinguish between a feature being "not set" and "null". Get returns both as null and there is no tryget method.
|
||||
Since the [default implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs) also supports value types, it throws for null values of value types.
|
||||
A TryGet method would be more appropriate.
|
||||
|
||||
## Feature Layering
|
||||
|
||||
One possible scenario when adding support for feature collections is to allow layering of features by scope.
|
||||
|
||||
The following levels of scope could be supported:
|
||||
|
||||
1. Application - Application wide features that apply to all agents / chat clients
|
||||
2. Artifact (Agent / ChatClient) - Features that apply to all runs of a specific agent or chat client instance
|
||||
3. Action (GetNewThread / Run / GetResponse) - Feature that apply to a single action only
|
||||
|
||||
When retrieving a feature from the collection, the search would start from the most specific scope (Action) and progress to the least specific scope (Application), returning the first matching feature found.
|
||||
|
||||
Introducing layering adds some challenges:
|
||||
|
||||
- There may be multiple feature collections at the same scope level, e.g. an Agent that uses a ChatClient where both have their own feature collections.
|
||||
- Do we layer the agent feature collection over the chat client feature collection (Application -> ChatClient -> Agent -> Run), or only use the agent feature collection in the agent (Application -> Agent -> Run), and the chat client feature collection in the chat client (Application -> ChatClient -> Run)?
|
||||
- The appropriate base feature collection may change when progressing down the stack, e.g. when an Agent calls a ChatClient, the action feature collection stays the same, but the artifact feature collection changes.
|
||||
- Who creates the feature collection hierarchy?
|
||||
- Since the hierarchy changes as it progresses down the execution stack, and the caller can only pass in the action level feature collection, the callee needs to combine it with its own artifact level feature collection and the application level feature collection. Each action will need to build the appropriate feature collection hierarchy, at the start of its execution.
|
||||
- For Artifact level features, it seems odd to pass them in as a bag of untyped features, when we are constructing a known artifact type and therefore can have typed settings.
|
||||
- E.g. today we have a strongly typed setting on ChatClientAgentOptions to configure a ChatMessageStore for the agent.
|
||||
- To avoid global statics for application level features, the user would need to pass in the application level feature collection to each artifact that they create.
|
||||
- This would be very odd if the user also already has to strongly typed settings for each feature that they want to set at the artifact level.
|
||||
|
||||
### Layering Options
|
||||
|
||||
1. No layering - only a single feature collection is supported per action (the caller can still create a layered collection if desired, but the callee does not do any layering automatically).
|
||||
1. Fallback is to any features configured on the artifact via strongly typed settings.
|
||||
1. Full layering - support layering at all levels (Application -> Artifact -> Action).
|
||||
1. Only apply applicable artifact level features when calling into that artifact.
|
||||
1. Apply upstream artifact features when calling into downstream artifacts, e.g. Feature hierarchy in ChatClientAgent would be `Application -> Agent -> Run` and in ChatClient would be `Application -> ChatClient -> Agent -> Run` or `Application -> Agent -> ChatClient -> Run`
|
||||
1. The user needs to provide the application level feature collection to each artifact that they create and artifact features are passed via strongly typed settings.
|
||||
|
||||
### Accessing application level features Options
|
||||
|
||||
We need to consider how application level features would be accessed if supported.
|
||||
|
||||
1. The user provides the application level feature collection to each artifact that the user constructs
|
||||
1. Passing the application level feature collection to each artifact is tedious for the user.
|
||||
1. There is a static application level feature collection that can be accessed globally.
|
||||
1. Statics create issues with testing and isolation.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Feature Collections Container: Use AdditionalProperties
|
||||
- Feature Layering: No layering - only a single collection/dictionary is supported per action. Application layers can be added later if needed.
|
||||
@@ -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.
|
||||
@@ -125,7 +125,7 @@ The proposed solution is to add helper methods which allow developers to either
|
||||
- [Foundry SDK] Create a `PersistentAgentsClient`
|
||||
- [Foundry SDK] Create a `PersistentAgent` using the `PersistentAgentsClient`
|
||||
- [Foundry SDK] Retrieve an `AIAgent` using the `PersistentAgentsClient`
|
||||
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentRunResponse`
|
||||
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
|
||||
- [Foundry SDK] Clean up the agent
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
|
||||
|
||||
- [Foundry SDK] Create a `PersistentAgentsClient`
|
||||
- [Foundry SDK] Create a `AIAgent` using the `PersistentAgentsClient`
|
||||
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentRunResponse`
|
||||
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
|
||||
- [Foundry SDK] Clean up the agent
|
||||
|
||||
```csharp
|
||||
@@ -184,7 +184,7 @@ await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
|
||||
- [Foundry SDK] Create a `PersistentAgentsClient`
|
||||
- [Foundry SDK] Create a `AIAgent` using the `PersistentAgentsClient`
|
||||
- [Agent Framework SDK] Optionally create an `AgentThread` for the agent run
|
||||
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentRunResponse`
|
||||
- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
|
||||
- [Foundry SDK] Clean up the agent and the agent thread
|
||||
|
||||
```csharp
|
||||
@@ -227,7 +227,7 @@ await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
|
||||
- [Foundry SDK] Create a `PersistentAgentsClient`
|
||||
- [Foundry SDK] Create multiple `AIAgent` instances using the `PersistentAgentsClient`
|
||||
- [Agent Framework SDK] Create a `SequentialOrchestration` and add all of the agents to it
|
||||
- [Agent Framework SDK] Invoke the `SequentialOrchestration` instance and access response from the `AgentRunResponse`
|
||||
- [Agent Framework SDK] Invoke the `SequentialOrchestration` instance and access response from the `AgentResponse`
|
||||
- [Foundry SDK] Clean up the agents
|
||||
|
||||
```csharp
|
||||
@@ -281,7 +281,7 @@ SequentialOrchestration orchestration =
|
||||
// Run the orchestration
|
||||
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
|
||||
Console.WriteLine($"\n# INPUT: {input}\n");
|
||||
AgentRunResponse result = await orchestration.RunAsync(input);
|
||||
AgentResponse result = await orchestration.RunAsync(input);
|
||||
Console.WriteLine($"\n# RESULT: {result}");
|
||||
|
||||
// Cleanup
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,40 +11,40 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="11.0.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.0.1" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||
<!-- 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" />
|
||||
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
|
||||
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
||||
<!-- Microsoft.Azure.* -->
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
|
||||
<!-- 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.2" />
|
||||
<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.2" />
|
||||
<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.2" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.2" />
|
||||
<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.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.2.0-preview.1.26063.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.2" />
|
||||
<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.2" />
|
||||
<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,11 +100,10 @@
|
||||
<!-- MCP -->
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.7" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
|
||||
<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 -->
|
||||
@@ -114,19 +112,21 @@
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.16.2" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.16.2-preview.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.16.2" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.16.2-preview.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
|
||||
<!-- Azure Functions -->
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.9.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Mcp" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
|
||||
<!-- Redis -->
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.22" />
|
||||
@@ -143,6 +143,7 @@
|
||||
<!-- Symbols -->
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<!-- Toolset -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
|
||||
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
@@ -20,14 +20,9 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AGUIClientServer/">
|
||||
<Project Path="samples/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AGUIDojo/">
|
||||
<File Path="samples/AGUIDojo/Design.md" />
|
||||
<File Path="samples/AGUIDojo/Plan.md" />
|
||||
<Project Path="samples/AGUIDojo/AGUIDojoClient/AGUIDojoClient.csproj" />
|
||||
<Project Path="samples/AGUIDojo/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AzureFunctions/">
|
||||
<File Path="samples/AzureFunctions/.editorconfig" />
|
||||
<File Path="samples/AzureFunctions/README.md" />
|
||||
@@ -38,6 +33,19 @@
|
||||
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/DurableAgents/">
|
||||
<File Path="samples/DurableAgents/ConsoleApps/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/DurableAgents/ConsoleApps/">
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
@@ -85,6 +93,7 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Agent_Step20_AdditionalAIContext.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
|
||||
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||
@@ -134,6 +143,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/">
|
||||
@@ -289,6 +299,11 @@
|
||||
<File Path="../docs/decisions/0007-agent-filtering-middleware.md" />
|
||||
<File Path="../docs/decisions/0008-python-subpackages.md" />
|
||||
<File Path="../docs/decisions/0009-support-long-running-operations.md" />
|
||||
<File Path="../docs/decisions/0010-ag-ui-support.md" />
|
||||
<File Path="../docs/decisions/0011-create-get-agent-api.md" />
|
||||
<File Path="../docs/decisions/0012-python-typeddict-options.md" />
|
||||
<File Path="../docs/decisions/0013-python-get-response-simplification.md" />
|
||||
<File Path="../docs/decisions/0014-feature-collections.md" />
|
||||
<File Path="../docs/decisions/adr-short-template.md" />
|
||||
<File Path="../docs/decisions/adr-template.md" />
|
||||
<File Path="../docs/decisions/README.md" />
|
||||
@@ -399,6 +414,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Tests/" />
|
||||
@@ -438,6 +454,7 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251204.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251204.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251204.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260121.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260121.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260121.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -29,7 +29,7 @@ internal sealed class HostClientAgent
|
||||
// Create the agent that uses the remote agents as tools
|
||||
this.Agent = new OpenAIClient(new ApiKeyCredential(apiKey))
|
||||
.GetChatClient(modelId)
|
||||
.CreateAIAgent(instructions: "You specialize in handling queries for users and using your tools to provide answers.", name: "HostClient", tools: tools);
|
||||
.AsAIAgent(instructions: "You specialize in handling queries for users and using your tools to provide answers.", name: "HostClient", tools: tools);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ public static class Program
|
||||
// Create the Host agent
|
||||
var hostAgent = new HostClientAgent(loggerFactory);
|
||||
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
|
||||
AgentThread thread = hostAgent.Agent!.GetNewThread();
|
||||
AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
|
||||
@@ -35,7 +35,7 @@ internal static class HostAgentFactory
|
||||
{
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
.CreateAIAgent(instructions, name, tools: tools);
|
||||
.AsAIAgent(instructions, name, tools: tools);
|
||||
|
||||
AgentCard agentCard = agentType.ToUpperInvariant() switch
|
||||
{
|
||||
|
||||
@@ -83,12 +83,12 @@ public static class Program
|
||||
serverUrl,
|
||||
jsonSerializerOptions: AGUIClientSerializerContext.Default.Options);
|
||||
|
||||
AIAgent agent = chatClient.CreateAIAgent(
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent",
|
||||
tools: [changeBackground, readClientClimateSensors]);
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
|
||||
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
|
||||
try
|
||||
{
|
||||
@@ -114,7 +114,7 @@ public static class Program
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
|
||||
{
|
||||
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks></TargetFrameworks>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
|
||||
+4
-4
@@ -19,12 +19,12 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -69,7 +69,7 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
|
||||
|
||||
yield return update;
|
||||
|
||||
yield return new AgentRunResponseUpdate(
|
||||
yield return new AgentResponseUpdate(
|
||||
new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
|
||||
{
|
||||
MessageId = "delta_" + Guid.NewGuid().ToString("N"),
|
||||
+9
-9
@@ -33,7 +33,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
name: "AgenticChat",
|
||||
description: "A simple chat agent using Azure OpenAI");
|
||||
}
|
||||
@@ -42,7 +42,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
name: "BackendToolRenderer",
|
||||
description: "An agent that can render backend tools using Azure OpenAI",
|
||||
tools: [AIFunctionFactory.Create(
|
||||
@@ -56,7 +56,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
name: "HumanInTheLoopAgent",
|
||||
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
|
||||
}
|
||||
@@ -65,7 +65,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
name: "ToolBasedGenerativeUIAgent",
|
||||
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
|
||||
}
|
||||
@@ -73,7 +73,7 @@ internal static class ChatClientAgentFactory
|
||||
public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "AgenticUIAgent",
|
||||
Description = "An agent that generates agentic user interfaces using Azure OpenAI",
|
||||
@@ -116,7 +116,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(
|
||||
name: "SharedStateAgent",
|
||||
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
|
||||
|
||||
@@ -127,7 +127,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "PredictiveStateUpdatesAgent",
|
||||
Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
|
||||
@@ -135,7 +135,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
Instructions = """
|
||||
You are a document editor assistant. When asked to write or edit content:
|
||||
|
||||
|
||||
IMPORTANT:
|
||||
- Use the `write_document` tool with the full document text in Markdown format
|
||||
- Format the document extensively so it's easy to read
|
||||
@@ -145,7 +145,7 @@ internal static class ChatClientAgentFactory
|
||||
- When making edits to the document, try to make them minimal - do not change every word
|
||||
- Keep stories SHORT!
|
||||
- After you are done writing the document you MUST call a confirm_changes tool after you call write_document
|
||||
|
||||
|
||||
After the user confirms the changes, provide a brief summary of what you wrote.
|
||||
""",
|
||||
Tools = [
|
||||
+4
-4
@@ -20,12 +20,12 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -79,7 +79,7 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
|
||||
stateUpdate,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(DocumentState)));
|
||||
|
||||
yield return new AgentRunResponseUpdate(
|
||||
yield return new AgentResponseUpdate(
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, [new DataContent(stateBytes, "application/json")])
|
||||
{
|
||||
MessageId = "snapshot" + Guid.NewGuid().ToString("N"),
|
||||
+6
-6
@@ -19,12 +19,12 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -63,7 +63,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
|
||||
var firstRunMessages = messages.Append(stateUpdateMessage);
|
||||
|
||||
var allUpdates = new List<AgentRunResponseUpdate>();
|
||||
var allUpdates = new List<AgentResponseUpdate>();
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
allUpdates.Add(update);
|
||||
@@ -76,14 +76,14 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
}
|
||||
}
|
||||
|
||||
var response = allUpdates.ToAgentRunResponse();
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
yield return new AgentRunResponseUpdate
|
||||
yield return new AgentResponseUpdate
|
||||
{
|
||||
Contents = [new DataContent(stateBytes, "application/json")]
|
||||
};
|
||||
@@ -23,7 +23,7 @@ var agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
tools: [
|
||||
AIFunctionFactory.Create(
|
||||
|
||||
@@ -119,7 +119,7 @@ The `AGUIServer` uses the `MapAGUI` extension method to expose an agent through
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
.CreateAIAgent(
|
||||
.AsAIAgent(
|
||||
instructions: "You are a helpful assistant.",
|
||||
name: "AGUIAssistant");
|
||||
|
||||
@@ -144,16 +144,16 @@ var chatClient = new AGUIChatClient(
|
||||
modelId: "agui-client",
|
||||
jsonSerializerOptions: null);
|
||||
|
||||
AIAgent agent = chatClient.CreateAIAgent(
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
instructions: null,
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent",
|
||||
tools: []);
|
||||
|
||||
bool isFirstUpdate = true;
|
||||
AgentRunResponseUpdate? currentUpdate = null;
|
||||
AgentResponseUpdate? currentUpdate = null;
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
|
||||
{
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
@@ -190,19 +190,19 @@ if (currentUpdate != null)
|
||||
The `RunStreamingAsync` method:
|
||||
1. Sends messages to the server via HTTP POST
|
||||
2. Receives server-sent events (SSE) stream
|
||||
3. Parses events into `AgentRunResponseUpdate` objects
|
||||
3. Parses events into `AgentResponseUpdate` objects
|
||||
4. Yields updates as they arrive for real-time display
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
|
||||
- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
|
||||
- **AgentRunResponseUpdate**: Contains the response data with:
|
||||
- **AgentResponseUpdate**: Contains the response data with:
|
||||
- `ResponseId`: The unique run identifier
|
||||
- `ConversationId`: The thread/conversation identifier
|
||||
- `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
|
||||
- **Run Lifecycle**:
|
||||
- The **first** `AgentRunResponseUpdate` in a run indicates the run has started
|
||||
- The **first** `AgentResponseUpdate` in a run indicates the run has started
|
||||
- Subsequent updates contain streaming content as the agent processes
|
||||
- The **last** `AgentRunResponseUpdate` in a run indicates the run has finished
|
||||
- The **last** `AgentResponseUpdate` in a run indicates the run has finished
|
||||
- If an error occurs, the update will contain `ErrorContent`
|
||||
@@ -1 +0,0 @@
|
||||
.playwright-mcp/
|
||||
@@ -1,11 +0,0 @@
|
||||
<Solution>
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
<Project Path="AGUIDojoClient/AGUIDojoClient.csproj" />
|
||||
<Project Path="AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
</Solution>
|
||||
@@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks></TargetFrameworks>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
[CascadingTypeParameter(nameof(TState))]
|
||||
public partial class AgentStateBoundary<TState> : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private RenderFragment? _renderWithState;
|
||||
private protected AgentBoundaryContext<TState>? _context;
|
||||
private AgentThread? _currentThread;
|
||||
private AIAgent? _currentAgent;
|
||||
private bool _disposed;
|
||||
|
||||
[EditorRequired][Parameter] public AIAgent? Agent { get; set; }
|
||||
|
||||
[Parameter] public AgentThread? Thread { get; set; }
|
||||
|
||||
[Parameter] public TState? State { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked when the boundary context is created or recreated.
|
||||
/// Use this to register tools or perform other initialization.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<IAgentBoundaryContext> OnContextCreated { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
this._renderWithState = this.RenderWithState;
|
||||
}
|
||||
|
||||
// Agent boundary renders once when it receives the initial set of parameters
|
||||
// and only ever renders again if the Agent, Thread, or State changes.
|
||||
// Re-rendering the agent boundary will dispose the existing context and create
|
||||
// a new one.
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (this.Agent == null)
|
||||
{
|
||||
throw new InvalidOperationException("AgentBoundary requires an Agent parameter.");
|
||||
}
|
||||
|
||||
// Use Thread parameter if provided, otherwise keep existing thread or create new one
|
||||
var thread = this.Thread ?? this._currentThread ?? this.Agent.GetNewThread();
|
||||
|
||||
TState? currentState = this._context != null ? this._context.CurrentState : default;
|
||||
|
||||
bool agentChanged = this.Agent != this._currentAgent;
|
||||
bool threadChanged = thread != this._currentThread;
|
||||
bool stateChanged = !EqualityComparer<TState?>.Default.Equals(this.State, currentState);
|
||||
|
||||
bool refresh = agentChanged || threadChanged || stateChanged;
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
this._context?.Dispose();
|
||||
this._context = null;
|
||||
this._currentThread = null;
|
||||
}
|
||||
|
||||
this._currentAgent = this.Agent;
|
||||
this._currentThread = thread;
|
||||
|
||||
// Agent is validated non-null above, thread is either from Thread parameter or GetNewThread()
|
||||
bool isNewContext = this._context == null;
|
||||
this._context ??= new AgentBoundaryContext<TState>(this.Agent!, thread!);
|
||||
this._context.CurrentState = this.State;
|
||||
|
||||
// Invoke the context created callback if we created a new context
|
||||
if (isNewContext && this.OnContextCreated.HasDelegate)
|
||||
{
|
||||
_ = this.OnContextCreated.InvokeAsync(this._context);
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<AgentBoundaryContext<TState>>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this._context);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", this._renderWithState);
|
||||
builder.CloseComponent();
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void RenderWithState(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<TState?>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this.State);
|
||||
builder.AddComponentParameter(2, "IsFixed", false);
|
||||
builder.AddComponentParameter(3, "ChildContent", this.ChildContent);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!this._disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
this._context?.Dispose();
|
||||
}
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AgentBoundary : AgentStateBoundary<object?>
|
||||
{
|
||||
protected override void RenderWithState(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<IAgentBoundaryContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this._context);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", this.ChildContent);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public sealed partial class AgentBoundaryContext<TState> : IAgentBoundaryContext, IDisposable
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private readonly AgentThread _thread;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly List<ChatMessage> _messages = [];
|
||||
private readonly List<ChatMessage> _pendingMessages = [];
|
||||
private readonly List<Action> _messageChangeSubscribers = [];
|
||||
private readonly List<Action> _responseUpdateSubscribers = [];
|
||||
private readonly List<Action> _runStatusSubscribers = [];
|
||||
private readonly List<AITool> _tools = [];
|
||||
private readonly Dictionary<string, TaskCompletionSource<object>> _pendingResponses = [];
|
||||
|
||||
public CancellationToken CancellationToken => this._cancellationTokenSource.Token;
|
||||
|
||||
public IReadOnlyList<ChatMessage> CompletedMessages => this._messages.AsReadOnly();
|
||||
|
||||
public TState? CurrentState { get; set; }
|
||||
|
||||
public IReadOnlyList<ChatMessage> PendingMessages => this._pendingMessages.AsReadOnly();
|
||||
|
||||
public ChatMessage? CurrentMessage { get; private set; }
|
||||
|
||||
public ChatResponseUpdate? CurrentUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the agent is currently processing a turn.
|
||||
/// </summary>
|
||||
public bool IsProcessing { get; private set; }
|
||||
|
||||
public AgentBoundaryContext(AIAgent agent, AgentThread thread)
|
||||
{
|
||||
this._agent = agent;
|
||||
this._thread = thread;
|
||||
this._cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a client-side tool that will be passed to the agent during invocation.
|
||||
/// These tools are sent to the server via ChatClientAgentRunOptions.ChatOptions.Tools.
|
||||
/// </summary>
|
||||
public void RegisterTool(AITool tool)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tool);
|
||||
this._tools.Add(tool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers multiple client-side tools that will be passed to the agent during invocation.
|
||||
/// </summary>
|
||||
public void RegisterTools(params AITool[] tools)
|
||||
{
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
this.RegisterTool(tool);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pending response for the given key and returns a task that completes when ProvideResponse is called.
|
||||
/// Used by frontend tools to wait for user input from UI components.
|
||||
/// </summary>
|
||||
/// <param name="key">A unique key to identify this pending response (e.g., function call ID).</param>
|
||||
/// <returns>A task that completes with the response object when ProvideResponse is called.</returns>
|
||||
public Task<object> WaitForResponse(string key)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
this._pendingResponses[key] = tcs;
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a response for a pending request, completing the task returned by WaitForResponse.
|
||||
/// Called by UI components when user interaction is complete.
|
||||
/// </summary>
|
||||
/// <param name="key">The key used when calling WaitForResponse.</param>
|
||||
/// <param name="response">The response object to return.</param>
|
||||
/// <returns>True if the response was provided successfully, false if no pending request was found.</returns>
|
||||
public bool ProvideResponse(string key, object response)
|
||||
{
|
||||
if (this._pendingResponses.TryGetValue(key, out var tcs))
|
||||
{
|
||||
this._pendingResponses.Remove(key);
|
||||
tcs.TrySetResult(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._cancellationTokenSource.Cancel();
|
||||
this._cancellationTokenSource.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task SendAsync(params ChatMessage[] userMessages)
|
||||
{
|
||||
// This starts a new turn. Once completed, we will add the new messages to the conversation.
|
||||
this._pendingMessages.Clear();
|
||||
this._pendingMessages.AddRange(userMessages);
|
||||
|
||||
// Mark as processing
|
||||
this.IsProcessing = true;
|
||||
|
||||
// Notify subscribers that run status changed (processing started)
|
||||
this.TriggerRunStatusChanged();
|
||||
|
||||
// User messages added, notify subscribers.
|
||||
this.TriggerMessageChanges();
|
||||
|
||||
// Build run options with registered tools if any
|
||||
AgentRunOptions? options = null;
|
||||
if (this._tools.Count > 0)
|
||||
{
|
||||
options = new ChatClientAgentRunOptions(new ChatOptions
|
||||
{
|
||||
Tools = [.. this._tools]
|
||||
});
|
||||
}
|
||||
|
||||
// Start a turn. Collect all updates as we stream them.
|
||||
await foreach (var update in this._agent.RunStreamingAsync(
|
||||
userMessages,
|
||||
this._thread,
|
||||
options,
|
||||
this._cancellationTokenSource.Token))
|
||||
{
|
||||
var chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
this.CurrentUpdate = chatUpdate;
|
||||
|
||||
// Notify subscribers of the new update, this always happens before a new message is created/updated.
|
||||
this.TriggerChatResponseUpdate();
|
||||
|
||||
// This creates or adds a new message to the pending messages as needed.
|
||||
var isNewMessage = MessageHelpers.ProcessUpdate(chatUpdate, this._pendingMessages);
|
||||
if (isNewMessage)
|
||||
{
|
||||
this.CurrentMessage = this._pendingMessages[this._pendingMessages.Count - 1];
|
||||
|
||||
// Finalize the previous message's content if we have 2 or more messages now.
|
||||
if (this._pendingMessages.Count > userMessages.Length + 1)
|
||||
{
|
||||
MessageHelpers.CoalesceContent(this._pendingMessages[this._pendingMessages.Count - 2].Contents);
|
||||
}
|
||||
|
||||
// Notify subscribers of new message
|
||||
this.TriggerMessageChanges();
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining updates to finalize the last message.
|
||||
if (this._pendingMessages.Count > userMessages.Length)
|
||||
{
|
||||
MessageHelpers.CoalesceContent(this._pendingMessages[this._pendingMessages.Count - 1].Contents);
|
||||
}
|
||||
|
||||
// Add the new messages to the conversation
|
||||
this._messages.AddRange(this._pendingMessages);
|
||||
|
||||
// Finish the turn
|
||||
this._pendingMessages.Clear();
|
||||
this.CurrentMessage = null;
|
||||
this.CurrentUpdate = null;
|
||||
this.IsProcessing = false;
|
||||
|
||||
// Notify subscribers that run status changed (processing ended)
|
||||
this.TriggerRunStatusChanged();
|
||||
|
||||
// Notify subscribers
|
||||
this.TriggerChatResponseUpdate();
|
||||
this.TriggerMessageChanges();
|
||||
}
|
||||
|
||||
private void TriggerChatResponseUpdate()
|
||||
{
|
||||
// Iterate backwards to avoid issues if subscribers are removed during iteration
|
||||
for (var i = this._responseUpdateSubscribers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
this._responseUpdateSubscribers[i]();
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerMessageChanges()
|
||||
{
|
||||
// Iterate backwards to avoid issues if subscribers are removed during iteration
|
||||
for (var i = this._messageChangeSubscribers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
this._messageChangeSubscribers[i]();
|
||||
}
|
||||
}
|
||||
|
||||
public MessageSubscription SubscribeToMessageChanges(Action onNewMessage)
|
||||
{
|
||||
return new MessageSubscription(this._messageChangeSubscribers, onNewMessage);
|
||||
}
|
||||
|
||||
public ResponseUpdateSubscription SubscribeToResponseUpdates(Action onChatResponse)
|
||||
{
|
||||
return new ResponseUpdateSubscription(this._responseUpdateSubscribers, onChatResponse);
|
||||
}
|
||||
|
||||
public RunStatusSubscription SubscribeToRunStatusChanges(Action onRunStatusChanged)
|
||||
{
|
||||
return new RunStatusSubscription(this._runStatusSubscribers, onRunStatusChanged);
|
||||
}
|
||||
|
||||
private void TriggerRunStatusChanged()
|
||||
{
|
||||
// Iterate backwards to avoid issues if subscribers are removed during iteration
|
||||
for (var i = this._runStatusSubscribers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
this._runStatusSubscribers[i]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAgentBoundaryContext
|
||||
{
|
||||
// Push new messages to the agent
|
||||
Task SendAsync(params ChatMessage[] userMessages);
|
||||
|
||||
// All message interactions from previous turns
|
||||
IReadOnlyList<ChatMessage> CompletedMessages { get; }
|
||||
|
||||
// All message interactions from the current turn. These represent completed messages only.
|
||||
IReadOnlyList<ChatMessage> PendingMessages { get; }
|
||||
|
||||
// The current message being processed by the agent.
|
||||
ChatMessage? CurrentMessage { get; }
|
||||
|
||||
ChatResponseUpdate? CurrentUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the agent is currently processing a turn.
|
||||
/// </summary>
|
||||
bool IsProcessing { get; }
|
||||
|
||||
// Triggered any time there is a change on a message.
|
||||
MessageSubscription SubscribeToMessageChanges(Action onNewMessage);
|
||||
|
||||
ResponseUpdateSubscription SubscribeToResponseUpdates(Action onChatResponse);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to run status changes (when processing starts or ends).
|
||||
/// </summary>
|
||||
/// <param name="onRunStatusChanged">The callback to invoke when run status changes.</param>
|
||||
/// <returns>A subscription that can be disposed to unsubscribe.</returns>
|
||||
RunStatusSubscription SubscribeToRunStatusChanges(Action onRunStatusChanged);
|
||||
|
||||
CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a client-side tool that will be passed to the agent during invocation.
|
||||
/// </summary>
|
||||
void RegisterTool(AITool tool);
|
||||
|
||||
/// <summary>
|
||||
/// Registers multiple client-side tools that will be passed to the agent during invocation.
|
||||
/// </summary>
|
||||
void RegisterTools(params AITool[] tools);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pending response for the given key and returns a task that completes when ProvideResponse is called.
|
||||
/// Used by frontend tools to wait for user input from UI components.
|
||||
/// </summary>
|
||||
/// <param name="key">A unique key to identify this pending response (e.g., function call ID).</param>
|
||||
/// <returns>A task that completes with the response object when ProvideResponse is called.</returns>
|
||||
Task<object> WaitForResponse(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Provides a response for a pending request, completing the task returned by WaitForResponse.
|
||||
/// Called by UI components when user interaction is complete.
|
||||
/// </summary>
|
||||
/// <param name="key">The key used when calling WaitForResponse.</param>
|
||||
/// <param name="response">The response object to return.</param>
|
||||
/// <returns>True if the response was provided successfully, false if no pending request was found.</returns>
|
||||
bool ProvideResponse(string key, object response);
|
||||
}
|
||||
|
||||
public readonly struct MessageSubscription : IDisposable, IEquatable<MessageSubscription>
|
||||
{
|
||||
internal readonly List<Action> _subscribers;
|
||||
internal readonly Action _subscription;
|
||||
|
||||
internal MessageSubscription(List<Action> subscribers, Action subscription)
|
||||
{
|
||||
this._subscribers = subscribers;
|
||||
this._subscription = subscription;
|
||||
this._subscribers.Add(this._subscription);
|
||||
}
|
||||
|
||||
public void Dispose() => this._subscribers.Remove(this._subscription);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is MessageSubscription subscription && this.Equals(subscription);
|
||||
}
|
||||
|
||||
public bool Equals(MessageSubscription other)
|
||||
{
|
||||
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
|
||||
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this._subscribers, this._subscription);
|
||||
}
|
||||
|
||||
public static bool operator ==(MessageSubscription left, MessageSubscription right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(MessageSubscription left, MessageSubscription right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ResponseUpdateSubscription : IDisposable, IEquatable<ResponseUpdateSubscription>
|
||||
{
|
||||
private readonly List<Action> _subscribers;
|
||||
private readonly Action _subscription;
|
||||
|
||||
public ResponseUpdateSubscription(List<Action> subscribers, Action subscription)
|
||||
{
|
||||
this._subscribers = subscribers;
|
||||
this._subscription = subscription;
|
||||
this._subscribers.Add(this._subscription);
|
||||
}
|
||||
|
||||
public void Dispose() => this._subscribers.Remove(this._subscription);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ResponseUpdateSubscription subscription && this.Equals(subscription);
|
||||
}
|
||||
|
||||
public bool Equals(ResponseUpdateSubscription other)
|
||||
{
|
||||
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
|
||||
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this._subscribers, this._subscription);
|
||||
}
|
||||
|
||||
public static bool operator ==(ResponseUpdateSubscription left, ResponseUpdateSubscription right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ResponseUpdateSubscription left, ResponseUpdateSubscription right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct RunStatusSubscription : IDisposable, IEquatable<RunStatusSubscription>
|
||||
{
|
||||
private readonly List<Action> _subscribers;
|
||||
private readonly Action _subscription;
|
||||
|
||||
public RunStatusSubscription(List<Action> subscribers, Action subscription)
|
||||
{
|
||||
this._subscribers = subscribers;
|
||||
this._subscription = subscription;
|
||||
this._subscribers.Add(this._subscription);
|
||||
}
|
||||
|
||||
public void Dispose() => this._subscribers.Remove(this._subscription);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is RunStatusSubscription subscription && this.Equals(subscription);
|
||||
}
|
||||
|
||||
public bool Equals(RunStatusSubscription other)
|
||||
{
|
||||
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
|
||||
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this._subscribers, this._subscription);
|
||||
}
|
||||
|
||||
public static bool operator ==(RunStatusSubscription left, RunStatusSubscription right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(RunStatusSubscription left, RunStatusSubscription right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public sealed partial class AgentInput : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private AgentBoundaryContext<object?>? _context;
|
||||
private string? _inputText;
|
||||
private RunStatusSubscription? _subscription;
|
||||
|
||||
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
|
||||
|
||||
[Parameter] public string Placeholder { get; set; } = "Type a message...";
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Unsubscribe from previous context if it changed
|
||||
if (this._context != this.AgentContext)
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
this._context = this.AgentContext;
|
||||
|
||||
// Subscribe to run status changes
|
||||
if (this._context != null)
|
||||
{
|
||||
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this.Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRunStatusChanged()
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var isProcessing = this._context?.IsProcessing ?? false;
|
||||
var isDisabled = string.IsNullOrWhiteSpace(this._inputText) || isProcessing;
|
||||
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "agent-input");
|
||||
|
||||
builder.OpenElement(2, "textarea");
|
||||
builder.AddAttribute(3, "value", this._inputText);
|
||||
builder.AddAttribute(4, "oninput", EventCallback.Factory.Create<ChangeEventArgs>(this, e => { this._inputText = e.Value?.ToString(); this.Render(); }));
|
||||
builder.AddAttribute(5, "placeholder", this.Placeholder);
|
||||
builder.AddAttribute(6, "rows", "1");
|
||||
builder.AddAttribute(7, "disabled", isProcessing);
|
||||
builder.CloseElement();
|
||||
|
||||
builder.OpenElement(8, "button");
|
||||
builder.AddAttribute(9, "class", "send-button");
|
||||
builder.AddAttribute(10, "onclick", EventCallback.Factory.Create(this, this.SendAsync));
|
||||
builder.AddAttribute(11, "disabled", isDisabled);
|
||||
builder.AddMarkupContent(12, """<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>""");
|
||||
builder.CloseElement(); // close button
|
||||
|
||||
builder.CloseElement(); // close div
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this._inputText) && this._context is { IsProcessing: false })
|
||||
{
|
||||
var text = this._inputText;
|
||||
this._inputText = ""; // Clear input immediately
|
||||
this.Render(); // Re-render to clear input
|
||||
|
||||
await this._context.SendAsync(new ChatMessage(ChatRole.User, text));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// A component that displays a loading indicator (three animated dots) when the agent is processing.
|
||||
/// </summary>
|
||||
public sealed partial class AgentLoadingIndicator : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private AgentBoundaryContext<object?>? _context;
|
||||
private RunStatusSubscription? _subscription;
|
||||
|
||||
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Unsubscribe from previous context if it changed
|
||||
if (this._context != this.AgentContext)
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
this._context = this.AgentContext;
|
||||
|
||||
// Subscribe to run status changes
|
||||
if (this._context != null)
|
||||
{
|
||||
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this.Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRunStatusChanged()
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var isProcessing = this._context?.IsProcessing ?? false;
|
||||
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
if (!isProcessing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "agent-loading-indicator");
|
||||
builder.OpenElement(2, "div");
|
||||
builder.AddAttribute(3, "class", "agent-loading-dots");
|
||||
builder.AddMarkupContent(4, "<span></span><span></span><span></span>");
|
||||
builder.CloseElement();
|
||||
builder.CloseElement();
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@namespace Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@typeparam TState
|
||||
@implements IDisposable
|
||||
|
||||
<CascadingValue Value="@CurrentState" IsFixed="false">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
private ResponseUpdateSubscription? _subscription;
|
||||
|
||||
/// <summary>
|
||||
/// The agent boundary context to subscribe to for state updates.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
public IAgentBoundaryContext? BoundaryContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current state value. Can be set initially and will be updated by state events.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public TState? CurrentState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to deserialize a STATE_SNAPSHOT (application/json) into TState.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<ReadOnlyMemory<byte>, TState?>? OnSnapshot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to apply a STATE_DELTA (application/json-patch+json) to the current state.
|
||||
/// Returns the updated state.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<TState?, ReadOnlyMemory<byte>, TState?>? OnDelta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback invoked whenever state changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<TState?> CurrentStateChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Child content that will receive the cascaded state.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (BoundaryContext is not null)
|
||||
{
|
||||
_subscription = BoundaryContext.SubscribeToResponseUpdates(OnResponseUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResponseUpdate()
|
||||
{
|
||||
var update = BoundaryContext?.CurrentUpdate;
|
||||
if (update?.Contents is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
if (content is DataContent dataContent)
|
||||
{
|
||||
if (string.Equals(dataContent.MediaType, "application/json", StringComparison.OrdinalIgnoreCase) && OnSnapshot is not null)
|
||||
{
|
||||
// STATE_SNAPSHOT - let app deserialize
|
||||
CurrentState = OnSnapshot(dataContent.Data);
|
||||
_ = CurrentStateChanged.InvokeAsync(CurrentState);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
else if (string.Equals(dataContent.MediaType, "application/json-patch+json", StringComparison.OrdinalIgnoreCase) && OnDelta is not null)
|
||||
{
|
||||
// STATE_DELTA - let app apply the patch
|
||||
CurrentState = OnDelta(CurrentState, dataContent.Data);
|
||||
_ = CurrentStateChanged.InvokeAsync(CurrentState);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a suggestion that can be displayed and sent to the agent.
|
||||
/// </summary>
|
||||
public readonly struct Suggestion : IEquatable<Suggestion>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Suggestion"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="text">The display text for the suggestion.</param>
|
||||
/// <param name="message">The message to send when the suggestion is selected.</param>
|
||||
public Suggestion(string text, ChatMessage message)
|
||||
{
|
||||
this.Text = text;
|
||||
this.Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Suggestion"/> struct with a simple text message.
|
||||
/// </summary>
|
||||
/// <param name="text">The display text for the suggestion, also used as the message content.</param>
|
||||
public Suggestion(string text)
|
||||
{
|
||||
this.Text = text;
|
||||
this.Message = new ChatMessage(ChatRole.User, text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display text for the suggestion.
|
||||
/// </summary>
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message to send when the suggestion is selected.
|
||||
/// </summary>
|
||||
public ChatMessage Message { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is Suggestion other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(Suggestion other) => this.Text == other.Text;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.Text?.GetHashCode() ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two <see cref="Suggestion"/> instances are equal.
|
||||
/// </summary>
|
||||
public static bool operator ==(Suggestion left, Suggestion right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two <see cref="Suggestion"/> instances are not equal.
|
||||
/// </summary>
|
||||
public static bool operator !=(Suggestion left, Suggestion right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
public sealed partial class AgentSuggestions : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private AgentBoundaryContext<object?>? _context;
|
||||
private IReadOnlyList<Suggestion>? _suggestions;
|
||||
private RunStatusSubscription? _subscription;
|
||||
|
||||
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
|
||||
|
||||
[Parameter] public IReadOnlyList<Suggestion>? Suggestions { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Unsubscribe from previous context if it changed
|
||||
if (this._context != this.AgentContext)
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
this._context = this.AgentContext;
|
||||
|
||||
// Subscribe to run status changes
|
||||
if (this._context != null)
|
||||
{
|
||||
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this._suggestions = this.Suggestions;
|
||||
this.Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRunStatusChanged()
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var isProcessing = this._context?.IsProcessing ?? false;
|
||||
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
if (this._suggestions is null || this._suggestions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "agent-suggestions");
|
||||
|
||||
for (var i = 0; i < this._suggestions.Count; i++)
|
||||
{
|
||||
var suggestion = this._suggestions[i];
|
||||
builder.OpenElement(2, "button");
|
||||
builder.SetKey(suggestion.Text);
|
||||
builder.AddAttribute(3, "class", "suggestion-button");
|
||||
builder.AddAttribute(4, "onclick", EventCallback.Factory.Create(this, () => this.SelectSuggestionAsync(suggestion)));
|
||||
builder.AddAttribute(5, "disabled", isProcessing);
|
||||
builder.AddContent(6, suggestion.Text);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SelectSuggestionAsync(Suggestion suggestion)
|
||||
{
|
||||
if (this._context is { IsProcessing: false })
|
||||
{
|
||||
await this._context.SendAsync(suggestion.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class is apparently never instantiated
|
||||
internal sealed class ContentBlock : IComponent
|
||||
#pragma warning restore CA1812 // Internal class is apparently never instantiated
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
// Always render when parameters are set - the parent component (MessageList)
|
||||
// only triggers renders when there are actual updates to show.
|
||||
this._renderHandle.Render(this.Render);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, this.ChildContent);
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public class ContentContext(AIContent content)
|
||||
{
|
||||
public AIContent Content { get; init; } = content;
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public abstract class ContentTemplateBase : IComponent
|
||||
{
|
||||
public abstract void Attach(RenderHandle renderHandle);
|
||||
|
||||
public abstract Task SetParametersAsync(ParameterView parameters);
|
||||
|
||||
public virtual bool When(ContentContext context) => true;
|
||||
|
||||
[Parameter] public RenderFragment<ContentContext> ChildContent { get; set; } = (content) => builder => { };
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for rendering DataContent. Renders nothing by default
|
||||
/// as DataContent typically contains binary/JSON data not meant for display.
|
||||
/// </summary>
|
||||
public class DataContentTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this.ChildContent = this.RenderData;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
return context.Content is DataContent;
|
||||
}
|
||||
|
||||
private RenderFragment RenderData(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, render nothing.
|
||||
};
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for rendering ErrorContent. Renders nothing by default
|
||||
/// to prevent the error from crashing the UI. More specific templates can
|
||||
/// override this to display error messages.
|
||||
/// </summary>
|
||||
public class ErrorTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this.ChildContent = this.RenderError;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this template should handle the given content.
|
||||
/// Matches ErrorContent.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
return context.Content is ErrorContent;
|
||||
}
|
||||
|
||||
private RenderFragment RenderError(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, render nothing.
|
||||
// Specific templates can override to display error messages.
|
||||
};
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Template for rendering function/tool call content in messages.
|
||||
/// Provides access to both the call and its result (when available) via InvocationContext.
|
||||
/// </summary>
|
||||
public class FunctionCallTemplate : ContentTemplateBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the tool name to filter on. If null, matches all function calls.
|
||||
/// </summary>
|
||||
[Parameter] public string? ToolName { get; set; }
|
||||
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderFunctionCall;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this template should handle the given content.
|
||||
/// </summary>
|
||||
/// <param name="context">The content context.</param>
|
||||
/// <returns>True if this template should render the content.</returns>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
if (context.Content is not FunctionCallContent call)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by tool name if specified
|
||||
if (this.ToolName != null && !string.Equals(call.Name, this.ToolName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private RenderFragment RenderFunctionCall(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, function calls are not rendered visually.
|
||||
// Custom templates (like WeatherCallTemplate) can override this
|
||||
// behavior for specific functions by registering before this template.
|
||||
};
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for FunctionResultContent that renders nothing.
|
||||
/// Function results are internal tool responses and typically don't need
|
||||
/// visual representation in the chat UI.
|
||||
/// </summary>
|
||||
public class FunctionResultTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderFunctionResult;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only match FunctionResultContent.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context) => context.Content is FunctionResultContent;
|
||||
|
||||
private RenderFragment RenderFunctionResult(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, function results are not rendered visually.
|
||||
// The result data is typically processed by the agent to generate text responses.
|
||||
};
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Context for a function invocation, tracking the call and its result.
|
||||
/// </summary>
|
||||
public class InvocationContext
|
||||
{
|
||||
private Action? _resultArrived;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvocationContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="call">The function call content.</param>
|
||||
public InvocationContext(FunctionCallContent call)
|
||||
{
|
||||
this.Call = call;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function call content.
|
||||
/// </summary>
|
||||
public FunctionCallContent Call { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function result content, if available.
|
||||
/// </summary>
|
||||
public FunctionResultContent? ResultContent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the result has arrived.
|
||||
/// </summary>
|
||||
public bool HasResult => this.ResultContent != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function name from the call.
|
||||
/// </summary>
|
||||
public string FunctionName => this.Call.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the call ID.
|
||||
/// </summary>
|
||||
public string CallId => this.Call.CallId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the arguments from the call.
|
||||
/// </summary>
|
||||
public IDictionary<string, object?>? Arguments => this.Call.Arguments;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the result arrives.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1003 // Use generic event handler instances
|
||||
public event Action? ResultArrived
|
||||
#pragma warning restore CA1003 // Use generic event handler instances
|
||||
{
|
||||
add => this._resultArrived += value;
|
||||
remove => this._resultArrived -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the result and raises the ResultArrived event.
|
||||
/// </summary>
|
||||
/// <param name="result">The function result content.</param>
|
||||
internal void SetResult(FunctionResultContent result)
|
||||
{
|
||||
this.ResultContent = result;
|
||||
this._resultArrived?.Invoke();
|
||||
this._resultArrived = null; // Clear invocation list after firing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an argument value by name, deserializing from JSON if necessary.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize to.</typeparam>
|
||||
/// <param name="name">The argument name.</param>
|
||||
/// <returns>The argument value, or default if not found.</returns>
|
||||
public T? GetArgument<T>(string name)
|
||||
{
|
||||
if (this.Arguments is null || !this.Arguments.TryGetValue(name, out var value))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (value is T typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
return jsonElement.Deserialize<T>();
|
||||
}
|
||||
|
||||
// Try to convert via JSON serialization
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result as a specific type, deserializing from JSON if necessary.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize to.</typeparam>
|
||||
/// <returns>The result value, or default if not available.</returns>
|
||||
public T? GetResult<T>()
|
||||
{
|
||||
if (this.ResultContent?.Result is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (this.ResultContent.Result is T typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
if (this.ResultContent.Result is JsonElement jsonElement)
|
||||
{
|
||||
return jsonElement.Deserialize<T>();
|
||||
}
|
||||
|
||||
// Try to convert via JSON serialization
|
||||
var json = JsonSerializer.Serialize(this.ResultContent.Result);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public class TextTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderText;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only match TextContent, allowing other content templates to handle
|
||||
/// FunctionCallContent, FunctionResultContent, etc.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context) => context.Content is TextContent;
|
||||
|
||||
private RenderFragment RenderText(ContentContext content) => builder =>
|
||||
{
|
||||
if (content.Content is TextContent textContent)
|
||||
{
|
||||
builder.AddContent(0, textContent.Text);
|
||||
}
|
||||
};
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class is apparently never instantiated
|
||||
internal sealed class DefaultMessageTemplate : MessageTemplateBase
|
||||
#pragma warning restore CA1812 // Internal class is apparently never instantiated
|
||||
{
|
||||
public override bool When(MessageContext context) => true;
|
||||
|
||||
// Buffer to convert response updates to chat messages.
|
||||
private readonly List<ChatMessage> _buffer = [];
|
||||
|
||||
public DefaultMessageTemplate()
|
||||
{
|
||||
this.ChildContent = this.SelectTemplate;
|
||||
}
|
||||
|
||||
private RenderFragment SelectTemplate(MessageContext messageContext)
|
||||
{
|
||||
if (messageContext.ChatMessage is not null)
|
||||
{
|
||||
var getRenderContents = messageContext.RenderContents();
|
||||
// Return a render fragment that checks visibility at render time
|
||||
// This is important because message contents may change during streaming
|
||||
return CreateRenderMessage(
|
||||
messageContext.ChatMessage,
|
||||
getRenderContents);
|
||||
}
|
||||
else
|
||||
{
|
||||
var getRenderContents = messageContext.RenderContents();
|
||||
var updates = messageContext.ResponseUpdates ?? [];
|
||||
if (updates.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("MessageContext must have either a ChatMessage or at least one ResponseUpdate.");
|
||||
}
|
||||
this._buffer.Clear();
|
||||
this._buffer.AddMessages(updates);
|
||||
if (this._buffer.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException("DefaultMessageTemplate only supports a single ResponseUpdate.");
|
||||
}
|
||||
|
||||
return CreateRenderMessage(
|
||||
this._buffer[0],
|
||||
getRenderContents);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a message has any visible content that should be displayed.
|
||||
/// Messages that only contain FunctionResultContent are internal tool messages
|
||||
/// and should not be rendered as chat bubbles.
|
||||
/// FunctionCallContent is considered visible because content templates can render them
|
||||
/// (e.g., WeatherCallTemplate renders weather tool calls as cards).
|
||||
/// </summary>
|
||||
private static bool HasVisibleContent(ChatMessage message)
|
||||
{
|
||||
if (message.Contents.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var content in message.Contents)
|
||||
{
|
||||
// TextContent is visible
|
||||
if (content is TextContent textContent && !string.IsNullOrWhiteSpace(textContent.Text))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// FunctionCallContent is visible - content templates can render them
|
||||
// (e.g., WeatherCallTemplate renders weather tool calls as cards)
|
||||
if (content is FunctionCallContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Other content types that are not function results are visible
|
||||
if (content is not FunctionResultContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static RenderFragment CreateRenderMessage(
|
||||
ChatMessage message,
|
||||
RenderFragment getRenderContents)
|
||||
{
|
||||
var roleClass = $"{message.Role}-message";
|
||||
return builder =>
|
||||
{
|
||||
// Check visibility at render time, not at template creation time
|
||||
// This is important because message contents may change during streaming
|
||||
if (!HasVisibleContent(message))
|
||||
{
|
||||
return; // Don't render messages without visible content
|
||||
}
|
||||
|
||||
builder.OpenElement(0, "div");
|
||||
if (!string.IsNullOrEmpty(message.MessageId))
|
||||
{
|
||||
builder.AddAttribute(1, "id", message.MessageId);
|
||||
}
|
||||
builder.AddAttribute(2, "class", $"chat-message {roleClass}");
|
||||
builder.AddContent(3, getRenderContents);
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public class MessageContext
|
||||
{
|
||||
private readonly MessageListContext _messageListContext;
|
||||
private RenderFragment? _contentsRenderer;
|
||||
private MessageTemplateBase? _template;
|
||||
private readonly Dictionary<AIContent, RenderFragment> _contentRenderers = [];
|
||||
|
||||
internal MessageContext(ChatMessage message, MessageListContext messageListContext)
|
||||
{
|
||||
this.ChatMessage = message;
|
||||
this._messageListContext = messageListContext;
|
||||
}
|
||||
|
||||
public ChatMessage? ChatMessage { get; init; }
|
||||
|
||||
public IList<ChatResponseUpdate>? ResponseUpdates { get; init; }
|
||||
|
||||
public RenderFragment RenderContents() => this.GetOrCreateContentsRenderer();
|
||||
|
||||
internal void SetTemplate(MessageTemplateBase template)
|
||||
{
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
private RenderFragment GetOrCreateContentsRenderer()
|
||||
{
|
||||
if (this._contentsRenderer != null)
|
||||
{
|
||||
return this._contentsRenderer;
|
||||
}
|
||||
|
||||
if (this._template == null)
|
||||
{
|
||||
throw new InvalidOperationException("Message template has not been set for this message context.");
|
||||
}
|
||||
|
||||
this._contentsRenderer = builder =>
|
||||
{
|
||||
if (this.ChatMessage != null)
|
||||
{
|
||||
for (int i = 0; i < this.ChatMessage.Contents.Count; i++)
|
||||
{
|
||||
var content = this.ChatMessage.Contents[i];
|
||||
builder.AddContent(0, this.ResolveContentRenderer(content));
|
||||
}
|
||||
}
|
||||
else if (this.ResponseUpdates != null)
|
||||
{
|
||||
for (int i = 0; i < this.ResponseUpdates.Count; i++)
|
||||
{
|
||||
var update = this.ResponseUpdates[i];
|
||||
for (int j = 0; j < update.Contents.Count; j++)
|
||||
{
|
||||
var content = update.Contents[j];
|
||||
if (this._contentRenderers.TryGetValue(content, out var contentRenderer))
|
||||
{
|
||||
builder.AddContent(0, contentRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("MessageContext must have either a ChatMessage or ResponseUpdates to render contents.");
|
||||
}
|
||||
};
|
||||
|
||||
return this._contentsRenderer;
|
||||
}
|
||||
|
||||
private RenderFragment ResolveContentRenderer(AIContent content)
|
||||
{
|
||||
if (this._contentRenderers.TryGetValue(content, out var contentRenderer))
|
||||
{
|
||||
return contentRenderer;
|
||||
}
|
||||
|
||||
Debug.Assert(this._template != null);
|
||||
contentRenderer = this._template.GetContentTemplate(content);
|
||||
if (contentRenderer != null)
|
||||
{
|
||||
this._contentRenderers[content] = contentRenderer;
|
||||
return contentRenderer;
|
||||
}
|
||||
contentRenderer = this._messageListContext.GetContentTemplate(content);
|
||||
this._contentRenderers[content] = contentRenderer;
|
||||
return contentRenderer;
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
// Roughly lifted from src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs
|
||||
internal static class MessageHelpers
|
||||
{
|
||||
internal static void CoalesceContent(IList<AIContent> contents)
|
||||
{
|
||||
Coalesce<TextContent>(
|
||||
contents,
|
||||
mergeSingle: false,
|
||||
canMerge: null,
|
||||
static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() });
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: false,
|
||||
canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
TextReasoningContent content = new(MergeText(contents, start, end))
|
||||
{
|
||||
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
|
||||
};
|
||||
|
||||
#if DEBUG
|
||||
for (int i = start; i < end - 1; i++)
|
||||
{
|
||||
Debug.Assert(contents[i] is TextReasoningContent { ProtectedData: null }, "Expected all but the last to have a null ProtectedData");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (((TextReasoningContent)contents[end - 1]).ProtectedData is { } protectedData)
|
||||
{
|
||||
content.ProtectedData = protectedData;
|
||||
}
|
||||
|
||||
return content;
|
||||
});
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: false,
|
||||
canMerge: static (r1, r2) => r1.MediaType == r2.MediaType && r1.HasTopLevelMediaType("text") && r1.Name == r2.Name,
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
Debug.Assert(end - start > 1, "Expected multiple contents to merge");
|
||||
|
||||
MemoryStream ms = new();
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
var current = (DataContent)contents[i];
|
||||
#if NET
|
||||
ms.Write(current.Data.Span);
|
||||
#else
|
||||
if (!MemoryMarshal.TryGetArray(current.Data, out var segment))
|
||||
{
|
||||
segment = new(current.Data.ToArray());
|
||||
}
|
||||
|
||||
ms.Write(segment.Array!, segment.Offset, segment.Count);
|
||||
#endif
|
||||
}
|
||||
|
||||
var first = (DataContent)contents[start];
|
||||
return new DataContent(new ReadOnlyMemory<byte>(ms.GetBuffer(), 0, (int)ms.Length), first.MediaType) { Name = first.Name };
|
||||
});
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: true,
|
||||
canMerge: static (r1, r2) => r1.CallId == r2.CallId,
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
var firstContent = (CodeInterpreterToolCallContent)contents[start];
|
||||
|
||||
if (start == end - 1)
|
||||
{
|
||||
if (firstContent.Inputs is not null)
|
||||
{
|
||||
CoalesceContent(firstContent.Inputs);
|
||||
}
|
||||
|
||||
return firstContent;
|
||||
}
|
||||
|
||||
List<AIContent>? inputs = null;
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
(inputs ??= []).AddRange(((CodeInterpreterToolCallContent)contents[i]).Inputs ?? []);
|
||||
}
|
||||
|
||||
if (inputs is not null)
|
||||
{
|
||||
CoalesceContent(inputs);
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
CallId = firstContent.CallId,
|
||||
Inputs = inputs,
|
||||
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
|
||||
};
|
||||
});
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: true,
|
||||
canMerge: static (r1, r2) => r1.CallId is not null && r2.CallId is not null && r1.CallId == r2.CallId,
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
var firstContent = (CodeInterpreterToolResultContent)contents[start];
|
||||
|
||||
if (start == end - 1)
|
||||
{
|
||||
if (firstContent.Outputs is not null)
|
||||
{
|
||||
CoalesceContent(firstContent.Outputs);
|
||||
}
|
||||
|
||||
return firstContent;
|
||||
}
|
||||
|
||||
List<AIContent>? output = null;
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
(output ??= []).AddRange(((CodeInterpreterToolResultContent)contents[i]).Outputs ?? []);
|
||||
}
|
||||
|
||||
if (output is not null)
|
||||
{
|
||||
CoalesceContent(output);
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
CallId = firstContent.CallId,
|
||||
Outputs = output,
|
||||
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
|
||||
};
|
||||
});
|
||||
|
||||
static string MergeText(IList<AIContent> contents, int start, int end)
|
||||
{
|
||||
Debug.Assert(end - start > 1, "Expected multiple contents to merge");
|
||||
|
||||
StringBuilder sb = new();
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
_ = sb.Append(contents[i]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static void Coalesce<TContent>(
|
||||
IList<AIContent> contents,
|
||||
bool mergeSingle,
|
||||
Func<TContent, TContent, bool>? canMerge,
|
||||
Func<IList<AIContent>, int, int, TContent> merge)
|
||||
where TContent : AIContent
|
||||
{
|
||||
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
|
||||
int start = 0;
|
||||
while (start < contents.Count)
|
||||
{
|
||||
if (!TryAsCoalescable(contents[start], out var firstContent))
|
||||
{
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate until we find a non-coalescable item.
|
||||
int i = start + 1;
|
||||
TContent prev = firstContent;
|
||||
while (i < contents.Count && TryAsCoalescable(contents[i], out TContent? next) && (canMerge is null || canMerge(prev, next)))
|
||||
{
|
||||
i++;
|
||||
prev = next;
|
||||
}
|
||||
|
||||
// If there's only one item in the run, and we don't want to merge single items, skip it.
|
||||
if (start == i - 1 && !mergeSingle)
|
||||
{
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store the replacement node and null out all of the nodes that we coalesced.
|
||||
// We can then remove all coalesced nodes in one O(N) operation via RemoveAll.
|
||||
// Leave start positioned at the start of the next run.
|
||||
contents[start] = merge(contents, start, i);
|
||||
|
||||
start++;
|
||||
while (start < i)
|
||||
{
|
||||
contents[start++] = null!;
|
||||
}
|
||||
|
||||
static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent? coalescable)
|
||||
{
|
||||
if (content is TContent tmp && tmp.Annotations is not { Count: > 0 })
|
||||
{
|
||||
coalescable = tmp;
|
||||
return true;
|
||||
}
|
||||
|
||||
coalescable = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all of the null slots left over from the coalescing process.
|
||||
RemoveNullContents(contents);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveNullContents<T>(IList<T> contents)
|
||||
where T : class
|
||||
{
|
||||
if (contents is List<AIContent> contentsList)
|
||||
{
|
||||
_ = contentsList.RemoveAll(u => u is null);
|
||||
}
|
||||
else
|
||||
{
|
||||
int nextSlot = 0;
|
||||
int contentsCount = contents.Count;
|
||||
for (int i = 0; i < contentsCount; i++)
|
||||
{
|
||||
if (contents[i] is { } content)
|
||||
{
|
||||
contents[nextSlot++] = content;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = contentsCount - 1; i >= nextSlot; i--)
|
||||
{
|
||||
contents.RemoveAt(i);
|
||||
}
|
||||
|
||||
Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool ProcessUpdate(ChatResponseUpdate update, List<ChatMessage> messages)
|
||||
{
|
||||
// If there is no message created yet, or if the last update we saw had a different
|
||||
// identifying parts, create a new message.
|
||||
bool isNewMessage = true;
|
||||
if (messages.Count != 0)
|
||||
{
|
||||
var lastMessage = messages[messages.Count - 1];
|
||||
isNewMessage =
|
||||
NotEmptyOrEqual(update.AuthorName, lastMessage.AuthorName) ||
|
||||
NotEmptyOrEqual(update.MessageId, lastMessage.MessageId) ||
|
||||
NotNullOrEqual(update.Role, lastMessage.Role);
|
||||
}
|
||||
|
||||
// Get the message to target, either a new one or the last ones.
|
||||
ChatMessage message;
|
||||
if (isNewMessage)
|
||||
{
|
||||
message = new(ChatRole.Assistant, []);
|
||||
messages.Add(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
message = messages[messages.Count - 1];
|
||||
}
|
||||
|
||||
// Some members on ChatResponseUpdate map to members of ChatMessage.
|
||||
// Incorporate those into the latest message; in cases where the message
|
||||
// stores a single value, prefer the latest update's value over anything
|
||||
// stored in the message.
|
||||
|
||||
if (update.AuthorName is not null)
|
||||
{
|
||||
message.AuthorName = update.AuthorName;
|
||||
}
|
||||
|
||||
if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt))
|
||||
{
|
||||
message.CreatedAt = update.CreatedAt;
|
||||
}
|
||||
|
||||
if (update.Role is ChatRole role)
|
||||
{
|
||||
message.Role = role;
|
||||
}
|
||||
|
||||
if (update.MessageId is { Length: > 0 })
|
||||
{
|
||||
// Note that this must come after the message checks earlier, as they depend
|
||||
// on this value for change detection.
|
||||
message.MessageId = update.MessageId;
|
||||
}
|
||||
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
message.Contents.Add(content);
|
||||
}
|
||||
|
||||
return isNewMessage;
|
||||
}
|
||||
|
||||
/// <summary>Gets whether both strings are not null/empty and not the same as each other.</summary>
|
||||
private static bool NotEmptyOrEqual(string? s1, string? s2) =>
|
||||
s1 is { Length: > 0 } str1 && s2 is { Length: > 0 } str2 && str1 != str2;
|
||||
|
||||
/// <summary>Gets whether two roles are not null and not the same as each other.</summary>
|
||||
private static bool NotNullOrEqual(ChatRole? r1, ChatRole? r2) =>
|
||||
r1.HasValue && r2.HasValue && r1.Value != r2.Value;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class is apparently never instantiated
|
||||
internal sealed partial class MessageList : IComponent, IDisposable
|
||||
#pragma warning restore CA1812 // Internal class is apparently never instantiated
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private MessageSubscription _messageSubscription;
|
||||
private ResponseUpdateSubscription _responseSubscription;
|
||||
|
||||
[CascadingParameter] public MessageListContext MessageListContext { get; set; } = default!;
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
var previousContext = this.MessageListContext;
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (previousContext != null && this.MessageListContext != previousContext)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(MessageList)} does not support changing the {nameof(this.MessageListContext)} once it has been set.");
|
||||
}
|
||||
|
||||
// Subscribe to message updates and response updates for streaming
|
||||
this._messageSubscription = this.MessageListContext.AgentBoundaryContext.SubscribeToMessageChanges(this.ProcessUpdate);
|
||||
this._responseSubscription = this.MessageListContext.AgentBoundaryContext.SubscribeToResponseUpdates(this.ProcessUpdate);
|
||||
|
||||
// Initial render. This component will only render once since the only parameter is a cascading parameter and it's fixed.
|
||||
this._renderHandle.Render(this.Render);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ProcessUpdate()
|
||||
{
|
||||
// Scan all messages for function calls and results to track invocations.
|
||||
// This allows us to associate results with their calls when results arrive.
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.CompletedMessages)
|
||||
{
|
||||
this.ProcessMessageContents(message);
|
||||
}
|
||||
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.PendingMessages)
|
||||
{
|
||||
this.ProcessMessageContents(message);
|
||||
}
|
||||
|
||||
this._renderHandle.Render(this.Render);
|
||||
}
|
||||
|
||||
private void ProcessMessageContents(ChatMessage message)
|
||||
{
|
||||
foreach (var content in message.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent call)
|
||||
{
|
||||
this.MessageListContext.GetOrCreateInvocation(call);
|
||||
}
|
||||
else if (content is FunctionResultContent result)
|
||||
{
|
||||
this.MessageListContext.AssociateResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(RenderTreeBuilder builder)
|
||||
{
|
||||
// Track all render keys to detect duplicates
|
||||
var allRenderKeys = new HashSet<string?>();
|
||||
|
||||
// Open container div for message list with flex layout
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "messages-container");
|
||||
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.CompletedMessages)
|
||||
{
|
||||
var renderKey = GetUniqueRenderKey(message);
|
||||
|
||||
// Calling GetTemplate will stop template collection on the first message if it
|
||||
// was still ongoing.
|
||||
builder.OpenComponent<ContentBlock>(2);
|
||||
builder.SetKey(renderKey);
|
||||
builder.AddComponentParameter(3, "ChildContent", this.MessageListContext.GetTemplate(message));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.PendingMessages)
|
||||
{
|
||||
var renderKey = GetUniqueRenderKey(message);
|
||||
|
||||
builder.OpenComponent<ContentBlock>(4);
|
||||
builder.SetKey(renderKey);
|
||||
builder.AddComponentParameter(5, "ChildContent", this.MessageListContext.GetTemplate(message));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
// Close container div
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unique render key for a message.
|
||||
/// For tool result messages (role=tool), we use MessageId + CallId to ensure uniqueness
|
||||
/// because the AGUI protocol may reuse MessageId for multiple tool results in the same batch.
|
||||
/// </summary>
|
||||
private static string? GetUniqueRenderKey(ChatMessage message)
|
||||
{
|
||||
// For tool result messages, combine MessageId with CallId to ensure uniqueness
|
||||
// This works around a bug in the AGUI protocol where multiple tool results
|
||||
// can share the same MessageId when processed in the same update batch.
|
||||
if (message.Role == ChatRole.Tool)
|
||||
{
|
||||
var resultContent = message.Contents.OfType<FunctionResultContent>().FirstOrDefault();
|
||||
if (resultContent != null && !string.IsNullOrEmpty(resultContent.CallId))
|
||||
{
|
||||
return $"{message.MessageId}_{resultContent.CallId}";
|
||||
}
|
||||
}
|
||||
|
||||
return message.MessageId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
((IDisposable)this._messageSubscription).Dispose();
|
||||
((IDisposable)this._responseSubscription).Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public sealed partial class MessageListContext
|
||||
{
|
||||
private bool _collectingTemplates;
|
||||
|
||||
private readonly List<MessageTemplateBase> _templates = [];
|
||||
private readonly List<ContentTemplateBase> _contentTemplates = [];
|
||||
|
||||
// We compute a render fragment to render each message only once and cache it here.
|
||||
private readonly Dictionary<ChatMessage, RenderFragment> _templateCache = [];
|
||||
|
||||
// Track function invocations by CallId to associate calls with results.
|
||||
private readonly Dictionary<string, InvocationContext> _invocationMap = [];
|
||||
|
||||
public MessageListContext(IAgentBoundaryContext context)
|
||||
{
|
||||
this.AgentBoundaryContext = context;
|
||||
}
|
||||
|
||||
public IAgentBoundaryContext AgentBoundaryContext { get; }
|
||||
|
||||
public void BeginCollectingTemplates()
|
||||
{
|
||||
// This is triggered by the Messages component before rendering its children.
|
||||
// In this situation we are going to render again the MessageTemplates and
|
||||
// ContentTemplates and since we can't tell if they have changed we have to
|
||||
// recompute all the templates again.
|
||||
this._collectingTemplates = true;
|
||||
this._templates.Clear();
|
||||
this._contentTemplates.Clear();
|
||||
this._templateCache.Clear();
|
||||
}
|
||||
|
||||
public void RegisterTemplate(MessageTemplateBase template)
|
||||
{
|
||||
if (this._collectingTemplates)
|
||||
{
|
||||
this._templates.Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterContentTemplate(ContentTemplateBase template)
|
||||
{
|
||||
if (this._collectingTemplates)
|
||||
{
|
||||
this._contentTemplates.Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an invocation context for the given function call.
|
||||
/// </summary>
|
||||
/// <param name="call">The function call content.</param>
|
||||
/// <returns>The invocation context for this call.</returns>
|
||||
public InvocationContext GetOrCreateInvocation(FunctionCallContent call)
|
||||
{
|
||||
if (!this._invocationMap.TryGetValue(call.CallId, out var context))
|
||||
{
|
||||
context = new InvocationContext(call);
|
||||
this._invocationMap[call.CallId] = context;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Associates a function result with its corresponding call.
|
||||
/// </summary>
|
||||
/// <param name="result">The function result content.</param>
|
||||
public void AssociateResult(FunctionResultContent result)
|
||||
{
|
||||
if (this._invocationMap.TryGetValue(result.CallId, out var context))
|
||||
{
|
||||
context.SetResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an invocation context by call ID.
|
||||
/// </summary>
|
||||
/// <param name="callId">The call ID.</param>
|
||||
/// <returns>The invocation context, or null if not found.</returns>
|
||||
public InvocationContext? GetInvocation(string callId)
|
||||
{
|
||||
return this._invocationMap.TryGetValue(callId, out var context) ? context : null;
|
||||
}
|
||||
|
||||
internal RenderFragment GetTemplate(ChatMessage message)
|
||||
{
|
||||
// We are about to render the first message. If we were collecting templates, stop now.
|
||||
this._collectingTemplates = false;
|
||||
if (this._templateCache.TryGetValue(message, out var cachedTemplate))
|
||||
{
|
||||
return cachedTemplate;
|
||||
}
|
||||
|
||||
var messageContext = new MessageContext(message, this);
|
||||
foreach (var template in this._templates)
|
||||
{
|
||||
if (template.When(messageContext))
|
||||
{
|
||||
var chosen = template;
|
||||
messageContext.SetTemplate(chosen);
|
||||
// We ask the template to create a RenderFragment for the message.
|
||||
// The template will render a wrapper and use the messageContext to
|
||||
// render the contents.
|
||||
// The template might call back through the messageContext to get renderers for
|
||||
// contents if the message template doesn't override the full rendering or
|
||||
// if it doesn't define the rendering for a content type.
|
||||
var renderer = chosen.ChildContent(messageContext);
|
||||
this._templateCache[message] = renderer;
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"No message template found for message of type {message.Role}.");
|
||||
}
|
||||
|
||||
internal RenderFragment GetContentTemplate(AIContent content)
|
||||
{
|
||||
foreach (var template in this._contentTemplates)
|
||||
{
|
||||
var contentContext = new ContentContext(content);
|
||||
if (template.When(contentContext))
|
||||
{
|
||||
return template.ChildContent(contentContext);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"No content template found for content of type {content.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public abstract class MessageTemplateBase : IComponent
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public abstract bool When(MessageContext context);
|
||||
|
||||
[Parameter] public RenderFragment<MessageContext> ChildContent { get; set; } = (message) => builder => { };
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal RenderFragment? GetContentTemplate(AIContent content)
|
||||
{
|
||||
return this.Context.GetContentTemplate(content);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public partial class Messages : IComponent
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private RenderFragment? _renderContents;
|
||||
|
||||
private IAgentBoundaryContext? _context;
|
||||
private MessageListContext? _messageListContext;
|
||||
|
||||
[CascadingParameter] public IAgentBoundaryContext? AgentContext { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment MessageTemplates { get; set; } = builder =>
|
||||
{
|
||||
builder.OpenComponent<DefaultMessageTemplate>(0);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ContentTemplates { get; set; } = builder =>
|
||||
{
|
||||
builder.OpenComponent<TextTemplate>(0);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<FunctionCallTemplate>(1);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<FunctionResultTemplate>(2);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<ErrorTemplate>(3);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<DataContentTemplate>(4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
this._renderContents = this.RenderContents;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (this.AgentContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("Messages component must be used within an AgentBoundary.");
|
||||
}
|
||||
|
||||
if (this._context != null && this.AgentContext != this._context)
|
||||
{
|
||||
throw new InvalidOperationException("Messages component cannot change AgentBoundaryContext.");
|
||||
}
|
||||
|
||||
this._context = this.AgentContext;
|
||||
|
||||
if (this._messageListContext == null)
|
||||
{
|
||||
this._messageListContext = new MessageListContext(this.AgentContext);
|
||||
this.Render();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
this._renderHandle.Render(this.RenderCore);
|
||||
}
|
||||
|
||||
private void RenderCore(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<MessageListContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this._messageListContext);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", this._renderContents);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderContents(RenderTreeBuilder builder)
|
||||
{
|
||||
Debug.Assert(this._messageListContext != null);
|
||||
this._messageListContext.BeginCollectingTemplates();
|
||||
builder.AddContent(1, this.MessageTemplates);
|
||||
builder.AddContent(2, this.ContentTemplates);
|
||||
builder.OpenComponent<MessageList>(3);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
/* ==========================================================================
|
||||
AG-UI Components - Shared Styles
|
||||
These styles provide consistent appearance for AI components across demos.
|
||||
========================================================================== */
|
||||
|
||||
/* ==========================================================================
|
||||
Agent Input Component
|
||||
========================================================================== */
|
||||
|
||||
.agent-input {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-input textarea {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
resize: none;
|
||||
min-height: 44px;
|
||||
max-height: 200px;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.agent-input textarea:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.agent-input textarea::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.agent-input textarea:disabled {
|
||||
background-color: #f9fafb;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.agent-input .send-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-input .send-button:hover:not(:disabled) {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.agent-input .send-button:disabled {
|
||||
background-color: #d1d5db;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.agent-input .send-button svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Agent Suggestions Component
|
||||
========================================================================== */
|
||||
|
||||
.agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.suggestion-button:hover:not(:disabled) {
|
||||
background: #f0f0f0;
|
||||
border-color: #0078d4;
|
||||
color: #0078d4;
|
||||
}
|
||||
|
||||
.suggestion-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Loading Indicator - Triple Dot Animation
|
||||
========================================================================== */
|
||||
|
||||
.agent-loading-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agent-loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: agent-dot-bounce 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.agent-loading-dots span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.agent-loading-dots span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.agent-loading-dots span:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes agent-dot-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline loading indicator for use within messages */
|
||||
.agent-loading-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.agent-loading-inline span {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: #6b7280;
|
||||
border-radius: 50%;
|
||||
animation: agent-dot-bounce 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.agent-loading-inline span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.agent-loading-inline span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.agent-loading-inline span:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Processing State Overlay
|
||||
========================================================================== */
|
||||
|
||||
.agent-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-input-container.processing .agent-input textarea {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Message Styles
|
||||
========================================================================== */
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-assistant {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 80%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-user .message-content {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message-assistant .message-content {
|
||||
background-color: #f3f4f6;
|
||||
color: #1f2937;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Common Chat Layout Styles
|
||||
========================================================================== */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="AGUIDojoClient.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet @rendermode="@renderMode" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="@renderMode" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@code {
|
||||
private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Services
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject IBackgroundColorService BackgroundColorService
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Agentic Chat</PageTitle>
|
||||
|
||||
<div class="chat-layout" style="@GetBackgroundStyle()">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">AGUI WebChat</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversationAsync">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent">
|
||||
<div class="chat-content">
|
||||
<Messages />
|
||||
<AgentLoadingIndicator />
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Type your message..." />
|
||||
</div>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private string? _backgroundColor;
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Change background", new ChatMessage(ChatRole.User, "Change background to light blue")),
|
||||
new Suggestion("Generate sonnet")
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "agentic_chat";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("agentic-chat");
|
||||
BackgroundColorService.ColorChanged += OnColorChanged;
|
||||
}
|
||||
|
||||
private string GetBackgroundStyle()
|
||||
{
|
||||
return _backgroundColor != null ? $"background-color: {_backgroundColor}" : "";
|
||||
}
|
||||
|
||||
private async void OnColorChanged(object? sender, BackgroundColorChangedEventArgs e)
|
||||
{
|
||||
_backgroundColor = e.Color;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void ResetConversationAsync()
|
||||
{
|
||||
// Reset would need to be implemented in AgentBoundary
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BackgroundColorService.ColorChanged -= OnColorChanged;
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1rem 2rem 1.5rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.agentic-chat-demo {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
padding: 0 2rem 1.5rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Components.Shared
|
||||
@using System.Text.Json
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<PageTitle>Agentic Generative UI</PageTitle>
|
||||
|
||||
<div class="chat-layout">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">Agentic Generative UI</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversation">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent">
|
||||
<AgentState TState="Plan"
|
||||
CurrentState="@currentPlan"
|
||||
OnSnapshot="@DeserializePlan"
|
||||
OnDelta="@ApplyPlanDelta"
|
||||
CurrentStateChanged="@OnPlanChanged">
|
||||
<div class="chat-content">
|
||||
<Messages>
|
||||
<ContentTemplates>
|
||||
<TaskProgressTemplate />
|
||||
<TextTemplate />
|
||||
<FunctionCallTemplate />
|
||||
<FunctionResultTemplate />
|
||||
<ErrorTemplate />
|
||||
<DataContentTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
<AgentLoadingIndicator />
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Ask me to plan something..." />
|
||||
</div>
|
||||
</AgentState>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private Plan? currentPlan;
|
||||
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Simple plan", new ChatMessage(ChatRole.User, "Please build a plan to go to mars in 5 steps.")),
|
||||
new Suggestion("Complex plan", new ChatMessage(ChatRole.User, "Please build a plan to make pizza in 10 steps."))
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "agentic_generative_ui";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("agentic-generative-ui");
|
||||
}
|
||||
|
||||
private Plan? DeserializePlan(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<Plan>(data.Span);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Plan? ApplyPlanDelta(Plan? current, ReadOnlyMemory<byte> deltaData)
|
||||
{
|
||||
if (current is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var operations = JsonSerializer.Deserialize<List<JsonPatchOperation>>(deltaData.Span);
|
||||
if (operations is not null)
|
||||
{
|
||||
PlanPatcher.Apply(current, operations);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore deserialization errors
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private void OnPlanChanged(Plan? plan)
|
||||
{
|
||||
currentPlan = plan;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ResetConversation()
|
||||
{
|
||||
currentPlan = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
|
||||
::deep .agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
::deep .suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
::deep .suggestion-button:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #0078d4;
|
||||
color: #0078d4;
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON Patch operation (RFC 6902).
|
||||
/// </summary>
|
||||
public sealed class JsonPatchOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation to perform (e.g., "replace", "add", "remove").
|
||||
/// </summary>
|
||||
[JsonPropertyName("op")]
|
||||
public string Op { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON Pointer path to the target location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The value for the operation (used with "replace", "add", "test").
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a plan with multiple steps.
|
||||
/// </summary>
|
||||
public sealed class Plan
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of steps in the plan.
|
||||
/// </summary>
|
||||
[JsonPropertyName("steps")]
|
||||
public List<Step> Steps { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of completed steps.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int CompletedCount => this.Steps.Count(s => s.IsCompleted);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of steps.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int TotalCount => this.Steps.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether all steps are completed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsComplete => this.Steps.Count > 0 && this.Steps.All(s => s.IsCompleted);
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Applies JSON Patch operations to a Plan object.
|
||||
/// </summary>
|
||||
public static partial class PlanPatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a list of JSON Patch operations to the given plan.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to modify.</param>
|
||||
/// <param name="operations">The patch operations to apply.</param>
|
||||
public static void Apply(Plan plan, IEnumerable<JsonPatchOperation> operations)
|
||||
{
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
ApplyOperation(plan, operation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyOperation(Plan plan, JsonPatchOperation operation)
|
||||
{
|
||||
// Parse paths like "/steps/0/status" or "/steps/0/description"
|
||||
var match = StepPathRegex().Match(operation.Path);
|
||||
if (!match.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(match.Groups["index"].Value, out var index))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= plan.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var property = match.Groups["property"].Value;
|
||||
var step = plan.Steps[index];
|
||||
|
||||
if (string.Equals(operation.Op, "replace", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(property, "status", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
step.Status = GetStringValue(operation.Value) ?? step.Status;
|
||||
}
|
||||
else if (string.Equals(property, "description", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
step.Description = GetStringValue(operation.Value) ?? step.Description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetStringValue(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
string s => s,
|
||||
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
|
||||
_ => value?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^/steps/(?<index>\d+)/(?<property>\w+)$")]
|
||||
private static partial Regex StepPathRegex();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user