Compare commits

..
Author SHA1 Message Date
westey f109a5fc9a Fix formatting 2025-12-04 09:18:48 +00:00
westey c5f003498b Fix formatting issue 2025-12-04 07:17:51 +00:00
westeyandGitHub 6c8ef728fa Merge branch 'main' into contextual-function-provider-port 2025-12-03 12:18:48 +00:00
CopilotGitHubwestey-mcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
7965285808 .NET: Add serialization support for ContextualFunctionProvider (#2601)
* Initial plan

* Add serialization support for ContextualFunctionProvider

- Added constructor overload that accepts JsonElement serializedState
- Implemented Serialize method to persist recent messages
- Added ContextualFunctionProviderState class
- Registered state class with AgentJsonUtilities JsonContext
- Added comprehensive unit tests for serialization/deserialization

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Fix constructor parameter ordering and make default more explicit

- Reordered constructor parameters to maintain consistency (options before jsonSerializerOptions)
- Made default JsonElement explicit with default(JsonElement) in constructor chaining
- All tests continue to pass

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Improve XML documentation comments for clarity

- Updated jsonSerializerOptions parameter comments to clarify actual usage
- Made it clear that AgentJsonUtilities.DefaultOptions is used
- All tests continue to pass

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Change NumberOfRecentMessagesInContext to 2 to demonstrate truncation

- Updated test to use limit of 2 messages instead of 3
- Now test shows truncation in action (3 messages added, only 2 kept)
- Updated assertion to expect 2 messages in serialized state
- All tests pass

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
2025-12-03 12:18:19 +00:00
CopilotGitHubwestey-mcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
9008bb8cb7 Skip adding messages to context when invocation fails with exception (#2600)
* Initial plan

* Add exception check in InvokedAsync to skip adding messages when invocation fails

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Refactor InvokedAsync to use early return pattern

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
2025-12-03 11:56:29 +00:00
938961d2cc Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-27 18:16:21 +00:00
westey d9ce6ee292 Format files. 2025-11-27 17:33:37 +00:00
westey 2a3e41dbdd Port ContextualFunctionProvider from SK 2025-11-27 17:14:47 +00:00
918 changed files with 13415 additions and 60485 deletions
-8
View File
@@ -1,8 +0,0 @@
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.
-70
View File
@@ -1,70 +0,0 @@
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
@@ -1,51 +0,0 @@
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
-70
View File
@@ -1,70 +0,0 @@
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
@@ -28,18 +28,6 @@ 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: |
@@ -1,17 +0,0 @@
---
applyTo: "dotnet/src/Microsoft.Agents.AI.DurableTask/**,dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**"
---
# Durable Task area code instructions
The following guidelines apply to pull requests that modify files under
`dotnet/src/Microsoft.Agents.AI.DurableTask/**` or
`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**`:
## CHANGELOG.md
- Each pull request that modifies code should add just one bulleted entry to the `CHANGELOG.md` file containing a change title (usually the PR title) and a link to the PR itself.
- New PRs should be added to the top of the `CHANGELOG.md` file under a "## [Unreleased]" heading.
- If the PR is the first since the last release, the existing "## [Unreleased]" heading should be replaced with a "## v[X.Y.Z]" heading and the PRs since the last release should be added to the new "## [Unreleased]" heading.
- The style of new `CHANGELOG.md` entries should match the style of the other entries in the file.
- If the PR introduces a breaking change, the changelog entry should be prefixed with "[BREAKING]".
@@ -142,9 +142,9 @@ Replace these Semantic Kernel agent classes with their Agent Framework equivalen
|----------------------|----------------------------|-------------------|
| `IChatCompletionService` | `IChatClient` | Convert to `IChatClient` using `chatService.AsChatClient()` extensions |
| `ChatCompletionAgent` | `ChatClientAgent` | Remove `Kernel` parameter, add `IChatClient` parameter |
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | ⚠️ **Deprecated** - Use Responses API instead. <br> **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
| `AzureAIAgent` | `AIAgent` (via extension) | **New**: `PersistentAgentsClient.CreateAIAgent()` <br> **Existing**: `PersistentAgentsClient.GetAIAgent(agentId)` |
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient(modelId).CreateAIAgent()` |
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient().CreateAIAgent()` |
| `A2AAgent` | `AIAgent` (via extension) | Replace with `A2ACardResolver.GetAIAgentAsync()` |
| `BedrockAgent` | Not supported | Custom implementation required |
@@ -529,14 +529,14 @@ AIAgent agent = new OpenAIClient(apiKey)
.CreateAIAgent(instructions: instructions);
```
**OpenAI Assistants (New):** ⚠️ *Deprecated - Use Responses API instead*
**OpenAI Assistants (New):**
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetAssistantClient()
.CreateAIAgent(modelId, instructions: instructions);
```
**OpenAI Assistants (Existing):** ⚠️ *Deprecated - Use Responses API instead*
**OpenAI Assistants (Existing):**
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetAssistantClient()
@@ -562,20 +562,6 @@ AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
.GetAIAgentAsync(agentId);
```
**OpenAI Responses:** *(Recommended for OpenAI)*
```csharp
AIAgent agent = new OpenAIClient(apiKey)
.GetOpenAIResponseClient(modelId)
.CreateAIAgent(instructions: instructions);
```
**Azure OpenAI Responses:** *(Recommended for Azure OpenAI)*
```csharp
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.GetOpenAIResponseClient(deploymentName)
.CreateAIAgent(instructions: instructions);
```
**A2A:**
```csharp
A2ACardResolver resolver = new(new Uri(agentHost));
@@ -776,57 +762,35 @@ await foreach (var content in agent.InvokeAsync(userInput, thread))
**With this Agent Framework CodeInterpreter pattern:**
```csharp
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var result = await agent.RunAsync(userInput, thread);
Console.WriteLine(result);
// Get the CodeInterpreterToolCallContent (code input)
CodeInterpreterToolCallContent? toolCallContent = result.Messages
.SelectMany(m => m.Contents)
.OfType<CodeInterpreterToolCallContent>()
.FirstOrDefault();
// Extract chat response MEAI type via first level breaking glass
var chatResponse = result.RawRepresentation as ChatResponse;
if (toolCallContent?.Inputs is not null)
// Extract underlying SDK updates via second level breaking glass
var underlyingStreamingUpdates = chatResponse?.RawRepresentation as IEnumerable<object?> ?? [];
StringBuilder generatedCode = new();
foreach (object? underlyingUpdate in underlyingStreamingUpdates ?? [])
{
DataContent? codeInput = toolCallContent.Inputs.OfType<DataContent>().FirstOrDefault();
if (codeInput?.HasTopLevelMediaType("text") ?? false)
if (underlyingUpdate is RunStepDetailsUpdate stepDetailsUpdate && stepDetailsUpdate.CodeInterpreterInput is not null)
{
Console.WriteLine($"Code Input: {Encoding.UTF8.GetString(codeInput.Data.ToArray())}");
generatedCode.Append(stepDetailsUpdate.CodeInterpreterInput);
}
}
// Get the CodeInterpreterToolResultContent (code output)
CodeInterpreterToolResultContent? toolResultContent = result.Messages
.SelectMany(m => m.Contents)
.OfType<CodeInterpreterToolResultContent>()
.FirstOrDefault();
if (toolResultContent?.Outputs is not null)
if (!string.IsNullOrEmpty(generatedCode.ToString()))
{
TextContent? resultOutput = toolResultContent.Outputs.OfType<TextContent>().FirstOrDefault();
if (resultOutput is not null)
{
Console.WriteLine($"Code Tool Result: {resultOutput.Text}");
}
}
// Getting any annotations generated by the tool
foreach (AIAnnotation annotation in result.Messages
.SelectMany(m => m.Contents)
.SelectMany(c => c.Annotations ?? []))
{
Console.WriteLine($"Annotation: {annotation}");
Console.WriteLine($"\n# {chatResponse?.Messages[0].Role}:Generated Code:\n{generatedCode}");
}
```
**Functional differences:**
1. Code interpreter content is now available via MEAI abstractions - no breaking glass required
2. Use `CodeInterpreterToolCallContent` to access code inputs (the generated code)
3. Use `CodeInterpreterToolResultContent` to access code outputs (execution results)
4. Annotations are accessible via `AIAnnotation` on content items
1. Code interpreter output is separate from text content, not a metadata property
2. Access code via `RunStepDetailsUpdate.CodeInterpreterInput` instead of metadata
3. Use breaking glass pattern to access underlying SDK objects
4. Process text content and code interpreter output independently
</behavioral_changes>
#### Provider-Specific Options Configuration
@@ -839,7 +803,7 @@ var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
{
MaxOutputTokens = 8000,
// Breaking glass to access provider-specific options
RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions()
{
ReasoningOptions = new()
{
@@ -1016,8 +980,6 @@ AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential(
### 3. OpenAI Assistants Migration
> ⚠️ **DEPRECATION WARNING**: The OpenAI Assistants API has been deprecated. The Agent Framework extension methods for Assistants are marked as `[Obsolete]`. **Please use the Responses API instead** (see Section 6: OpenAI Responses Migration).
<configuration_changes>
**Remove Semantic Kernel Packages:**
```xml
@@ -1329,7 +1291,52 @@ var result = await agent.RunAsync(userInput, thread);
```
</api_changes>
### 8. Unsupported Providers (Require Custom Implementation)
### 8. A2A Migration
<configuration_changes>
**Remove Semantic Kernel Packages:**
```xml
<PackageReference Include="Microsoft.SemanticKernel.Agents.A2A" />
```
**Add Agent Framework Packages:**
```xml
<PackageReference Include="Microsoft.Agents.AI.A2A" />
```
</configuration_changes>
<api_changes>
**Replace this Semantic Kernel pattern:**
```csharp
using A2A;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.A2A;
using var httpClient = CreateHttpClient();
var client = new A2AClient(agentUrl, httpClient);
var cardResolver = new A2ACardResolver(url, httpClient);
var agentCard = await cardResolver.GetAgentCardAsync();
Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
var agent = new A2AAgent(client, agentCard);
```
**With this Agent Framework pattern:**
```csharp
using System;
using A2A;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.A2A;
// Initialize an A2ACardResolver to get an A2A agent card.
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent agent = await agentCardResolver.GetAIAgentAsync();
```
</api_changes>
### 9. Unsupported Providers (Require Custom Implementation)
<behavioral_changes>
#### BedrockAgent Migration
@@ -1500,7 +1507,7 @@ Console.WriteLine(result);
```
</behavioral_changes>
### 9. Function Invocation Filtering
### 10. Function Invocation Filtering
**Invocation Context**
@@ -1608,4 +1615,25 @@ var filteredAgent = originalAgent
.Build();
```
### 11. Function Invocation Contexts
**Invocation Context**
Semantic Kernel's `IAutoFunctionInvocationFilter` provides a `AutoFunctionInvocationContext` where Agent Framework provides `FunctionInvocationContext`
The property mapping guide from a `AutoFunctionInvocationContext` to a `FunctionInvocationContext` is as follows:
| Semantic Kernel | Agent Framework |
| --- | --- |
| RequestSequenceIndex | Iteration |
| FunctionSequenceIndex | FunctionCallIndex |
| ToolCallId | CallContent.CallId |
| ChatMessageContent | Messages[0] |
| ExecutionSettings | Options |
| ChatHistory | Messages |
| Function | Function |
| Kernel | N/A |
| Result | Use `return` from the delegate |
| Terminate | Terminate |
| CancellationToken | provided via argument to middleware delegate |
| Arguments | Arguments |
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
persist-credentials: false
+16 -22
View File
@@ -35,25 +35,19 @@ jobs:
contents: read
pull-requests: read
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- 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'
@@ -74,7 +68,7 @@ jobs:
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
persist-credentials: false
sparse-checkout: |
@@ -83,16 +77,6 @@ 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
@@ -139,7 +123,17 @@ 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: |
@@ -223,7 +217,7 @@ jobs:
# Generate test reports and check coverage
- name: Generate test reports
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.0
with:
reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
targetdir: "./TestResults/Reports"
@@ -231,7 +225,7 @@ jobs:
- name: Upload coverage report artifact
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
path: ./TestResults/Reports # Directory containing files to upload
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
+11 -50
View File
@@ -45,58 +45,19 @@ jobs:
labels.push("triage")
}
// 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 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")
}
// 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")
}
// 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")
}
// Add the labels to the issue (only if there are labels to add)
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04
# check out the latest version of the code
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
persist-credentials: false
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
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@v5
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -59,7 +59,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up python and install the project
id: python-setup
+4 -4
View File
@@ -28,7 +28,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -75,7 +75,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -135,7 +135,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- 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 --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
run: uv run poe azure-ai-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
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- 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@v6
- uses: actions/checkout@v5
- name: Download coverage report
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
run-id: ${{ github.event.workflow_run.id }}
@@ -39,7 +39,7 @@ jobs:
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@v1.2.0
uses: MishaKav/pytest-coverage-comment@v1.1.59
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
issue-number: ${{ env.PR_NUMBER }}
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
env:
UV_PYTHON: "3.10"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
# 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@v6
uses: actions/upload-artifact@v5
with:
path: |
python/python-coverage.xml
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+1 -1
View File
@@ -1,3 +1,3 @@
# Declarative Agents
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/).
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/).
+1 -1
View File
@@ -11,7 +11,7 @@ model:
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_API_KEY
key: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
+1 -1
View File
@@ -11,7 +11,7 @@ model:
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_API_KEY
key: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
+5 -5
View File
@@ -10,19 +10,19 @@ model:
temperature: 0.9
topP: 0.95
connection:
kind: key
apiKey: =Env.OPENAI_APIKEY
kind: ApiKey
key: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
kind: string
type: string
required: true
description: The language of the answer.
answer:
kind: string
type: string
required: true
description: The answer text.
type:
kind: string
type: string
required: true
description: The type of the response.
+4 -4
View File
@@ -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/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) |
| 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) |
| 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) |
@@ -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 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) |
| 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) |
| 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/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. |
| 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. |
| 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). |
@@ -1,147 +0,0 @@
# 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.
-1
View File
@@ -209,7 +209,6 @@ 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
+2 -1
View File
@@ -3,7 +3,8 @@
<!-- Default properties inherited by all projects. Projects can override. -->
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>10.0-all</AnalysisLevel>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
+27 -29
View File
@@ -7,46 +7,45 @@
</PropertyGroup>
<PropertyGroup>
<!-- Aspire -->
<AspireAppHostSdkVersion>13.0.2</AspireAppHostSdkVersion>
<AspireAppHostSdkVersion>13.0.1</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.0.1" />
<PackageVersion Include="Anthropic" Version="11.0.0" />
<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" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<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.Agents.Persistent" Version="1.2.0-beta.7" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.9.0" />
<PackageVersion Include="Google.GenAI" Version="0.6.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.1" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<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.1" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
<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.1" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.1" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
@@ -61,9 +60,10 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
<PackageVersion Include="Microsoft.Extensions.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.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" />
@@ -71,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.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<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.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<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" />
@@ -100,10 +100,10 @@
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
<!-- Inference SDKs -->
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.6" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.8.0" />
<PackageVersion Include="OpenAI" Version="2.7.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<!-- Workflows -->
@@ -112,21 +112,19 @@
<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.19.1" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.19.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.19.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.19.0" />
<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" />
<!-- 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.13.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
<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.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" />
+1 -35
View File
@@ -33,14 +33,6 @@
<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" />
<Project Path="samples/AzureFunctions/09_Workflow/09_Workflow.csproj" />
<Project Path="samples/AzureFunctions/10_WorkflowConcurrent/10_WorkflowConcurrent.csproj" />
<Project Path="samples/AzureFunctions/11_WorkflowSharedState/11_WorkflowSharedState.csproj" />
<Project Path="samples/AzureFunctions/12_ConditionalEdges/12_ConditionalEdges.csproj" />
</Folder>
<Folder Name="/Samples/DurableWorkflows/">
<Project Path="samples/DurableWorkflows/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -92,29 +84,6 @@
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/">
<File Path="samples/GettingStarted/AGUI/README.md" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step01_GettingStarted/">
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step02_BackendTools/">
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step03_FrontendTools/">
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step04_HumanInLoop/">
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step05_StateManagement/">
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/DevUI/">
<File Path="samples/GettingStarted/DevUI/README.md" />
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
@@ -135,9 +104,6 @@
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
<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/">
@@ -207,7 +173,7 @@
<Project Path="samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
<File Path="../workflow-samples/CustomerSupport.yaml" />
<File Path="../workflow-samples/ConfirmInput.yaml" />
<File Path="../workflow-samples/DeepResearch.yaml" />
<File Path="../workflow-samples/Marketing.yaml" />
<File Path="../workflow-samples/MathChat.yaml" />
-31
View File
@@ -1,31 +0,0 @@
{
"solution": {
"path": "agent-framework-dotnet.slnx",
"projects": [
"src\\Microsoft.Agents.AI.A2A\\Microsoft.Agents.AI.A2A.csproj",
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
"src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj",
"src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
"src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj"
]
}
}
-4
View File
@@ -3,14 +3,10 @@
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="LocalNugetSource" value="C:\LocalNugetSource" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="LocalNugetSource">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260108.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260108.1</PackageVersion>
<GitTag>1.0.0-preview.260108.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251125.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251125.1</PackageVersion>
<GitTag>1.0.0-preview.251125.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
-1
View File
@@ -1,7 +1,6 @@
# Suppressing errors for Sample projects under dotnet/samples folder
[*.cs]
dotnet_diagnostic.CA1716.severity = none # Add summary to documentation comment.
dotnet_diagnostic.CA1873.severity = none # Evaluation of logging arguments may be expensive
dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on object before all references to it are out of scope
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
@@ -14,6 +14,11 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
@@ -4,7 +4,6 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using OpenAI;
using OpenAI.Chat;
namespace A2A;
@@ -6,7 +6,6 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
namespace A2AServer;
@@ -1,10 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using AGUIDojoServer.AgenticUI;
using AGUIDojoServer.BackendToolRendering;
using AGUIDojoServer.PredictiveStateUpdates;
using AGUIDojoServer.SharedState;
namespace AGUIDojoServer;
@@ -12,12 +8,4 @@ namespace AGUIDojoServer;
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(Plan))]
[JsonSerializable(typeof(Step))]
[JsonSerializable(typeof(StepStatus))]
[JsonSerializable(typeof(StepStatus?))]
[JsonSerializable(typeof(JsonPatchOperation))]
[JsonSerializable(typeof(List<JsonPatchOperation>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(DocumentState))]
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
@@ -1,52 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
namespace AGUIDojoServer.AgenticUI;
internal static class AgenticPlanningTools
{
[Description("Create a plan with multiple steps.")]
public static Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
{
return new Plan
{
Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
};
}
[Description("Update a step in the plan with new description or status.")]
public static async Task<List<JsonPatchOperation>> UpdatePlanStepAsync(
[Description("The index of the step to update.")] int index,
[Description("The new description for the step (optional).")] string? description = null,
[Description("The new status for the step (optional).")] StepStatus? status = null)
{
var changes = new List<JsonPatchOperation>();
if (description is not null)
{
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/description",
Value = description
});
}
if (status.HasValue)
{
// Status must be lowercase to match AG-UI frontend expectations: "pending" or "completed"
string statusValue = status.Value == StepStatus.Pending ? "pending" : "completed";
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/status",
Value = statusValue
});
}
await Task.Delay(1000);
return changes;
}
}
@@ -1,88 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer.AgenticUI;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateAgenticUI")]
internal sealed class AgenticUIAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public AgenticUIAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track function calls that should trigger state events
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Process contents: track function calls and emit state events for results
List<AIContent> stateEventsToEmit = new();
foreach (var content in update.Contents)
{
if (content is FunctionCallContent callContent)
{
if (callContent.Name == "create_plan" || callContent.Name == "update_plan_step")
{
trackedFunctionCalls[callContent.CallId] = callContent;
break;
}
}
else if (content is FunctionResultContent resultContent)
{
// Check if this result matches a tracked function call
if (trackedFunctionCalls.TryGetValue(resultContent.CallId, out var matchedCall))
{
var bytes = JsonSerializer.SerializeToUtf8Bytes((JsonElement)resultContent.Result!, this._jsonSerializerOptions);
// Determine event type based on the function name
if (matchedCall.Name == "create_plan")
{
stateEventsToEmit.Add(new DataContent(bytes, "application/json"));
}
else if (matchedCall.Name == "update_plan_step")
{
stateEventsToEmit.Add(new DataContent(bytes, "application/json-patch+json"));
}
}
}
}
yield return update;
yield return new AgentRunResponseUpdate(
new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
{
MessageId = "delta_" + Guid.NewGuid().ToString("N"),
CreatedAt = update.CreatedAt,
ResponseId = update.ResponseId,
AuthorName = update.AuthorName,
Role = update.Role,
ContinuationToken = update.ContinuationToken,
AdditionalProperties = update.AdditionalProperties,
})
{
AgentId = update.AgentId
};
}
}
}
@@ -1,20 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
internal sealed class JsonPatchOperation
{
[JsonPropertyName("op")]
public required string Op { get; set; }
[JsonPropertyName("path")]
public required string Path { get; set; }
[JsonPropertyName("value")]
public object? Value { get; set; }
[JsonPropertyName("from")]
public string? From { get; set; }
}
@@ -1,11 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
internal sealed class Plan
{
[JsonPropertyName("steps")]
public List<Step> Steps { get; set; } = [];
}
@@ -1,14 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
internal sealed class Step
{
[JsonPropertyName("description")]
public required string Description { get; set; }
[JsonPropertyName("status")]
public StepStatus Status { get; set; } = StepStatus.Pending;
}
@@ -1,12 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.AgenticUI;
[JsonConverter(typeof(JsonStringEnumConverter<StepStatus>))]
internal enum StepStatus
{
Pending,
Completed
}
@@ -2,10 +2,6 @@
using System.ComponentModel;
using System.Text.Json;
using AGUIDojoServer.AgenticUI;
using AGUIDojoServer.BackendToolRendering;
using AGUIDojoServer.PredictiveStateUpdates;
using AGUIDojoServer.SharedState;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -70,46 +66,13 @@ internal static class ChatClientAgentFactory
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
}
public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
public static ChatClientAgent CreateAgenticUI()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions
{
Name = "AgenticUIAgent",
Description = "An agent that generates agentic user interfaces using Azure OpenAI",
ChatOptions = new ChatOptions
{
Instructions = """
When planning use tools only, without any other messages.
IMPORTANT:
- Use the `create_plan` tool to set the initial state of the steps
- Use the `update_plan_step` tool to update the status of each step
- Do NOT repeat the plan or summarise it in a message
- Do NOT confirm the creation or updates in a message
- Do NOT ask the user for additional information or next steps
- Do NOT leave a plan hanging, always complete the plan via `update_plan_step` if one is ongoing.
- Continue calling update_plan_step until all steps are marked as completed.
Only one plan can be active at a time, so do not call the `create_plan` tool
again until all the steps in current plan are completed.
""",
Tools = [
AIFunctionFactory.Create(
AgenticPlanningTools.CreatePlan,
name: "create_plan",
description: "Create a plan with multiple steps.",
AGUIDojoServerSerializerContext.Default.Options),
AIFunctionFactory.Create(
AgenticPlanningTools.UpdatePlanStepAsync,
name: "update_plan_step",
description: "Update a step in the plan with new description or status.",
AGUIDojoServerSerializerContext.Default.Options)
],
AllowMultipleToolCalls = false
}
});
return new AgenticUIAgent(baseAgent, options);
return chatClient.AsIChatClient().CreateAIAgent(
name: "AgenticUIAgent",
description: "An agent that generates agentic user interfaces using Azure OpenAI");
}
public static AIAgent CreateSharedState(JsonSerializerOptions options)
@@ -123,44 +86,6 @@ internal static class ChatClientAgentFactory
return new SharedStateAgent(baseAgent, options);
}
public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options)
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions
{
Name = "PredictiveStateUpdatesAgent",
Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
ChatOptions = new ChatOptions
{
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
- You can use all kinds of markdown (headings, lists, bold, etc.)
- However, do NOT use italic or strike-through formatting
- You MUST write the full document, even when changing only a few words
- 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 = [
AIFunctionFactory.Create(
WriteDocument,
name: "write_document",
description: "Write a document. Use markdown formatting to format the document.",
AGUIDojoServerSerializerContext.Default.Options)
]
}
});
return new PredictiveStateUpdatesAgent(baseAgent, options);
}
[Description("Get the weather for a given location.")]
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
{
@@ -170,11 +95,4 @@ internal static class ChatClientAgentFactory
WindSpeed = 10,
FeelsLike = 25
};
[Description("Write a document in markdown format.")]
private static string WriteDocument([Description("The document content to write.")] string document)
{
// Simply return success - the document is tracked via state updates
return "Document written successfully";
}
}
@@ -2,7 +2,7 @@
using System.Text.Json.Serialization;
namespace AGUIDojoServer.SharedState;
namespace AGUIDojoServer;
internal sealed class Ingredient
{
@@ -1,11 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AGUIDojoServer.PredictiveStateUpdates;
internal sealed class DocumentState
{
[JsonPropertyName("document")]
public string Document { get; set; } = string.Empty;
}
@@ -1,104 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer.PredictiveStateUpdates;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreatePredictiveStateUpdates")]
internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
private const int ChunkSize = 10; // Characters per chunk for streaming effect
public PredictiveStateUpdatesAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track the last emitted document state to avoid duplicates
string? lastEmittedDocument = null;
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Check if we're seeing a write_document tool call and emit predictive state
bool hasToolCall = false;
string? documentContent = null;
foreach (var content in update.Contents)
{
if (content is FunctionCallContent callContent && callContent.Name == "write_document")
{
hasToolCall = true;
// Try to extract the document argument directly from the dictionary
if (callContent.Arguments?.TryGetValue("document", out var documentValue) == true)
{
documentContent = documentValue?.ToString();
}
}
}
// Always yield the original update first
yield return update;
// If we got a complete tool call with document content, "fake" stream it in chunks
if (hasToolCall && documentContent != null && documentContent != lastEmittedDocument)
{
// Chunk the document content and emit progressive state updates
int startIndex = 0;
if (lastEmittedDocument != null && documentContent.StartsWith(lastEmittedDocument, StringComparison.Ordinal))
{
// Only stream the new portion that was added
startIndex = lastEmittedDocument.Length;
}
// Stream the document in chunks
for (int i = startIndex; i < documentContent.Length; i += ChunkSize)
{
int length = Math.Min(ChunkSize, documentContent.Length - i);
string chunk = documentContent.Substring(0, i + length);
// Prepare predictive state update as DataContent
var stateUpdate = new DocumentState { Document = chunk };
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateUpdate,
this._jsonSerializerOptions.GetTypeInfo(typeof(DocumentState)));
yield return new AgentRunResponseUpdate(
new ChatResponseUpdate(role: ChatRole.Assistant, [new DataContent(stateBytes, "application/json")])
{
MessageId = "snapshot" + Guid.NewGuid().ToString("N"),
CreatedAt = update.CreatedAt,
ResponseId = update.ResponseId,
AdditionalProperties = update.AdditionalProperties,
AuthorName = update.AuthorName,
ContinuationToken = update.ContinuationToken,
})
{
AgentId = update.AgentId
};
// Small delay to simulate streaming
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
lastEmittedDocument = documentContent;
}
}
}
}
@@ -35,13 +35,11 @@ app.MapAGUI("/human_in_the_loop", ChatClientAgentFactory.CreateHumanInTheLoop())
app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI());
app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI());
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI(jsonOptions.Value.SerializerOptions));
app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));
app.MapAGUI("/predictive_state_updates", ChatClientAgentFactory.CreatePredictiveStateUpdates(jsonOptions.Value.SerializerOptions));
await app.RunAsync();
public partial class Program;
@@ -2,7 +2,7 @@
using System.Text.Json.Serialization;
namespace AGUIDojoServer.SharedState;
namespace AGUIDojoServer;
internal sealed class Recipe
{
@@ -2,7 +2,7 @@
using System.Text.Json.Serialization;
namespace AGUIDojoServer.SharedState;
namespace AGUIDojoServer;
#pragma warning disable CA1812 // Used for the JsonSchema response format
internal sealed class RecipeResponse
@@ -6,7 +6,7 @@ using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AGUIDojoServer.SharedState;
namespace AGUIDojoServer;
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")]
internal sealed class SharedStateAgent : DelegatingAIAgent
@@ -19,12 +19,12 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -2,7 +2,7 @@
using System.Text.Json.Serialization;
namespace AGUIDojoServer.BackendToolRendering;
namespace AGUIDojoServer;
internal sealed class WeatherInfo
{
@@ -6,7 +6,7 @@ using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using OpenAI;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
</ItemGroup>
</Project>
@@ -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="AGUIWebChatClient.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);
}
@@ -1 +0,0 @@
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
@@ -1,89 +0,0 @@
/* Used under CC0 license */
.lds-ellipsis {
color: #666;
animation: fade-in 1s;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.lds-ellipsis,
.lds-ellipsis div {
box-sizing: border-box;
}
.lds-ellipsis {
margin: auto;
display: block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ellipsis div {
position: absolute;
top: 33.33333px;
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
@@ -1,9 +0,0 @@
@inherits LayoutComponentBase
@Body
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">đź—™</span>
</div>
@@ -1,20 +0,0 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -1,94 +0,0 @@
@page "/"
@using System.ComponentModel
@inject IChatClient ChatClient
@inject NavigationManager Nav
@implements IDisposable
<PageTitle>Chat</PageTitle>
<ChatHeader OnNewChat="@ResetConversationAsync" />
<ChatMessageList Messages="@messages" InProgressMessage="@currentResponseMessage">
<NoMessagesContent>
<div>Ask the assistant a question to start a conversation.</div>
</NoMessagesContent>
</ChatMessageList>
<div class="chat-container">
<ChatSuggestions OnSelected="@AddUserMessageAsync" @ref="@chatSuggestions" />
<ChatInput OnSend="@AddUserMessageAsync" @ref="@chatInput" />
</div>
@code {
private const string SystemPrompt = @"
You are a helpful assistant.
";
private int statefulMessageCount;
private readonly ChatOptions chatOptions = new();
private readonly List<ChatMessage> messages = new();
private CancellationTokenSource? currentResponseCancellation;
private ChatMessage? currentResponseMessage;
private ChatInput? chatInput;
private ChatSuggestions? chatSuggestions;
protected override void OnInitialized()
{
statefulMessageCount = 0;
messages.Add(new(ChatRole.System, SystemPrompt));
}
private async Task AddUserMessageAsync(ChatMessage userMessage)
{
CancelAnyCurrentResponse();
// Add the user message to the conversation
messages.Add(userMessage);
chatSuggestions?.Clear();
await chatInput!.FocusAsync();
// Stream and display a new response from the IChatClient
var responseText = new TextContent("");
currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
StateHasChanged();
currentResponseCancellation = new();
await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token))
{
messages.AddMessages(update, filter: c => c is not TextContent);
responseText.Text += update.Text;
chatOptions.ConversationId = update.ConversationId;
ChatMessageItem.NotifyChanged(currentResponseMessage);
}
// Store the final response in the conversation, and begin getting suggestions
messages.Add(currentResponseMessage!);
statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0;
currentResponseMessage = null;
chatSuggestions?.Update(messages);
}
private void CancelAnyCurrentResponse()
{
// If a response was cancelled while streaming, include it in the conversation so it's not lost
if (currentResponseMessage is not null)
{
messages.Add(currentResponseMessage);
}
currentResponseCancellation?.Cancel();
currentResponseMessage = null;
}
private async Task ResetConversationAsync()
{
CancelAnyCurrentResponse();
messages.Clear();
messages.Add(new(ChatRole.System, SystemPrompt));
chatOptions.ConversationId = null;
statefulMessageCount = 0;
chatSuggestions?.Clear();
await chatInput!.FocusAsync();
}
public void Dispose()
=> currentResponseCancellation?.Cancel();
}
@@ -1,11 +0,0 @@
.chat-container {
position: sticky;
bottom: 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 1.5rem;
border-top-width: 1px;
background-color: #F3F4F6;
border-color: #E5E7EB;
}
@@ -1,38 +0,0 @@
@using System.Web
@if (!string.IsNullOrWhiteSpace(viewerUrl))
{
<a href="@viewerUrl" target="_blank" class="citation">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
<div class="citation-content">
<div class="citation-file">@File</div>
<div>@Quote</div>
</div>
</a>
}
@code {
[Parameter]
public required string File { get; set; }
[Parameter]
public int? PageNumber { get; set; }
[Parameter]
public required string Quote { get; set; }
private string? viewerUrl;
protected override void OnParametersSet()
{
viewerUrl = null;
// If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here
if (File.EndsWith(".pdf"))
{
var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\'');
viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true";
}
}
}
@@ -1,37 +0,0 @@
.citation {
display: inline-flex;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 1rem;
margin-right: 1rem;
border-bottom: 2px solid #a770de;
gap: 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
background-color: #ffffff;
}
.citation[href]:hover {
outline: 1px solid #865cb1;
}
.citation svg {
width: 1.5rem;
height: 1.5rem;
}
.citation:active {
background-color: rgba(0,0,0,0.05);
}
.citation-content {
display: flex;
flex-direction: column;
}
.citation-file {
font-weight: 600;
}
@@ -1,17 +0,0 @@
<div class="chat-header-container main-background-gradient">
<div class="chat-header-controls page-width">
<button class="btn-default" @onclick="@OnNewChat">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="new-chat-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
New chat
</button>
</div>
<h1 class="page-width">AGUI WebChat</h1>
</div>
@code {
[Parameter]
public EventCallback OnNewChat { get; set; }
}
@@ -1,25 +0,0 @@
.chat-header-container {
top: 0;
padding: 1.5rem;
}
.chat-header-controls {
margin-bottom: 1.5rem;
}
h1 {
overflow: hidden;
text-overflow: ellipsis;
}
.new-chat-icon {
width: 1.25rem;
height: 1.25rem;
color: rgb(55, 65, 81);
}
@media (min-width: 768px) {
.chat-header-container {
position: sticky;
}
}
@@ -1,51 +0,0 @@
@inject IJSRuntime JS
<EditForm Model="@this" OnValidSubmit="@SendMessageAsync">
<label class="input-box page-width">
<textarea @ref="@textArea" @bind="@messageText" placeholder="Type your message..." rows="1"></textarea>
<div class="tools">
<button type="submit" title="Send" class="send-button">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="tool-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>
</button>
</div>
</label>
</EditForm>
@code {
private ElementReference textArea;
private string? messageText;
[Parameter]
public EventCallback<ChatMessage> OnSend { get; set; }
public ValueTask FocusAsync()
=> textArea.FocusAsync();
private async Task SendMessageAsync()
{
if (messageText is { Length: > 0 } text)
{
messageText = null;
await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text));
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Chat/ChatInput.razor.js");
await module.InvokeVoidAsync("init", textArea);
await module.DisposeAsync();
}
catch (JSDisconnectedException)
{
}
}
}
}
@@ -1,57 +0,0 @@
.input-box {
display: flex;
flex-direction: column;
background: white;
border: 1px solid rgb(229, 231, 235);
border-radius: 8px;
padding: 0.5rem 0.75rem;
margin-top: 0.75rem;
}
.input-box:focus-within {
outline: 2px solid #4152d5;
}
textarea {
resize: none;
border: none;
outline: none;
flex-grow: 1;
}
textarea:placeholder-shown + .tools {
--send-button-color: #aaa;
}
.tools {
display: flex;
margin-top: 1rem;
align-items: center;
}
.tool-icon {
width: 1.25rem;
height: 1.25rem;
}
.send-button {
color: var(--send-button-color);
margin-left: auto;
}
.send-button:hover {
color: black;
}
.attach {
background-color: white;
border-style: dashed;
color: #888;
border-color: #888;
padding: 3px 8px;
}
.attach:hover {
background-color: #f0f0f0;
color: black;
}
@@ -1,43 +0,0 @@
export function init(elem) {
elem.focus();
// Auto-resize whenever the user types or if the value is set programmatically
elem.addEventListener('input', () => resizeToFit(elem));
afterPropertyWritten(elem, 'value', () => resizeToFit(elem));
// Auto-submit the form on 'enter' keypress
elem.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
elem.dispatchEvent(new CustomEvent('change', { bubbles: true }));
elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true }));
}
});
}
function resizeToFit(elem) {
const lineHeight = parseFloat(getComputedStyle(elem).lineHeight);
elem.rows = 1;
const numLines = Math.ceil(elem.scrollHeight / lineHeight);
elem.rows = Math.min(5, Math.max(1, numLines));
}
function afterPropertyWritten(target, propName, callback) {
const descriptor = getPropertyDescriptor(target, propName);
Object.defineProperty(target, propName, {
get: function () {
return descriptor.get.apply(this, arguments);
},
set: function () {
const result = descriptor.set.apply(this, arguments);
callback();
return result;
}
});
}
function getPropertyDescriptor(target, propertyName) {
return Object.getOwnPropertyDescriptor(target, propertyName)
|| getPropertyDescriptor(Object.getPrototypeOf(target), propertyName);
}
@@ -1,73 +0,0 @@
@using System.Runtime.CompilerServices
@using System.Text.RegularExpressions
@using System.Linq
@if (Message.Role == ChatRole.User)
{
<div class="user-message">
@Message.Text
</div>
}
else if (Message.Role == ChatRole.Assistant)
{
foreach (var content in Message.Contents)
{
if (content is TextContent { Text: { Length: > 0 } text })
{
<div class="assistant-message">
<div>
<div class="assistant-message-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
</svg>
</div>
</div>
<div class="assistant-message-header">Assistant</div>
<div class="assistant-message-text">
<div>@((MarkupString)text)</div>
</div>
</div>
}
else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true)
{
<div class="assistant-search">
<div class="assistant-search-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</div>
<div class="assistant-search-content">
Searching:
<span class="assistant-search-phrase">@searchPhrase</span>
@if (fcc.Arguments?.TryGetValue("filenameFilter", out var filenameObj) is true && filenameObj is string filename && !string.IsNullOrEmpty(filename))
{
<text> in </text><span class="assistant-search-phrase">@filename</span>
}
</div>
</div>
}
}
}
@code {
private static readonly ConditionalWeakTable<ChatMessage, ChatMessageItem> SubscribersLookup = new();
[Parameter, EditorRequired]
public required ChatMessage Message { get; set; }
[Parameter]
public bool InProgress { get; set;}
protected override void OnInitialized()
{
SubscribersLookup.AddOrUpdate(Message, this);
}
public static void NotifyChanged(ChatMessage source)
{
if (SubscribersLookup.TryGetValue(source, out var subscriber))
{
subscriber.StateHasChanged();
}
}
}
@@ -1,67 +0,0 @@
.user-message {
background: rgb(182 215 232);
align-self: flex-end;
min-width: 25%;
max-width: calc(100% - 5rem);
padding: 0.5rem 1.25rem;
border-radius: 0.25rem;
color: #1F2937;
white-space: pre-wrap;
}
.assistant-message, .assistant-search {
display: grid;
grid-template-rows: min-content;
grid-template-columns: 2rem minmax(0, 1fr);
gap: 0.25rem;
}
.assistant-message-header {
font-weight: 600;
}
.assistant-message-text {
grid-column-start: 2;
}
.assistant-message-icon {
display: flex;
justify-content: center;
align-items: center;
border-radius: 9999px;
width: 1.5rem;
height: 1.5rem;
color: #ffffff;
background: #9b72ce;
}
.assistant-message-icon svg {
width: 1rem;
height: 1rem;
}
.assistant-search {
font-size: 0.875rem;
line-height: 1.25rem;
}
.assistant-search-icon {
display: flex;
justify-content: center;
align-items: center;
width: 1.5rem;
height: 1.5rem;
}
.assistant-search-icon svg {
width: 1rem;
height: 1rem;
}
.assistant-search-content {
align-content: center;
}
.assistant-search-phrase {
font-weight: 600;
}
@@ -1,42 +0,0 @@
@inject IJSRuntime JS
<div class="message-list-container">
<chat-messages class="page-width message-list" in-progress="@(InProgressMessage is not null)">
@foreach (var message in Messages)
{
<ChatMessageItem @key="@message" Message="@message" />
}
@if (InProgressMessage is not null)
{
<ChatMessageItem Message="@InProgressMessage" InProgress="true" />
<LoadingSpinner />
}
else if (IsEmpty)
{
<div class="no-messages">@NoMessagesContent</div>
}
</chat-messages>
</div>
@code {
[Parameter]
public required IEnumerable<ChatMessage> Messages { get; set; }
[Parameter]
public ChatMessage? InProgressMessage { get; set; }
[Parameter]
public RenderFragment? NoMessagesContent { get; set; }
private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text));
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Activates the auto-scrolling behavior
await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js");
}
}
}
@@ -1,22 +0,0 @@
.message-list-container {
margin: 2rem 1.5rem;
flex-grow: 1;
}
.message-list {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.no-messages {
text-align: center;
font-size: 1.25rem;
color: #999;
margin-top: calc(40vh - 18rem);
}
chat-messages > ::deep div:last-of-type {
/* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */
margin-bottom: 2rem;
}
@@ -1,34 +0,0 @@
// The following logic provides auto-scroll behavior for the chat messages list.
// If you don't want that behavior, you can simply not load this module.
window.customElements.define('chat-messages', class ChatMessages extends HTMLElement {
static _isFirstAutoScroll = true;
connectedCallback() {
this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations));
this._observer.observe(this, { childList: true, attributes: true });
}
disconnectedCallback() {
this._observer.disconnect();
}
_scheduleAutoScroll(mutations) {
// Debounce the calls in case multiple DOM updates occur together
cancelAnimationFrame(this._nextAutoScroll);
this._nextAutoScroll = requestAnimationFrame(() => {
const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message')));
const elem = this.lastElementChild;
if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) {
elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' });
ChatMessages._isFirstAutoScroll = false;
}
});
}
_elemIsNearScrollBoundary(elem, threshold) {
const maxScrollPos = document.body.scrollHeight - window.innerHeight;
const remainingScrollDistance = maxScrollPos - window.scrollY;
return remainingScrollDistance < elem.offsetHeight + threshold;
}
});
@@ -1,78 +0,0 @@
@inject IChatClient ChatClient
@if (suggestions is not null)
{
<div class="page-width suggestions">
@foreach (var suggestion in suggestions)
{
<button class="btn-subtle" @onclick="@(() => AddSuggestionAsync(suggestion))">
@suggestion
</button>
}
</div>
}
@code {
private static string Prompt = @"
Suggest up to 3 follow-up questions that I could ask you to help me complete my task.
Each suggestion must be a complete sentence, maximum 6 words.
Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message,
for example 'How do I do that?' or 'Explain ...'.
If there are no suggestions, reply with an empty list.
";
private string[]? suggestions;
private CancellationTokenSource? cancellation;
[Parameter]
public EventCallback<ChatMessage> OnSelected { get; set; }
public void Clear()
{
suggestions = null;
cancellation?.Cancel();
}
public void Update(IReadOnlyList<ChatMessage> messages)
{
// Runs in the background and handles its own cancellation/errors
_ = UpdateSuggestionsAsync(messages);
}
private async Task UpdateSuggestionsAsync(IReadOnlyList<ChatMessage> messages)
{
cancellation?.Cancel();
cancellation = new CancellationTokenSource();
try
{
var response = await ChatClient.GetResponseAsync<string[]>(
[.. ReduceMessages(messages), new(ChatRole.User, Prompt)],
cancellationToken: cancellation.Token);
if (!response.TryGetResult(out suggestions))
{
suggestions = null;
}
StateHasChanged();
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
await DispatchExceptionAsync(ex);
}
}
private async Task AddSuggestionAsync(string text)
{
await OnSelected.InvokeAsync(new(ChatRole.User, text));
}
private IEnumerable<ChatMessage> ReduceMessages(IReadOnlyList<ChatMessage> messages)
{
// Get any leading system messages, plus up to 5 user/assistant messages
// This should be enough context to generate suggestions without unnecessarily resending entire conversations when long
var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System);
var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5);
return systemMessages.Concat(otherMessages);
}
}
@@ -1,9 +0,0 @@
.suggestions {
text-align: right;
white-space: nowrap;
gap: 0.5rem;
justify-content: flex-end;
flex-wrap: wrap;
display: flex;
margin-bottom: 0.75rem;
}
@@ -1,6 +0,0 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@@ -1,12 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using AGUIWebChatClient
@using AGUIWebChatClient.Components
@using AGUIWebChatClient.Components.Layout
@using Microsoft.Extensions.AI
@@ -1,34 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUIWebChatClient.Components;
using Microsoft.Agents.AI.AGUI;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
string serverUrl = builder.Configuration["SERVER_URL"] ?? "http://localhost:5100";
builder.Services.AddHttpClient("aguiserver", httpClient => httpClient.BaseAddress = new Uri(serverUrl));
builder.Services.AddChatClient(sp => new AGUIChatClient(
sp.GetRequiredService<IHttpClientFactory>().CreateClient("aguiserver"), "ag-ui"));
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
@@ -1,15 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"SERVER_URL": "http://localhost:5100"
}
}
}
}
@@ -1,93 +0,0 @@
html {
min-height: 100vh;
}
html, .main-background-gradient {
background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem);
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
html::after {
content: '';
background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red);
width: 100%;
height: 2px;
position: fixed;
top: 0;
}
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
font-weight: 600;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.btn-default {
display: flex;
padding: 0.25rem 0.75rem;
gap: 0.25rem;
align-items: center;
border-radius: 0.25rem;
border: 1px solid #9CA3AF;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 600;
background-color: #D1D5DB;
}
.btn-default:hover {
background-color: #E5E7EB;
}
.btn-subtle {
display: flex;
padding: 0.25rem 0.75rem;
gap: 0.25rem;
align-items: center;
border-radius: 0.25rem;
border: 1px solid #D1D5DB;
font-size: 0.875rem;
line-height: 1.25rem;
}
.btn-subtle:hover {
border-color: #93C5FD;
background-color: #DBEAFE;
}
.page-width {
max-width: 1024px;
margin: auto;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

-185
View File
@@ -1,185 +0,0 @@
# AGUI WebChat Sample
This sample demonstrates a Blazor-based web chat application using the AG-UI protocol to communicate with an AI agent server.
The sample consists of two projects:
1. **Server** - An ASP.NET Core server that hosts a simple chat agent using the AG-UI protocol
2. **Client** - A Blazor Server application with a rich chat UI for interacting with the agent
## Prerequisites
### Azure OpenAI Configuration
The server requires Azure OpenAI credentials. Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
$env:AZURE_OPENAI_DEPLOYMENT_NAME="your-deployment-name" # e.g., "gpt-4o"
```
The server uses `DefaultAzureCredential` for authentication. Ensure you are logged in using one of the following methods:
- Azure CLI: `az login`
- Azure PowerShell: `Connect-AzAccount`
- Visual Studio or VS Code with Azure extensions
- Environment variables with service principal credentials
## Running the Sample
### Step 1: Start the Server
Open a terminal and navigate to the Server directory:
```powershell
cd Server
dotnet run
```
The server will start on `http://localhost:5100` and expose the AG-UI endpoint at `/ag-ui`.
### Step 2: Start the Client
Open a new terminal and navigate to the Client directory:
```powershell
cd Client
dotnet run
```
The client will start on `http://localhost:5000`. Open your browser and navigate to `http://localhost:5000` to access the chat interface.
### Step 3: Chat with the Agent
Type your message in the text box at the bottom of the page and press Enter or click the send button. The assistant will respond with streaming text that appears in real-time.
Features:
- **Streaming responses**: Watch the assistant's response appear word by word
- **Conversation suggestions**: The assistant may offer follow-up questions after responding
- **New chat**: Click the "New chat" button to start a fresh conversation
- **Auto-scrolling**: The chat automatically scrolls to show new messages
## How It Works
### Server (AG-UI Host)
The server (`Server/Program.cs`) creates a simple chat agent:
```csharp
// Create Azure OpenAI client
AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential());
ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName);
// Create AI agent
ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "ChatAssistant",
instructions: "You are a helpful assistant.");
// Map AG-UI endpoint
app.MapAGUI("/ag-ui", agent);
```
The server exposes the agent via the AG-UI protocol at `http://localhost:5100/ag-ui`.
### Client (Blazor Web App)
The client (`Client/Program.cs`) configures an `AGUIChatClient` to connect to the server:
```csharp
string serverUrl = builder.Configuration["SERVER_URL"] ?? "http://localhost:5100";
builder.Services.AddHttpClient("aguiserver", httpClient => httpClient.BaseAddress = new Uri(serverUrl));
builder.Services.AddChatClient(sp => new AGUIChatClient(
sp.GetRequiredService<IHttpClientFactory>().CreateClient("aguiserver"), "ag-ui"));
```
The Blazor UI (`Client/Components/Pages/Chat/Chat.razor`) uses the `IChatClient` to:
- Send user messages to the agent
- Stream responses back in real-time
- Maintain conversation history
- Display messages with appropriate styling
### UI Components
The chat interface is built from several Blazor components:
- **Chat.razor** - Main chat page coordinating the conversation flow
- **ChatHeader.razor** - Header with "New chat" button
- **ChatMessageList.razor** - Scrollable list of messages with auto-scroll
- **ChatMessageItem.razor** - Individual message rendering (user vs assistant)
- **ChatInput.razor** - Text input with auto-resize and keyboard shortcuts
- **ChatSuggestions.razor** - AI-generated follow-up question suggestions
- **LoadingSpinner.razor** - Animated loading indicator during streaming
## Configuration
### Server Configuration
The server URL and port are configured in `Server/Properties/launchSettings.json`:
```json
{
"profiles": {
"http": {
"applicationUrl": "http://localhost:5100"
}
}
}
```
### Client Configuration
The client connects to the server URL specified in `Client/Properties/launchSettings.json`:
```json
{
"profiles": {
"http": {
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"SERVER_URL": "http://localhost:5100"
}
}
}
}
```
To change the server URL, modify the `SERVER_URL` environment variable in the client's launch settings or provide it at runtime:
```powershell
$env:SERVER_URL="http://your-server:5100"
dotnet run
```
## Customization
### Changing the Agent Instructions
Edit the instructions in `Server/Program.cs`:
```csharp
ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "ChatAssistant",
instructions: "You are a helpful coding assistant specializing in C# and .NET.");
```
### Styling the UI
The chat interface uses CSS files colocated with each Razor component. Key styles:
- `wwwroot/app.css` - Global styles, buttons, color scheme
- `Components/Pages/Chat/Chat.razor.css` - Chat container layout
- `Components/Pages/Chat/ChatMessageItem.razor.css` - Message bubbles and icons
- `Components/Pages/Chat/ChatInput.razor.css` - Input box styling
### Disabling Suggestions
To disable the AI-generated follow-up suggestions, comment out the suggestions component in `Chat.razor`:
```razor
@* <ChatSuggestions OnSelected="@AddUserMessageAsync" @ref="@chatSuggestions" /> *@
```
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,35 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates a basic AG-UI server hosting a chat agent for the Blazor web client.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUI();
WebApplication app = builder.Build();
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Create the AI agent
AzureOpenAIClient azureOpenAIClient = new(
new Uri(endpoint),
new DefaultAzureCredential());
ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName);
ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "ChatAssistant",
instructions: "You are a helpful assistant.");
// Map the AG-UI agent endpoint
app.MapAGUI("/ag-ui", agent);
await app.RunAsync();
@@ -1,14 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -25,6 +25,7 @@
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.AI.AzureAIInference" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.AspNetCore.OpenAPI" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentWebChat.AgentHost.Utilities;
using Azure;
using Azure.AI.Inference;
using Microsoft.Extensions.AI;
using OllamaSharp;
@@ -22,6 +24,7 @@ public static class ChatClientExtensions
ClientChatProvider.Ollama => builder.AddOllamaClient(connectionName, connectionInfo),
ClientChatProvider.OpenAI => builder.AddOpenAIClient(connectionName, connectionInfo),
ClientChatProvider.AzureOpenAI => builder.AddAzureOpenAIClient(connectionName).AddChatClient(connectionInfo.SelectedModel),
ClientChatProvider.AzureAIInference => builder.AddAzureInferenceClient(connectionName, connectionInfo),
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
};
@@ -41,6 +44,16 @@ public static class ChatClientExtensions
})
.AddChatClient(connectionInfo.SelectedModel);
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.Services.AddChatClient(sp =>
{
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
return client.AsIChatClient(connectionInfo.SelectedModel);
});
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
var httpKey = $"{connectionName}_http";
@@ -70,6 +83,7 @@ public static class ChatClientExtensions
ClientChatProvider.Ollama => builder.AddKeyedOllamaClient(connectionName, connectionInfo),
ClientChatProvider.OpenAI => builder.AddKeyedOpenAIClient(connectionName, connectionInfo),
ClientChatProvider.AzureOpenAI => builder.AddKeyedAzureOpenAIClient(connectionName).AddKeyedChatClient(connectionName, connectionInfo.SelectedModel),
ClientChatProvider.AzureAIInference => builder.AddKeyedAzureInferenceClient(connectionName, connectionInfo),
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
};
@@ -89,6 +103,16 @@ public static class ChatClientExtensions
})
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.Services.AddKeyedChatClient(connectionName, sp =>
{
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
return client.AsIChatClient(connectionInfo.SelectedModel);
});
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
var httpKey = $"{connectionName}_http";
@@ -27,7 +27,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
Transport = new HttpClientPipelineTransport(httpClient)
};
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var openAiClient = new OpenAIResponseClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var chatOptions = new ChatOptions()
{
ConversationId = threadId
@@ -8,6 +8,3 @@ dotnet_diagnostic.DURABLE0003.severity = none
dotnet_diagnostic.DURABLE0004.severity = none
dotnet_diagnostic.DURABLE0005.severity = none
dotnet_diagnostic.DURABLE0006.severity = none
# CA1812: Internal classes are instantiated via dependency injection or reflection in samples
dotnet_diagnostic.CA1812.severity = none
@@ -6,8 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>Workflow</AssemblyName>
<RootNamespace>Workflow</RootNamespace>
<AssemblyName>SingleAgent</AssemblyName>
<RootNamespace>SingleAgent</RootNamespace>
</PropertyGroup>
<ItemGroup>
@@ -7,7 +7,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -32,6 +32,6 @@ AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstruct
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.Build();
app.Run();
@@ -5,4 +5,4 @@
POST {{authority}}/api/agents/Joker/run
Content-Type: text/plain
Hello world
Tell me a joke about a pirate.
@@ -7,7 +7,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -7,7 +7,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -7,7 +7,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -7,7 +7,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -11,7 +11,7 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -13,7 +13,7 @@ using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using OpenAI;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
@@ -1,47 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>ReliableStreaming</AssemblyName>
<RootNamespace>ReliableStreaming</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Redis for reliable streaming -->
<ItemGroup>
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>

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