mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9c60c29ec | ||
|
|
fcb6ec9fd3 | ||
|
|
c0ce673354 | ||
|
|
0c93762cb6 | ||
|
|
c34a72c3bf | ||
|
|
64790c4c34 | ||
|
|
38f3e6a5ec | ||
|
|
495c2e9a92 | ||
|
|
99ac6fdaa5 | ||
|
|
0eef9949bf | ||
|
|
0340531f3a | ||
|
|
0d9ae1920d | ||
|
|
90964acd2d | ||
|
|
1949193a2e | ||
|
|
bc8dbe4b05 | ||
|
|
b01fd23cd2 | ||
|
|
2f0b2db12a | ||
|
|
7be860e4c4 | ||
|
|
1dce2581b8 | ||
|
|
9313168eeb | ||
|
|
b391088a68 | ||
|
|
43302fb4cd | ||
|
|
0965a83dcd | ||
|
|
2caf1bb151 | ||
|
|
dec4b3885d | ||
|
|
b55095f0fb | ||
|
|
69225b4aa2 | ||
|
|
71d9d268dd | ||
|
|
c6e73a4ebe | ||
|
|
d889fa6f87 | ||
|
|
1e6aedb3f9 | ||
|
|
d7ca96923b | ||
|
|
0675000f4b | ||
|
|
551219cb55 | ||
|
|
c34af002b8 | ||
|
|
cfcb71334a | ||
|
|
eb06faea2d | ||
|
|
8c6b12e664 | ||
|
|
2d3ba95036 | ||
|
|
f5f909f2e2 | ||
|
|
f43261094f | ||
|
|
900c2a04fb | ||
|
|
417a529e0e | ||
|
|
9c1b2f9a3f | ||
|
|
51721bd9f6 | ||
|
|
db36809744 | ||
|
|
b78a2b6d2e | ||
|
|
f2ed5b55f6 | ||
|
|
6809510413 | ||
|
|
f5786b2c5b | ||
|
|
d858c23bab | ||
|
|
365e438981 | ||
|
|
01a04a3114 | ||
|
|
ee340bef1d | ||
|
|
19ac6e57c0 | ||
|
|
292a2078fa | ||
|
|
ae06c9e4bb | ||
|
|
f7a9005235 | ||
|
|
b86c130411 | ||
|
|
d774b64df0 | ||
|
|
42ffe59592 | ||
|
|
a683c0e865 | ||
|
|
a597a925cb | ||
|
|
9c2e800189 | ||
|
|
a26e9d6274 | ||
|
|
411ee7a60f | ||
|
|
6835161f2d | ||
|
|
3cfd34836f | ||
|
|
a81279d960 | ||
|
|
931197ba95 | ||
|
|
d57303cf53 | ||
|
|
b5595f6f70 | ||
|
|
6232dd8305 | ||
|
|
e9ab514696 | ||
|
|
b69f994840 | ||
|
|
ceaea46765 | ||
|
|
1edd28b264 | ||
|
|
5c70e143a0 | ||
|
|
c2d285e614 | ||
|
|
cb343dd707 | ||
|
|
0d5d10d24a | ||
|
|
e3b700ad9f | ||
|
|
8ae33d3c2b | ||
|
|
bf326e7f78 | ||
|
|
b457d3cdbe | ||
|
|
139c292807 | ||
|
|
b26108975d | ||
|
|
a3909b8b5b | ||
|
|
67de7bcd93 | ||
|
|
e769256976 | ||
|
|
257e717608 | ||
|
|
66a399ee2d | ||
|
|
8cf8b0f995 | ||
|
|
6c624319db | ||
|
|
306c81aef8 | ||
|
|
0f2c5e6cb8 | ||
|
|
17b4dfab14 | ||
|
|
486b78126d | ||
|
|
0723f1d183 | ||
|
|
a57b37d5fa | ||
|
|
907d79ab3c | ||
|
|
ed53ba158b | ||
|
|
5d8be5836c | ||
|
|
0c1d12feca | ||
|
|
b1c210c9d8 | ||
|
|
b8260ae24c | ||
|
|
d302bffc63 | ||
|
|
7a4f6877c3 | ||
|
|
b6c3b8d3f2 | ||
|
|
1dde57981e | ||
|
|
112410ecd6 | ||
|
|
a89c15d6e6 | ||
|
|
27b3a517eb | ||
|
|
1b4103dce3 | ||
|
|
2a4802eac3 | ||
|
|
e303cc963d | ||
|
|
c78edda64f | ||
|
|
a610a4769c | ||
|
|
bcbf1b33e8 | ||
|
|
9f43108ef1 | ||
|
|
db424d56f3 | ||
|
|
c692821869 | ||
|
|
a116edaf5a | ||
|
|
b66efcc09d | ||
|
|
ec08ee541d | ||
|
|
f9a94ce7b5 | ||
|
|
f0f1051c7d | ||
|
|
dc2b109b50 | ||
|
|
ee8936d514 | ||
|
|
b7a19b0fa2 | ||
|
|
02b8ac277e | ||
|
|
7148718804 | ||
|
|
4c529037b8 | ||
|
|
1bf926bfc9 | ||
|
|
d18ce24bf8 | ||
|
|
a7a0660cac | ||
|
|
0781d4b459 | ||
|
|
5bdf8a774a | ||
|
|
cf13e35c73 | ||
|
|
5353b9a2f0 | ||
|
|
04e711cd55 | ||
|
|
ab3d898979 | ||
|
|
02af2bc0ef | ||
|
|
e5b63a1041 | ||
|
|
b575b631c8 | ||
|
|
ce738cc6bc | ||
|
|
f99dca033f | ||
|
|
6ae32f007d | ||
|
|
039e49f353 | ||
|
|
61dbacd6f8 | ||
|
|
c7a8c12296 | ||
|
|
d714b91a14 | ||
|
|
99689add09 | ||
|
|
4fcc5a4b7d | ||
|
|
79bb87061b | ||
|
|
b3e96b80ae | ||
|
|
92df9e14bf | ||
|
|
d2d0f46e15 | ||
|
|
84e2c0cc22 | ||
|
|
4e339f841a | ||
|
|
34a00f1b8a | ||
|
|
d5165e2532 | ||
|
|
f83c39f924 | ||
|
|
037349ff90 | ||
|
|
293abf5b56 | ||
|
|
e2d2299a4f | ||
|
|
8a7260140a | ||
|
|
1da9107f4a | ||
|
|
03b74bfad4 | ||
|
|
f6cd329a32 | ||
|
|
1f0ffc159c | ||
|
|
d50371729a | ||
|
|
77247a304e | ||
|
|
e413c5a285 | ||
|
|
ea066771d9 | ||
|
|
c361ad8d33 | ||
|
|
132597957a | ||
|
|
e7ac655b82 | ||
|
|
57ca139f8c | ||
|
|
e32f93dcb5 | ||
|
|
fcc3f1b6c0 | ||
|
|
45dba6b825 | ||
|
+2 |
e706f07868 | ||
|
|
580a0c431a | ||
|
|
c7e7020c32 | ||
|
|
939c2d69f9 | ||
|
|
c1786b38a7 | ||
|
|
8b3732795c | ||
|
|
9e69e66cfe | ||
|
|
8d7e01e2ee | ||
|
|
37e7c842e0 | ||
|
|
36c1217605 | ||
|
|
a2a9922cde | ||
|
|
aba3076203 | ||
|
|
8458b4ade7 | ||
|
|
e516322918 | ||
|
|
b19860b8a8 | ||
|
|
a75590eb9b | ||
|
|
15d0bda8a2 | ||
|
|
f04f5ef297 | ||
|
|
21dceca482 | ||
|
|
6d890e46ed | ||
|
|
de2abdf573 | ||
|
|
f273ca7353 | ||
|
|
7e5de8f920 | ||
|
|
e92dcb3d5d | ||
|
|
3c874d0073 | ||
|
|
665aacf1ad | ||
|
|
a4e82f4e04 | ||
|
|
67a8147151 | ||
|
|
5537b1da79 | ||
|
|
348ac764e6 | ||
|
|
e8243b7d11 | ||
|
|
4b0f724e62 | ||
|
+20 |
edb367a2b9 | ||
|
|
4bffe1ebc8 |
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "C# (.NET)",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/dotnet:2.4.0": {},
|
||||
"ghcr.io/devcontainers/features/powershell:1.5.1": {},
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2.12.4": {}
|
||||
},
|
||||
"workspaceFolder": "/workspaces/agent-framework/dotnet/",
|
||||
"customizations": {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Code ownership assignments
|
||||
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Azure Functions Integration Test Setup
|
||||
description: Prepare local emulators and tools for Azure Functions integration tests
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Start Durable Task Scheduler Emulator
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=dts-emulator)" ]; then
|
||||
echo "Stopping and removing existing Durable Task Scheduler Emulator"
|
||||
docker rm -f dts-emulator
|
||||
fi
|
||||
echo "Starting Durable Task Scheduler Emulator"
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
|
||||
echo "Waiting for Durable Task Scheduler Emulator to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
|
||||
echo "Durable Task Scheduler Emulator is ready"
|
||||
- name: Start Azurite (Azure Storage emulator)
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=azurite)" ]; then
|
||||
echo "Stopping and removing existing Azurite (Azure Storage emulator)"
|
||||
docker rm -f azurite
|
||||
fi
|
||||
echo "Starting Azurite (Azure Storage emulator)"
|
||||
docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
|
||||
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: Install Azure Functions Core Tools
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Installing Azure Functions Core Tools"
|
||||
npm install -g azure-functions-core-tools@4 --unsafe-perm true
|
||||
func --version
|
||||
@@ -11,9 +11,6 @@ updates:
|
||||
schedule:
|
||||
interval: "cron"
|
||||
cronjob: "0 8 * * 4,0" # Every Thursday(4) and Sunday(0) at 8:00 UTC
|
||||
experimental:
|
||||
nuget-native-updater: false
|
||||
enable-cooldown-metrics-collection: false
|
||||
ignore:
|
||||
# For all System.* and Microsoft.Extensions/Bcl.* packages, ignore all major version updates
|
||||
- dependency-name: "System.*"
|
||||
@@ -28,6 +25,14 @@ updates:
|
||||
- "dependencies"
|
||||
|
||||
# Maintain dependencies for python
|
||||
- package-ecosystem: "pip"
|
||||
directory: "python/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
labels:
|
||||
- "python"
|
||||
- "dependencies"
|
||||
- package-ecosystem: "uv"
|
||||
directory: "python/"
|
||||
schedule:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
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) | **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
|
||||
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | ⚠️ **Deprecated** - Use Responses API instead. <br> **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
|
||||
| `AzureAIAgent` | `AIAgent` (via extension) | **New**: `PersistentAgentsClient.CreateAIAgent()` <br> **Existing**: `PersistentAgentsClient.GetAIAgent(agentId)` |
|
||||
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient().CreateAIAgent()` |
|
||||
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient(modelId).CreateAIAgent()` |
|
||||
| `A2AAgent` | `AIAgent` (via extension) | Replace with `A2ACardResolver.GetAIAgentAsync()` |
|
||||
| `BedrockAgent` | Not supported | Custom implementation required |
|
||||
|
||||
@@ -529,14 +529,14 @@ AIAgent agent = new OpenAIClient(apiKey)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**OpenAI Assistants (New):**
|
||||
**OpenAI Assistants (New):** ⚠️ *Deprecated - Use Responses API instead*
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetAssistantClient()
|
||||
.CreateAIAgent(modelId, instructions: instructions);
|
||||
```
|
||||
|
||||
**OpenAI Assistants (Existing):**
|
||||
**OpenAI Assistants (Existing):** ⚠️ *Deprecated - Use Responses API instead*
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetAssistantClient()
|
||||
@@ -562,6 +562,20 @@ AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
|
||||
.GetAIAgentAsync(agentId);
|
||||
```
|
||||
|
||||
**OpenAI Responses:** *(Recommended for OpenAI)*
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetOpenAIResponseClient(modelId)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**Azure OpenAI Responses:** *(Recommended for Azure OpenAI)*
|
||||
```csharp
|
||||
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**A2A:**
|
||||
```csharp
|
||||
A2ACardResolver resolver = new(new Uri(agentHost));
|
||||
@@ -762,35 +776,57 @@ await foreach (var content in agent.InvokeAsync(userInput, thread))
|
||||
|
||||
**With this Agent Framework CodeInterpreter pattern:**
|
||||
```csharp
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var result = await agent.RunAsync(userInput, thread);
|
||||
Console.WriteLine(result);
|
||||
|
||||
// Extract chat response MEAI type via first level breaking glass
|
||||
var chatResponse = result.RawRepresentation as ChatResponse;
|
||||
// Get the CodeInterpreterToolCallContent (code input)
|
||||
CodeInterpreterToolCallContent? toolCallContent = result.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<CodeInterpreterToolCallContent>()
|
||||
.FirstOrDefault();
|
||||
|
||||
// Extract underlying SDK updates via second level breaking glass
|
||||
var underlyingStreamingUpdates = chatResponse?.RawRepresentation as IEnumerable<object?> ?? [];
|
||||
|
||||
StringBuilder generatedCode = new();
|
||||
foreach (object? underlyingUpdate in underlyingStreamingUpdates ?? [])
|
||||
if (toolCallContent?.Inputs is not null)
|
||||
{
|
||||
if (underlyingUpdate is RunStepDetailsUpdate stepDetailsUpdate && stepDetailsUpdate.CodeInterpreterInput is not null)
|
||||
DataContent? codeInput = toolCallContent.Inputs.OfType<DataContent>().FirstOrDefault();
|
||||
if (codeInput?.HasTopLevelMediaType("text") ?? false)
|
||||
{
|
||||
generatedCode.Append(stepDetailsUpdate.CodeInterpreterInput);
|
||||
Console.WriteLine($"Code Input: {Encoding.UTF8.GetString(codeInput.Data.ToArray())}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(generatedCode.ToString()))
|
||||
// Get the CodeInterpreterToolResultContent (code output)
|
||||
CodeInterpreterToolResultContent? toolResultContent = result.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<CodeInterpreterToolResultContent>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (toolResultContent?.Outputs is not null)
|
||||
{
|
||||
Console.WriteLine($"\n# {chatResponse?.Messages[0].Role}:Generated Code:\n{generatedCode}");
|
||||
TextContent? resultOutput = toolResultContent.Outputs.OfType<TextContent>().FirstOrDefault();
|
||||
if (resultOutput is not null)
|
||||
{
|
||||
Console.WriteLine($"Code Tool Result: {resultOutput.Text}");
|
||||
}
|
||||
}
|
||||
|
||||
// Getting any annotations generated by the tool
|
||||
foreach (AIAnnotation annotation in result.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.SelectMany(c => c.Annotations ?? []))
|
||||
{
|
||||
Console.WriteLine($"Annotation: {annotation}");
|
||||
}
|
||||
```
|
||||
|
||||
**Functional differences:**
|
||||
1. Code interpreter output is separate from text content, not a metadata property
|
||||
2. Access code via `RunStepDetailsUpdate.CodeInterpreterInput` instead of metadata
|
||||
3. Use breaking glass pattern to access underlying SDK objects
|
||||
4. Process text content and code interpreter output independently
|
||||
1. Code interpreter content is now available via MEAI abstractions - no breaking glass required
|
||||
2. Use `CodeInterpreterToolCallContent` to access code inputs (the generated code)
|
||||
3. Use `CodeInterpreterToolResultContent` to access code outputs (execution results)
|
||||
4. Annotations are accessible via `AIAnnotation` on content items
|
||||
</behavioral_changes>
|
||||
|
||||
#### Provider-Specific Options Configuration
|
||||
@@ -980,6 +1016,8 @@ AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential(
|
||||
|
||||
### 3. OpenAI Assistants Migration
|
||||
|
||||
> ⚠️ **DEPRECATION WARNING**: The OpenAI Assistants API has been deprecated. The Agent Framework extension methods for Assistants are marked as `[Obsolete]`. **Please use the Responses API instead** (see Section 6: OpenAI Responses Migration).
|
||||
|
||||
<configuration_changes>
|
||||
**Remove Semantic Kernel Packages:**
|
||||
```xml
|
||||
@@ -1291,52 +1329,7 @@ var result = await agent.RunAsync(userInput, thread);
|
||||
```
|
||||
</api_changes>
|
||||
|
||||
### 8. A2A Migration
|
||||
|
||||
<configuration_changes>
|
||||
**Remove Semantic Kernel Packages:**
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.A2A" />
|
||||
```
|
||||
|
||||
**Add Agent Framework Packages:**
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.Agents.AI.A2A" />
|
||||
```
|
||||
</configuration_changes>
|
||||
|
||||
<api_changes>
|
||||
**Replace this Semantic Kernel pattern:**
|
||||
```csharp
|
||||
using A2A;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.A2A;
|
||||
|
||||
using var httpClient = CreateHttpClient();
|
||||
var client = new A2AClient(agentUrl, httpClient);
|
||||
var cardResolver = new A2ACardResolver(url, httpClient);
|
||||
var agentCard = await cardResolver.GetAgentCardAsync();
|
||||
Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
|
||||
var agent = new A2AAgent(client, agentCard);
|
||||
```
|
||||
|
||||
**With this Agent Framework pattern:**
|
||||
```csharp
|
||||
using System;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.A2A;
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
|
||||
AIAgent agent = await agentCardResolver.GetAIAgentAsync();
|
||||
```
|
||||
</api_changes>
|
||||
|
||||
### 9. Unsupported Providers (Require Custom Implementation)
|
||||
### 8. Unsupported Providers (Require Custom Implementation)
|
||||
|
||||
<behavioral_changes>
|
||||
#### BedrockAgent Migration
|
||||
@@ -1507,7 +1500,7 @@ Console.WriteLine(result);
|
||||
```
|
||||
</behavioral_changes>
|
||||
|
||||
### 10. Function Invocation Filtering
|
||||
### 9. Function Invocation Filtering
|
||||
|
||||
**Invocation Context**
|
||||
|
||||
@@ -1615,25 +1608,4 @@ var filteredAgent = originalAgent
|
||||
.Build();
|
||||
```
|
||||
|
||||
### 11. Function Invocation Contexts
|
||||
|
||||
**Invocation Context**
|
||||
|
||||
Semantic Kernel's `IAutoFunctionInvocationFilter` provides a `AutoFunctionInvocationContext` where Agent Framework provides `FunctionInvocationContext`
|
||||
|
||||
The property mapping guide from a `AutoFunctionInvocationContext` to a `FunctionInvocationContext` is as follows:
|
||||
|
||||
| Semantic Kernel | Agent Framework |
|
||||
| --- | --- |
|
||||
| RequestSequenceIndex | Iteration |
|
||||
| FunctionSequenceIndex | FunctionCallIndex |
|
||||
| ToolCallId | CallContent.CallId |
|
||||
| ChatMessageContent | Messages[0] |
|
||||
| ExecutionSettings | Options |
|
||||
| ChatHistory | Messages |
|
||||
| Function | Function |
|
||||
| Kernel | N/A |
|
||||
| Result | Use `return` from the delegate |
|
||||
| Terminate | Terminate |
|
||||
| CancellationToken | provided via argument to middleware delegate |
|
||||
| Arguments | Arguments |
|
||||
@@ -18,6 +18,7 @@ on:
|
||||
|
||||
env:
|
||||
COVERAGE_THRESHOLD: 80
|
||||
COVERAGE_FRAMEWORK: net10.0 # framework target for which we run/report code coverage
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -59,9 +60,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
- { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Debug }
|
||||
- { targetFramework: "net9.0", os: "windows-latest", configuration: Release }
|
||||
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
- { targetFramework: "net9.0", os: "windows-latest", configuration: Debug }
|
||||
- { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release }
|
||||
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -69,16 +70,16 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.0.0
|
||||
uses: actions/setup-dotnet@v5.0.1
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
- name: Build dotnet solutions
|
||||
@@ -123,7 +124,17 @@ jobs:
|
||||
popd
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
- name: Run Unit Tests Windows
|
||||
# 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: |
|
||||
export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | tr '\n' ' ')
|
||||
@@ -133,12 +144,20 @@ jobs:
|
||||
|
||||
# Check if the project supports the target framework
|
||||
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
|
||||
if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
|
||||
else
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
|
||||
fi
|
||||
else
|
||||
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
|
||||
fi
|
||||
done
|
||||
|
||||
env:
|
||||
# Cosmos DB Emulator connection settings
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
|
||||
- name: Log event name and matrix integration-tests
|
||||
shell: bash
|
||||
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
|
||||
@@ -151,6 +170,14 @@ jobs:
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
# This setup action is required for both Durable Task and Azure Functions integration tests.
|
||||
# We only run it on Ubuntu since the Durable Task and Azure Functions features are not available
|
||||
# on .NET Framework (net472) which is what we use the Windows runner for.
|
||||
- name: Set up Durable Task and Azure Functions Integration Test Emulators
|
||||
if: github.event_name != 'pull_request' && matrix.integration-tests && matrix.os == 'ubuntu-latest'
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
|
||||
- name: Run Integration Tests
|
||||
shell: bash
|
||||
if: github.event_name != 'pull_request' && matrix.integration-tests
|
||||
@@ -168,10 +195,16 @@ jobs:
|
||||
fi
|
||||
done
|
||||
env:
|
||||
# Cosmos DB Emulator connection settings
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
# OpenAI Models
|
||||
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
|
||||
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
|
||||
# Azure OpenAI Models
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
# Azure AI Foundry
|
||||
AzureAI__Endpoint: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
@@ -183,19 +216,22 @@ jobs:
|
||||
|
||||
# Generate test reports and check coverage
|
||||
- name: Generate test reports
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.4.18
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
|
||||
with:
|
||||
reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
|
||||
targetdir: "./TestResults/Reports"
|
||||
reporttypes: "HtmlInline;JsonSummary"
|
||||
|
||||
- name: Upload coverage report artifact
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
|
||||
path: ./TestResults/Reports # Directory containing files to upload
|
||||
|
||||
- name: Check coverage
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
shell: pwsh
|
||||
run: .github/workflows/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { dotnet: "9.0", configuration: Release, os: ubuntu-latest }
|
||||
- { dotnet: "10.0", configuration: Release, os: ubuntu-latest }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
|
||||
@@ -28,6 +28,8 @@ jobs:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -46,4 +48,6 @@ jobs:
|
||||
with:
|
||||
extra_args: --config python/.pre-commit-config.yaml --all-files
|
||||
- name: Run Mypy
|
||||
run: uv run poe mypy
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
|
||||
run: uv run poe ci-mypy
|
||||
|
||||
@@ -66,6 +66,11 @@ jobs:
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
# For Azure Functions integration tests
|
||||
FUNCTIONS_WORKER_RUNTIME: "python"
|
||||
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
||||
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
@@ -87,6 +92,9 @@ jobs:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Set up Azure Functions Integration Test Emulators
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@v1.1.59
|
||||
uses: MishaKav/pytest-coverage-comment@v1.2.0
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
|
||||
+15
-1
@@ -204,6 +204,20 @@ agents.md
|
||||
# AI
|
||||
.claude/
|
||||
WARP.md
|
||||
**/memory-bank/
|
||||
**/projectBrief.md
|
||||
|
||||
# Azurite storage emulator files
|
||||
*/__azurite_db_blob__.json
|
||||
*/__azurite_db_blob_extent__.json
|
||||
*/__azurite_db_queue__.json
|
||||
*/__azurite_db_queue_extent__.json
|
||||
*/__azurite_db_table__.json
|
||||
*/__blobstorage__/
|
||||
*/__queuestorage__/
|
||||
|
||||
# Azure Functions local settings
|
||||
local.settings.json
|
||||
|
||||
# Frontend
|
||||
**/frontend/node_modules/
|
||||
@@ -211,4 +225,4 @@ WARP.md
|
||||
**/frontend/dist/
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.db
|
||||
|
||||
@@ -48,6 +48,8 @@ dotnet add package Microsoft.Agents.AI
|
||||
- **[Migration from Semantic Kernel](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel)** - Guide to migrate from Semantic Kernel
|
||||
- **[Migration from AutoGen](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-autogen)** - Guide to migrate from AutoGen
|
||||
|
||||
Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-community-office-hours) or ask questions in our [Discord channel](https://discord.gg/b5zjErwbQM) to get help from the team and other users.
|
||||
|
||||
### ✨ **Highlights**
|
||||
|
||||
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, human-in-the-loop, and time-travel capabilities
|
||||
|
||||
+2
-2
@@ -42,9 +42,9 @@ Microsoft Agent Framework relies on existing LLMs. Using the framework retains c
|
||||
|
||||
**Framework-Specific Limitations**:
|
||||
|
||||
- **Platform Requirements**: Python 3.10+ required, specific .NET versions (.NET 8.0, 9.0, netstandard2.0, net472)
|
||||
- **Platform Requirements**: Python 3.10+ required, specific .NET versions (.NET 8.0, 9.0, 10.0, netstandard2.0, net472)
|
||||
- **API Dependencies**: Requires proper configuration of LLM provider keys and endpoints
|
||||
- **Orchestration Features**: Advanced orchestration patterns like GroupChat, Sequential, and Concurrent orchestrations are "coming soon" for Python implementation
|
||||
- **Orchestration Features**: Advanced orchestration patterns including GroupChat, Sequential, and Concurrent workflows are now available in both Python and .NET implementations. See the respective language documentation for examples.
|
||||
- **Privacy and Data Protection**: The framework allows for human participation in conversations between agents. It is important to ensure that user data and conversations are protected and that developers use appropriate measures to safeguard privacy.
|
||||
- **Accountability and Transparency**: The framework involves multiple agents conversing and collaborating, it is important to establish clear accountability and transparency mechanisms. Users should be able to understand and trace the decision-making process of the agents involved in order to ensure accountability and address any potential issues or biases.
|
||||
- **Security & unintended consequences**: The use of multi-agent conversations and automation in complex tasks may have unintended consequences. Especially, allowing agents to make changes in external environments through tool calls or function execution could pose significant risks. Developers should carefully consider the potential risks and ensure that appropriate safeguards are in place to prevent harm or negative outcomes, including keeping a human in the loop for decision making.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Declarative Agents
|
||||
|
||||
This folder contains sample agent definitions than be ran using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
|
||||
@@ -0,0 +1,25 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
provider: AzureOpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,25 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Assistants
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,25 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,25 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Responses
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,18 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -0,0 +1,29 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions using the tools provided.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
allowMultipleToolCalls: true
|
||||
chatToolMode: auto
|
||||
tools:
|
||||
- kind: function
|
||||
name: GetWeather
|
||||
description: Get the weather for a given location.
|
||||
bindings:
|
||||
get_weather: get_weather
|
||||
parameters:
|
||||
properties:
|
||||
location:
|
||||
kind: string
|
||||
description: The city and state, e.g. San Francisco, CA
|
||||
required: true
|
||||
unit:
|
||||
kind: string
|
||||
description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
|
||||
required: false
|
||||
enum:
|
||||
- celsius
|
||||
- fahrenheit
|
||||
@@ -0,0 +1,22 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: Remote
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -0,0 +1,21 @@
|
||||
kind: Prompt
|
||||
name: MicrosoftLearnAgent
|
||||
description: Microsoft Learn Agent
|
||||
instructions: You answer questions by searching the Microsoft Learn content only.
|
||||
model:
|
||||
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: remote
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
tools:
|
||||
- kind: mcp
|
||||
name: microsoft_learn
|
||||
description: Get information from Microsoft Learn.
|
||||
url: https://learn.microsoft.com/api/mcp
|
||||
approvalMode:
|
||||
kind: never
|
||||
allowedTools:
|
||||
- microsoft_docs_search
|
||||
@@ -0,0 +1,22 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: remote
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -0,0 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: =Env.OPENAI_MODEL
|
||||
provider: OpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: key
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Assistants
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -0,0 +1,28 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Responses
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -3,17 +3,14 @@
|
||||
<!-- Default properties inherited by all projects. Projects can override. -->
|
||||
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<AnalysisLevel>10.0-all</AnalysisLevel>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<LangVersion>13</LangVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
<NoWarn>$(NoWarn);NU5128;CS8002</NoWarn>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ProjectsCoreTargetFrameworks>net9.0;net8.0</ProjectsCoreTargetFrameworks>
|
||||
<ProjectsDebugCoreTargetFrameworks>net9.0</ProjectsDebugCoreTargetFrameworks>
|
||||
<ProjectsTargetFrameworks>net9.0;net8.0;netstandard2.0;net472</ProjectsTargetFrameworks>
|
||||
<ProjectsDebugTargetFrameworks>net9.0;net472</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworksCore>net10.0;net9.0;net8.0</TargetFrameworksCore>
|
||||
<TargetFrameworks>$(TargetFrameworksCore);netstandard2.0;net472</TargetFrameworks>
|
||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.1.3" />
|
||||
<!-- Only run 'dotnet format' on dev machines, Release builds. Skip on GitHub Actions -->
|
||||
<!-- as this runs in its own Actions job. -->
|
||||
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' AND '$(TargetFramework)' == '$(ProjectsDebugTargetFrameworks)'">
|
||||
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' ">
|
||||
<Message Text="Running dotnet format" Importance="high" />
|
||||
<Exec Command="dotnet format --no-restore -v diag $(ProjectFileName)" />
|
||||
</Target>
|
||||
|
||||
@@ -7,24 +7,35 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Aspire -->
|
||||
<AspireAppHostSdkVersion>13.0.0</AspireAppHostSdkVersion>
|
||||
<AspireAppHostSdkVersion>13.0.2</AspireAppHostSdkVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<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-beta.435" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.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.8" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<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.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.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.8.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" />
|
||||
@@ -35,6 +46,7 @@
|
||||
<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" />
|
||||
@@ -46,13 +58,13 @@
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
|
||||
<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.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" />
|
||||
@@ -78,7 +90,11 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
<PackageVersion Include="Microsoft.Agents.Authentication.Msal" Version="1.3.171-beta" />
|
||||
<PackageVersion Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.3.171-beta" />
|
||||
<!-- A2A -->
|
||||
<PackageVersion Include="A2A" Version="0.3.3-preview" />
|
||||
<PackageVersion Include="A2A.AspNetCore" Version="0.3.3-preview" />
|
||||
@@ -86,29 +102,42 @@
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.6" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.7" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.6.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.7.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
|
||||
<!-- Workflows -->
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0" />
|
||||
<!-- Community -->
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.16.2" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.16.2-preview.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.16.2" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.16.2-preview.1" />
|
||||
<!-- Azure Functions -->
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.9.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.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" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net10.0'" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.67.0-beta" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xretry" Version="1.9.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<!-- Symbols -->
|
||||
@@ -135,17 +164,17 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.1]" />
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="4.14.1" />
|
||||
<PackageReference Include="Roslynator.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="[4.14.0]" />
|
||||
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1" />
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="[4.14.0]" />
|
||||
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.14.1" />
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -20,24 +20,44 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AGUIClientServer/">
|
||||
<Project Path="samples/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AGUIDojo/">
|
||||
<File Path="samples/AGUIDojo/Design.md" />
|
||||
<File Path="samples/AGUIDojo/Plan.md" />
|
||||
<Project Path="samples/AGUIDojo/AGUIDojoClient/AGUIDojoClient.csproj" />
|
||||
<Project Path="samples/AGUIDojo/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AzureFunctions/">
|
||||
<File Path="samples/AzureFunctions/.editorconfig" />
|
||||
<File Path="samples/AzureFunctions/README.md" />
|
||||
<Project Path="samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<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" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/A2A/">
|
||||
<File Path="samples/GettingStarted/A2A/README.md" />
|
||||
<Project Path="samples/GettingStarted/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentProviders/">
|
||||
<File Path="samples/GettingStarted/AgentProviders/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryAgent/Agent_With_AzureFoundryAgent.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_OpenAIAssistants/Agent_With_OpenAIAssistants.csproj" />
|
||||
@@ -64,11 +84,44 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj" />
|
||||
</Folder>
|
||||
<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" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithAnthropic/">
|
||||
<File Path="samples/GettingStarted/AgentWithAnthropic/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
|
||||
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||
@@ -79,12 +132,38 @@
|
||||
<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" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/Purview/" />
|
||||
<Folder Name="/Samples/Purview/AgentWithPurview/">
|
||||
<Project Path="samples/Purview/AgentWithPurview/AgentWithPurview.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithRAG/">
|
||||
<File Path="samples/GettingStarted/AgentWithRAG/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/FoundryAgents/">
|
||||
<File Path="samples/GettingStarted/FoundryAgents/README.md" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/FoundryAgents_Step01.1_Basics.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/FoundryAgents_Step01.2_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/FoundryAgents_Step02_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/FoundryAgents_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/FoundryAgents_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/FoundryAgents_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step07_Observability/FoundryAgents_Step07_Observability.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step08_DependencyInjection/FoundryAgents_Step08_DependencyInjection.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step09_UsingMcpClientAsTools/FoundryAgents_Step09_UsingMcpClientAsTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/FoundryAgents_Step11_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step12_Middleware/FoundryAgents_Step12_Middleware.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step13_Plugins/FoundryAgents_Step13_Plugins.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/FoundryAgents_Step14_CodeInterpreter.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/FoundryAgents_Step15_ComputerUse.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/ModelContextProtocol/">
|
||||
<File Path="samples/GettingStarted/ModelContextProtocol/README.md" />
|
||||
@@ -110,13 +189,22 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/">
|
||||
<File Path="samples/GettingStarted/Workflows/Declarative/README.md" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/DeepResearch/DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj" />
|
||||
<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/DeepResearch.yaml" />
|
||||
<File Path="../workflow-samples/HumanInLoop.yaml" />
|
||||
<File Path="../workflow-samples/Marketing.yaml" />
|
||||
<File Path="../workflow-samples/MathChat.yaml" />
|
||||
<File Path="../workflow-samples/README.md" />
|
||||
@@ -147,7 +235,7 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Visualization/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Visualization/Visualization.csproj" Id="99bf0bc6-2440-428e-b3e7-d880e4b7a5fd" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Visualization/Visualization.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/_Foundational/">
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
|
||||
@@ -161,8 +249,11 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/HostedAgents/">
|
||||
<Project Path="samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/M365Agent/">
|
||||
<Project Path="samples/M365Agent/M365Agent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
@@ -263,6 +354,7 @@
|
||||
<File Path="src/Shared/Demos/SampleEnvironment.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/IntegrationTests/">
|
||||
<File Path="src/Shared/IntegrationTests/AnthropicConfiguration.cs" />
|
||||
<File Path="src/Shared/IntegrationTests/AzureAIConfiguration.cs" />
|
||||
<File Path="src/Shared/IntegrationTests/Mem0Configuration.cs" />
|
||||
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
|
||||
@@ -287,16 +379,24 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
@@ -304,9 +404,13 @@
|
||||
<Folder Name="/Tests/" />
|
||||
<Folder Name="/Tests/IntegrationTests/">
|
||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" />
|
||||
@@ -317,15 +421,23 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
</Solution>
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,13 @@
|
||||
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedWorkflowsSettings)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Settings\*.cs" LinkBase="Shared\Workflows" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedFoundryAgents)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Foundry\Agents\*.cs" LinkBase="Shared\Foundry" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.300",
|
||||
"rollForward": "latestMajor",
|
||||
"version": "10.0.100",
|
||||
"rollForward": "minor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251110.2</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251110.2</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251110.2</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251204.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251204.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251204.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Suppressing errors for Sample projects under dotnet/samples folder
|
||||
[*.cs]
|
||||
dotnet_diagnostic.CA1716.severity = none # Add summary to documentation comment.
|
||||
dotnet_diagnostic.CA1873.severity = none # Evaluation of logging arguments may be expensive
|
||||
dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on object before all references to it are out of scope
|
||||
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
||||
@@ -12,8 +12,6 @@
|
||||
<PackageReference Include="A2A" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace A2A;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
||||
@@ -11,8 +11,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace A2AServer;
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentId "<Log
|
||||
|
||||
### Testing the Agents using the Rest Client
|
||||
|
||||
This sample contains a [.http file](https://learn.microsoft.com/aspnet/core/test/http-files?view=aspnetcore-9.0) which can be used to test the agent.
|
||||
This sample contains a [.http file](https://learn.microsoft.com/aspnet/core/test/http-files?view=aspnetcore-10.0) which can be used to test the agent.
|
||||
|
||||
1. In Visual Studio open [./A2AServer/A2AServer.http](./A2AServer/A2AServer.http)
|
||||
1. There are two sent requests for each agent, e.g., for the invoice agent:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
|
||||
@@ -11,8 +11,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -201,12 +201,12 @@ public static class Program
|
||||
{
|
||||
return "";
|
||||
}
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine();
|
||||
var builder = new StringBuilder().AppendLine();
|
||||
foreach (var kvp in arguments)
|
||||
{
|
||||
builder.AppendLine($" Name: {kvp.Key}");
|
||||
builder.AppendLine($" Value: {kvp.Value}");
|
||||
builder
|
||||
.AppendLine($" Name: {kvp.Key}")
|
||||
.AppendLine($" Value: {kvp.Value}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
[JsonSerializable(typeof(WeatherInfo))]
|
||||
[JsonSerializable(typeof(Recipe))]
|
||||
[JsonSerializable(typeof(Ingredient))]
|
||||
[JsonSerializable(typeof(RecipeResponse))]
|
||||
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ChatClient = OpenAI.Chat.ChatClient;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
internal static class ChatClientAgentFactory
|
||||
{
|
||||
private static AzureOpenAIClient? s_azureOpenAIClient;
|
||||
private static string? s_deploymentName;
|
||||
|
||||
public static void Initialize(IConfiguration configuration)
|
||||
{
|
||||
string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
s_azureOpenAIClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateAgenticChat()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "AgenticChat",
|
||||
description: "A simple chat agent using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateBackendToolRendering()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "BackendToolRenderer",
|
||||
description: "An agent that can render backend tools using Azure OpenAI",
|
||||
tools: [AIFunctionFactory.Create(
|
||||
GetWeather,
|
||||
name: "get_weather",
|
||||
description: "Get the weather for a given location.",
|
||||
AGUIDojoServerSerializerContext.Default.Options)]);
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateHumanInTheLoop()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "HumanInTheLoopAgent",
|
||||
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateToolBasedGenerativeUI()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "ToolBasedGenerativeUIAgent",
|
||||
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateAgenticUI()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "AgenticUIAgent",
|
||||
description: "An agent that generates agentic user interfaces using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static AIAgent CreateSharedState(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "SharedStateAgent",
|
||||
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
|
||||
|
||||
return new SharedStateAgent(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()
|
||||
{
|
||||
Temperature = 20,
|
||||
Conditions = "sunny",
|
||||
Humidity = 50,
|
||||
WindSpeed = 10,
|
||||
FeelsLike = 25
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
|
||||
@@ -11,8 +11,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -6,7 +6,7 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.playwright-mcp/
|
||||
@@ -0,0 +1,11 @@
|
||||
<Solution>
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="../../src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
<Project Path="AGUIDojoClient/AGUIDojoClient.csproj" />
|
||||
<Project Path="AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
</Solution>
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks></TargetFrameworks>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
[CascadingTypeParameter(nameof(TState))]
|
||||
public partial class AgentStateBoundary<TState> : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private RenderFragment? _renderWithState;
|
||||
private protected AgentBoundaryContext<TState>? _context;
|
||||
private AgentThread? _currentThread;
|
||||
private AIAgent? _currentAgent;
|
||||
private bool _disposed;
|
||||
|
||||
[EditorRequired][Parameter] public AIAgent? Agent { get; set; }
|
||||
|
||||
[Parameter] public AgentThread? Thread { get; set; }
|
||||
|
||||
[Parameter] public TState? State { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked when the boundary context is created or recreated.
|
||||
/// Use this to register tools or perform other initialization.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<IAgentBoundaryContext> OnContextCreated { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
this._renderWithState = this.RenderWithState;
|
||||
}
|
||||
|
||||
// Agent boundary renders once when it receives the initial set of parameters
|
||||
// and only ever renders again if the Agent, Thread, or State changes.
|
||||
// Re-rendering the agent boundary will dispose the existing context and create
|
||||
// a new one.
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (this.Agent == null)
|
||||
{
|
||||
throw new InvalidOperationException("AgentBoundary requires an Agent parameter.");
|
||||
}
|
||||
|
||||
// Use Thread parameter if provided, otherwise keep existing thread or create new one
|
||||
var thread = this.Thread ?? this._currentThread ?? this.Agent.GetNewThread();
|
||||
|
||||
TState? currentState = this._context != null ? this._context.CurrentState : default;
|
||||
|
||||
bool agentChanged = this.Agent != this._currentAgent;
|
||||
bool threadChanged = thread != this._currentThread;
|
||||
bool stateChanged = !EqualityComparer<TState?>.Default.Equals(this.State, currentState);
|
||||
|
||||
bool refresh = agentChanged || threadChanged || stateChanged;
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
this._context?.Dispose();
|
||||
this._context = null;
|
||||
this._currentThread = null;
|
||||
}
|
||||
|
||||
this._currentAgent = this.Agent;
|
||||
this._currentThread = thread;
|
||||
|
||||
// Agent is validated non-null above, thread is either from Thread parameter or GetNewThread()
|
||||
bool isNewContext = this._context == null;
|
||||
this._context ??= new AgentBoundaryContext<TState>(this.Agent!, thread!);
|
||||
this._context.CurrentState = this.State;
|
||||
|
||||
// Invoke the context created callback if we created a new context
|
||||
if (isNewContext && this.OnContextCreated.HasDelegate)
|
||||
{
|
||||
_ = this.OnContextCreated.InvokeAsync(this._context);
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<AgentBoundaryContext<TState>>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this._context);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", this._renderWithState);
|
||||
builder.CloseComponent();
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void RenderWithState(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<TState?>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this.State);
|
||||
builder.AddComponentParameter(2, "IsFixed", false);
|
||||
builder.AddComponentParameter(3, "ChildContent", this.ChildContent);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!this._disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
this._context?.Dispose();
|
||||
}
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AgentBoundary : AgentStateBoundary<object?>
|
||||
{
|
||||
protected override void RenderWithState(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<IAgentBoundaryContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this._context);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", this.ChildContent);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public sealed partial class AgentBoundaryContext<TState> : IAgentBoundaryContext, IDisposable
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private readonly AgentThread _thread;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly List<ChatMessage> _messages = [];
|
||||
private readonly List<ChatMessage> _pendingMessages = [];
|
||||
private readonly List<Action> _messageChangeSubscribers = [];
|
||||
private readonly List<Action> _responseUpdateSubscribers = [];
|
||||
private readonly List<Action> _runStatusSubscribers = [];
|
||||
private readonly List<AITool> _tools = [];
|
||||
private readonly Dictionary<string, TaskCompletionSource<object>> _pendingResponses = [];
|
||||
|
||||
public CancellationToken CancellationToken => this._cancellationTokenSource.Token;
|
||||
|
||||
public IReadOnlyList<ChatMessage> CompletedMessages => this._messages.AsReadOnly();
|
||||
|
||||
public TState? CurrentState { get; set; }
|
||||
|
||||
public IReadOnlyList<ChatMessage> PendingMessages => this._pendingMessages.AsReadOnly();
|
||||
|
||||
public ChatMessage? CurrentMessage { get; private set; }
|
||||
|
||||
public ChatResponseUpdate? CurrentUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the agent is currently processing a turn.
|
||||
/// </summary>
|
||||
public bool IsProcessing { get; private set; }
|
||||
|
||||
public AgentBoundaryContext(AIAgent agent, AgentThread thread)
|
||||
{
|
||||
this._agent = agent;
|
||||
this._thread = thread;
|
||||
this._cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a client-side tool that will be passed to the agent during invocation.
|
||||
/// These tools are sent to the server via ChatClientAgentRunOptions.ChatOptions.Tools.
|
||||
/// </summary>
|
||||
public void RegisterTool(AITool tool)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tool);
|
||||
this._tools.Add(tool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers multiple client-side tools that will be passed to the agent during invocation.
|
||||
/// </summary>
|
||||
public void RegisterTools(params AITool[] tools)
|
||||
{
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
this.RegisterTool(tool);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pending response for the given key and returns a task that completes when ProvideResponse is called.
|
||||
/// Used by frontend tools to wait for user input from UI components.
|
||||
/// </summary>
|
||||
/// <param name="key">A unique key to identify this pending response (e.g., function call ID).</param>
|
||||
/// <returns>A task that completes with the response object when ProvideResponse is called.</returns>
|
||||
public Task<object> WaitForResponse(string key)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
this._pendingResponses[key] = tcs;
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a response for a pending request, completing the task returned by WaitForResponse.
|
||||
/// Called by UI components when user interaction is complete.
|
||||
/// </summary>
|
||||
/// <param name="key">The key used when calling WaitForResponse.</param>
|
||||
/// <param name="response">The response object to return.</param>
|
||||
/// <returns>True if the response was provided successfully, false if no pending request was found.</returns>
|
||||
public bool ProvideResponse(string key, object response)
|
||||
{
|
||||
if (this._pendingResponses.TryGetValue(key, out var tcs))
|
||||
{
|
||||
this._pendingResponses.Remove(key);
|
||||
tcs.TrySetResult(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._cancellationTokenSource.Cancel();
|
||||
this._cancellationTokenSource.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task SendAsync(params ChatMessage[] userMessages)
|
||||
{
|
||||
// This starts a new turn. Once completed, we will add the new messages to the conversation.
|
||||
this._pendingMessages.Clear();
|
||||
this._pendingMessages.AddRange(userMessages);
|
||||
|
||||
// Mark as processing
|
||||
this.IsProcessing = true;
|
||||
|
||||
// Notify subscribers that run status changed (processing started)
|
||||
this.TriggerRunStatusChanged();
|
||||
|
||||
// User messages added, notify subscribers.
|
||||
this.TriggerMessageChanges();
|
||||
|
||||
// Build run options with registered tools if any
|
||||
AgentRunOptions? options = null;
|
||||
if (this._tools.Count > 0)
|
||||
{
|
||||
options = new ChatClientAgentRunOptions(new ChatOptions
|
||||
{
|
||||
Tools = [.. this._tools]
|
||||
});
|
||||
}
|
||||
|
||||
// Start a turn. Collect all updates as we stream them.
|
||||
await foreach (var update in this._agent.RunStreamingAsync(
|
||||
userMessages,
|
||||
this._thread,
|
||||
options,
|
||||
this._cancellationTokenSource.Token))
|
||||
{
|
||||
var chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
this.CurrentUpdate = chatUpdate;
|
||||
|
||||
// Notify subscribers of the new update, this always happens before a new message is created/updated.
|
||||
this.TriggerChatResponseUpdate();
|
||||
|
||||
// This creates or adds a new message to the pending messages as needed.
|
||||
var isNewMessage = MessageHelpers.ProcessUpdate(chatUpdate, this._pendingMessages);
|
||||
if (isNewMessage)
|
||||
{
|
||||
this.CurrentMessage = this._pendingMessages[this._pendingMessages.Count - 1];
|
||||
|
||||
// Finalize the previous message's content if we have 2 or more messages now.
|
||||
if (this._pendingMessages.Count > userMessages.Length + 1)
|
||||
{
|
||||
MessageHelpers.CoalesceContent(this._pendingMessages[this._pendingMessages.Count - 2].Contents);
|
||||
}
|
||||
|
||||
// Notify subscribers of new message
|
||||
this.TriggerMessageChanges();
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining updates to finalize the last message.
|
||||
if (this._pendingMessages.Count > userMessages.Length)
|
||||
{
|
||||
MessageHelpers.CoalesceContent(this._pendingMessages[this._pendingMessages.Count - 1].Contents);
|
||||
}
|
||||
|
||||
// Add the new messages to the conversation
|
||||
this._messages.AddRange(this._pendingMessages);
|
||||
|
||||
// Finish the turn
|
||||
this._pendingMessages.Clear();
|
||||
this.CurrentMessage = null;
|
||||
this.CurrentUpdate = null;
|
||||
this.IsProcessing = false;
|
||||
|
||||
// Notify subscribers that run status changed (processing ended)
|
||||
this.TriggerRunStatusChanged();
|
||||
|
||||
// Notify subscribers
|
||||
this.TriggerChatResponseUpdate();
|
||||
this.TriggerMessageChanges();
|
||||
}
|
||||
|
||||
private void TriggerChatResponseUpdate()
|
||||
{
|
||||
// Iterate backwards to avoid issues if subscribers are removed during iteration
|
||||
for (var i = this._responseUpdateSubscribers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
this._responseUpdateSubscribers[i]();
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerMessageChanges()
|
||||
{
|
||||
// Iterate backwards to avoid issues if subscribers are removed during iteration
|
||||
for (var i = this._messageChangeSubscribers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
this._messageChangeSubscribers[i]();
|
||||
}
|
||||
}
|
||||
|
||||
public MessageSubscription SubscribeToMessageChanges(Action onNewMessage)
|
||||
{
|
||||
return new MessageSubscription(this._messageChangeSubscribers, onNewMessage);
|
||||
}
|
||||
|
||||
public ResponseUpdateSubscription SubscribeToResponseUpdates(Action onChatResponse)
|
||||
{
|
||||
return new ResponseUpdateSubscription(this._responseUpdateSubscribers, onChatResponse);
|
||||
}
|
||||
|
||||
public RunStatusSubscription SubscribeToRunStatusChanges(Action onRunStatusChanged)
|
||||
{
|
||||
return new RunStatusSubscription(this._runStatusSubscribers, onRunStatusChanged);
|
||||
}
|
||||
|
||||
private void TriggerRunStatusChanged()
|
||||
{
|
||||
// Iterate backwards to avoid issues if subscribers are removed during iteration
|
||||
for (var i = this._runStatusSubscribers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
this._runStatusSubscribers[i]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAgentBoundaryContext
|
||||
{
|
||||
// Push new messages to the agent
|
||||
Task SendAsync(params ChatMessage[] userMessages);
|
||||
|
||||
// All message interactions from previous turns
|
||||
IReadOnlyList<ChatMessage> CompletedMessages { get; }
|
||||
|
||||
// All message interactions from the current turn. These represent completed messages only.
|
||||
IReadOnlyList<ChatMessage> PendingMessages { get; }
|
||||
|
||||
// The current message being processed by the agent.
|
||||
ChatMessage? CurrentMessage { get; }
|
||||
|
||||
ChatResponseUpdate? CurrentUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the agent is currently processing a turn.
|
||||
/// </summary>
|
||||
bool IsProcessing { get; }
|
||||
|
||||
// Triggered any time there is a change on a message.
|
||||
MessageSubscription SubscribeToMessageChanges(Action onNewMessage);
|
||||
|
||||
ResponseUpdateSubscription SubscribeToResponseUpdates(Action onChatResponse);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to run status changes (when processing starts or ends).
|
||||
/// </summary>
|
||||
/// <param name="onRunStatusChanged">The callback to invoke when run status changes.</param>
|
||||
/// <returns>A subscription that can be disposed to unsubscribe.</returns>
|
||||
RunStatusSubscription SubscribeToRunStatusChanges(Action onRunStatusChanged);
|
||||
|
||||
CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a client-side tool that will be passed to the agent during invocation.
|
||||
/// </summary>
|
||||
void RegisterTool(AITool tool);
|
||||
|
||||
/// <summary>
|
||||
/// Registers multiple client-side tools that will be passed to the agent during invocation.
|
||||
/// </summary>
|
||||
void RegisterTools(params AITool[] tools);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pending response for the given key and returns a task that completes when ProvideResponse is called.
|
||||
/// Used by frontend tools to wait for user input from UI components.
|
||||
/// </summary>
|
||||
/// <param name="key">A unique key to identify this pending response (e.g., function call ID).</param>
|
||||
/// <returns>A task that completes with the response object when ProvideResponse is called.</returns>
|
||||
Task<object> WaitForResponse(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Provides a response for a pending request, completing the task returned by WaitForResponse.
|
||||
/// Called by UI components when user interaction is complete.
|
||||
/// </summary>
|
||||
/// <param name="key">The key used when calling WaitForResponse.</param>
|
||||
/// <param name="response">The response object to return.</param>
|
||||
/// <returns>True if the response was provided successfully, false if no pending request was found.</returns>
|
||||
bool ProvideResponse(string key, object response);
|
||||
}
|
||||
|
||||
public readonly struct MessageSubscription : IDisposable, IEquatable<MessageSubscription>
|
||||
{
|
||||
internal readonly List<Action> _subscribers;
|
||||
internal readonly Action _subscription;
|
||||
|
||||
internal MessageSubscription(List<Action> subscribers, Action subscription)
|
||||
{
|
||||
this._subscribers = subscribers;
|
||||
this._subscription = subscription;
|
||||
this._subscribers.Add(this._subscription);
|
||||
}
|
||||
|
||||
public void Dispose() => this._subscribers.Remove(this._subscription);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is MessageSubscription subscription && this.Equals(subscription);
|
||||
}
|
||||
|
||||
public bool Equals(MessageSubscription other)
|
||||
{
|
||||
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
|
||||
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this._subscribers, this._subscription);
|
||||
}
|
||||
|
||||
public static bool operator ==(MessageSubscription left, MessageSubscription right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(MessageSubscription left, MessageSubscription right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ResponseUpdateSubscription : IDisposable, IEquatable<ResponseUpdateSubscription>
|
||||
{
|
||||
private readonly List<Action> _subscribers;
|
||||
private readonly Action _subscription;
|
||||
|
||||
public ResponseUpdateSubscription(List<Action> subscribers, Action subscription)
|
||||
{
|
||||
this._subscribers = subscribers;
|
||||
this._subscription = subscription;
|
||||
this._subscribers.Add(this._subscription);
|
||||
}
|
||||
|
||||
public void Dispose() => this._subscribers.Remove(this._subscription);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ResponseUpdateSubscription subscription && this.Equals(subscription);
|
||||
}
|
||||
|
||||
public bool Equals(ResponseUpdateSubscription other)
|
||||
{
|
||||
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
|
||||
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this._subscribers, this._subscription);
|
||||
}
|
||||
|
||||
public static bool operator ==(ResponseUpdateSubscription left, ResponseUpdateSubscription right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ResponseUpdateSubscription left, ResponseUpdateSubscription right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct RunStatusSubscription : IDisposable, IEquatable<RunStatusSubscription>
|
||||
{
|
||||
private readonly List<Action> _subscribers;
|
||||
private readonly Action _subscription;
|
||||
|
||||
public RunStatusSubscription(List<Action> subscribers, Action subscription)
|
||||
{
|
||||
this._subscribers = subscribers;
|
||||
this._subscription = subscription;
|
||||
this._subscribers.Add(this._subscription);
|
||||
}
|
||||
|
||||
public void Dispose() => this._subscribers.Remove(this._subscription);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is RunStatusSubscription subscription && this.Equals(subscription);
|
||||
}
|
||||
|
||||
public bool Equals(RunStatusSubscription other)
|
||||
{
|
||||
return EqualityComparer<List<Action>>.Default.Equals(this._subscribers, other._subscribers) &&
|
||||
EqualityComparer<Action>.Default.Equals(this._subscription, other._subscription);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this._subscribers, this._subscription);
|
||||
}
|
||||
|
||||
public static bool operator ==(RunStatusSubscription left, RunStatusSubscription right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(RunStatusSubscription left, RunStatusSubscription right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public sealed partial class AgentInput : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private AgentBoundaryContext<object?>? _context;
|
||||
private string? _inputText;
|
||||
private RunStatusSubscription? _subscription;
|
||||
|
||||
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
|
||||
|
||||
[Parameter] public string Placeholder { get; set; } = "Type a message...";
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Unsubscribe from previous context if it changed
|
||||
if (this._context != this.AgentContext)
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
this._context = this.AgentContext;
|
||||
|
||||
// Subscribe to run status changes
|
||||
if (this._context != null)
|
||||
{
|
||||
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this.Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRunStatusChanged()
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var isProcessing = this._context?.IsProcessing ?? false;
|
||||
var isDisabled = string.IsNullOrWhiteSpace(this._inputText) || isProcessing;
|
||||
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "agent-input");
|
||||
|
||||
builder.OpenElement(2, "textarea");
|
||||
builder.AddAttribute(3, "value", this._inputText);
|
||||
builder.AddAttribute(4, "oninput", EventCallback.Factory.Create<ChangeEventArgs>(this, e => { this._inputText = e.Value?.ToString(); this.Render(); }));
|
||||
builder.AddAttribute(5, "placeholder", this.Placeholder);
|
||||
builder.AddAttribute(6, "rows", "1");
|
||||
builder.AddAttribute(7, "disabled", isProcessing);
|
||||
builder.CloseElement();
|
||||
|
||||
builder.OpenElement(8, "button");
|
||||
builder.AddAttribute(9, "class", "send-button");
|
||||
builder.AddAttribute(10, "onclick", EventCallback.Factory.Create(this, this.SendAsync));
|
||||
builder.AddAttribute(11, "disabled", isDisabled);
|
||||
builder.AddMarkupContent(12, """<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>""");
|
||||
builder.CloseElement(); // close button
|
||||
|
||||
builder.CloseElement(); // close div
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this._inputText) && this._context is { IsProcessing: false })
|
||||
{
|
||||
var text = this._inputText;
|
||||
this._inputText = ""; // Clear input immediately
|
||||
this.Render(); // Re-render to clear input
|
||||
|
||||
await this._context.SendAsync(new ChatMessage(ChatRole.User, text));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// A component that displays a loading indicator (three animated dots) when the agent is processing.
|
||||
/// </summary>
|
||||
public sealed partial class AgentLoadingIndicator : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private AgentBoundaryContext<object?>? _context;
|
||||
private RunStatusSubscription? _subscription;
|
||||
|
||||
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Unsubscribe from previous context if it changed
|
||||
if (this._context != this.AgentContext)
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
this._context = this.AgentContext;
|
||||
|
||||
// Subscribe to run status changes
|
||||
if (this._context != null)
|
||||
{
|
||||
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this.Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRunStatusChanged()
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var isProcessing = this._context?.IsProcessing ?? false;
|
||||
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
if (!isProcessing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "agent-loading-indicator");
|
||||
builder.OpenElement(2, "div");
|
||||
builder.AddAttribute(3, "class", "agent-loading-dots");
|
||||
builder.AddMarkupContent(4, "<span></span><span></span><span></span>");
|
||||
builder.CloseElement();
|
||||
builder.CloseElement();
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@namespace Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@typeparam TState
|
||||
@implements IDisposable
|
||||
|
||||
<CascadingValue Value="@CurrentState" IsFixed="false">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
private ResponseUpdateSubscription? _subscription;
|
||||
|
||||
/// <summary>
|
||||
/// The agent boundary context to subscribe to for state updates.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
public IAgentBoundaryContext? BoundaryContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current state value. Can be set initially and will be updated by state events.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public TState? CurrentState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to deserialize a STATE_SNAPSHOT (application/json) into TState.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<ReadOnlyMemory<byte>, TState?>? OnSnapshot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to apply a STATE_DELTA (application/json-patch+json) to the current state.
|
||||
/// Returns the updated state.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<TState?, ReadOnlyMemory<byte>, TState?>? OnDelta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback invoked whenever state changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<TState?> CurrentStateChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Child content that will receive the cascaded state.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (BoundaryContext is not null)
|
||||
{
|
||||
_subscription = BoundaryContext.SubscribeToResponseUpdates(OnResponseUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResponseUpdate()
|
||||
{
|
||||
var update = BoundaryContext?.CurrentUpdate;
|
||||
if (update?.Contents is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
if (content is DataContent dataContent)
|
||||
{
|
||||
if (string.Equals(dataContent.MediaType, "application/json", StringComparison.OrdinalIgnoreCase) && OnSnapshot is not null)
|
||||
{
|
||||
// STATE_SNAPSHOT - let app deserialize
|
||||
CurrentState = OnSnapshot(dataContent.Data);
|
||||
_ = CurrentStateChanged.InvokeAsync(CurrentState);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
else if (string.Equals(dataContent.MediaType, "application/json-patch+json", StringComparison.OrdinalIgnoreCase) && OnDelta is not null)
|
||||
{
|
||||
// STATE_DELTA - let app apply the patch
|
||||
CurrentState = OnDelta(CurrentState, dataContent.Data);
|
||||
_ = CurrentStateChanged.InvokeAsync(CurrentState);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a suggestion that can be displayed and sent to the agent.
|
||||
/// </summary>
|
||||
public readonly struct Suggestion : IEquatable<Suggestion>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Suggestion"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="text">The display text for the suggestion.</param>
|
||||
/// <param name="message">The message to send when the suggestion is selected.</param>
|
||||
public Suggestion(string text, ChatMessage message)
|
||||
{
|
||||
this.Text = text;
|
||||
this.Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Suggestion"/> struct with a simple text message.
|
||||
/// </summary>
|
||||
/// <param name="text">The display text for the suggestion, also used as the message content.</param>
|
||||
public Suggestion(string text)
|
||||
{
|
||||
this.Text = text;
|
||||
this.Message = new ChatMessage(ChatRole.User, text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display text for the suggestion.
|
||||
/// </summary>
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message to send when the suggestion is selected.
|
||||
/// </summary>
|
||||
public ChatMessage Message { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is Suggestion other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(Suggestion other) => this.Text == other.Text;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.Text?.GetHashCode() ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two <see cref="Suggestion"/> instances are equal.
|
||||
/// </summary>
|
||||
public static bool operator ==(Suggestion left, Suggestion right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two <see cref="Suggestion"/> instances are not equal.
|
||||
/// </summary>
|
||||
public static bool operator !=(Suggestion left, Suggestion right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
public sealed partial class AgentSuggestions : IComponent, IDisposable
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private AgentBoundaryContext<object?>? _context;
|
||||
private IReadOnlyList<Suggestion>? _suggestions;
|
||||
private RunStatusSubscription? _subscription;
|
||||
|
||||
[CascadingParameter] public AgentBoundaryContext<object?>? AgentContext { get; set; }
|
||||
|
||||
[Parameter] public IReadOnlyList<Suggestion>? Suggestions { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Unsubscribe from previous context if it changed
|
||||
if (this._context != this.AgentContext)
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
this._context = this.AgentContext;
|
||||
|
||||
// Subscribe to run status changes
|
||||
if (this._context != null)
|
||||
{
|
||||
this._subscription = this._context.SubscribeToRunStatusChanges(this.OnRunStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
this._suggestions = this.Suggestions;
|
||||
this.Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnRunStatusChanged()
|
||||
{
|
||||
this.Render();
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var isProcessing = this._context?.IsProcessing ?? false;
|
||||
|
||||
this._renderHandle.Render(builder =>
|
||||
{
|
||||
if (this._suggestions is null || this._suggestions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "agent-suggestions");
|
||||
|
||||
for (var i = 0; i < this._suggestions.Count; i++)
|
||||
{
|
||||
var suggestion = this._suggestions[i];
|
||||
builder.OpenElement(2, "button");
|
||||
builder.SetKey(suggestion.Text);
|
||||
builder.AddAttribute(3, "class", "suggestion-button");
|
||||
builder.AddAttribute(4, "onclick", EventCallback.Factory.Create(this, () => this.SelectSuggestionAsync(suggestion)));
|
||||
builder.AddAttribute(5, "disabled", isProcessing);
|
||||
builder.AddContent(6, suggestion.Text);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SelectSuggestionAsync(Suggestion suggestion)
|
||||
{
|
||||
if (this._context is { IsProcessing: false })
|
||||
{
|
||||
await this._context.SendAsync(suggestion.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._subscription?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class is apparently never instantiated
|
||||
internal sealed class ContentBlock : IComponent
|
||||
#pragma warning restore CA1812 // Internal class is apparently never instantiated
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
// Always render when parameters are set - the parent component (MessageList)
|
||||
// only triggers renders when there are actual updates to show.
|
||||
this._renderHandle.Render(this.Render);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, this.ChildContent);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public class ContentContext(AIContent content)
|
||||
{
|
||||
public AIContent Content { get; init; } = content;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public abstract class ContentTemplateBase : IComponent
|
||||
{
|
||||
public abstract void Attach(RenderHandle renderHandle);
|
||||
|
||||
public abstract Task SetParametersAsync(ParameterView parameters);
|
||||
|
||||
public virtual bool When(ContentContext context) => true;
|
||||
|
||||
[Parameter] public RenderFragment<ContentContext> ChildContent { get; set; } = (content) => builder => { };
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for rendering DataContent. Renders nothing by default
|
||||
/// as DataContent typically contains binary/JSON data not meant for display.
|
||||
/// </summary>
|
||||
public class DataContentTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this.ChildContent = this.RenderData;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
return context.Content is DataContent;
|
||||
}
|
||||
|
||||
private RenderFragment RenderData(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, render nothing.
|
||||
};
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for rendering ErrorContent. Renders nothing by default
|
||||
/// to prevent the error from crashing the UI. More specific templates can
|
||||
/// override this to display error messages.
|
||||
/// </summary>
|
||||
public class ErrorTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this.ChildContent = this.RenderError;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this template should handle the given content.
|
||||
/// Matches ErrorContent.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
return context.Content is ErrorContent;
|
||||
}
|
||||
|
||||
private RenderFragment RenderError(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, render nothing.
|
||||
// Specific templates can override to display error messages.
|
||||
};
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Template for rendering function/tool call content in messages.
|
||||
/// Provides access to both the call and its result (when available) via InvocationContext.
|
||||
/// </summary>
|
||||
public class FunctionCallTemplate : ContentTemplateBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the tool name to filter on. If null, matches all function calls.
|
||||
/// </summary>
|
||||
[Parameter] public string? ToolName { get; set; }
|
||||
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderFunctionCall;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this template should handle the given content.
|
||||
/// </summary>
|
||||
/// <param name="context">The content context.</param>
|
||||
/// <returns>True if this template should render the content.</returns>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
if (context.Content is not FunctionCallContent call)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by tool name if specified
|
||||
if (this.ToolName != null && !string.Equals(call.Name, this.ToolName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private RenderFragment RenderFunctionCall(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, function calls are not rendered visually.
|
||||
// Custom templates (like WeatherCallTemplate) can override this
|
||||
// behavior for specific functions by registering before this template.
|
||||
};
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for FunctionResultContent that renders nothing.
|
||||
/// Function results are internal tool responses and typically don't need
|
||||
/// visual representation in the chat UI.
|
||||
/// </summary>
|
||||
public class FunctionResultTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderFunctionResult;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only match FunctionResultContent.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context) => context.Content is FunctionResultContent;
|
||||
|
||||
private RenderFragment RenderFunctionResult(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, function results are not rendered visually.
|
||||
// The result data is typically processed by the agent to generate text responses.
|
||||
};
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Context for a function invocation, tracking the call and its result.
|
||||
/// </summary>
|
||||
public class InvocationContext
|
||||
{
|
||||
private Action? _resultArrived;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvocationContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="call">The function call content.</param>
|
||||
public InvocationContext(FunctionCallContent call)
|
||||
{
|
||||
this.Call = call;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function call content.
|
||||
/// </summary>
|
||||
public FunctionCallContent Call { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function result content, if available.
|
||||
/// </summary>
|
||||
public FunctionResultContent? ResultContent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the result has arrived.
|
||||
/// </summary>
|
||||
public bool HasResult => this.ResultContent != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function name from the call.
|
||||
/// </summary>
|
||||
public string FunctionName => this.Call.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the call ID.
|
||||
/// </summary>
|
||||
public string CallId => this.Call.CallId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the arguments from the call.
|
||||
/// </summary>
|
||||
public IDictionary<string, object?>? Arguments => this.Call.Arguments;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the result arrives.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1003 // Use generic event handler instances
|
||||
public event Action? ResultArrived
|
||||
#pragma warning restore CA1003 // Use generic event handler instances
|
||||
{
|
||||
add => this._resultArrived += value;
|
||||
remove => this._resultArrived -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the result and raises the ResultArrived event.
|
||||
/// </summary>
|
||||
/// <param name="result">The function result content.</param>
|
||||
internal void SetResult(FunctionResultContent result)
|
||||
{
|
||||
this.ResultContent = result;
|
||||
this._resultArrived?.Invoke();
|
||||
this._resultArrived = null; // Clear invocation list after firing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an argument value by name, deserializing from JSON if necessary.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize to.</typeparam>
|
||||
/// <param name="name">The argument name.</param>
|
||||
/// <returns>The argument value, or default if not found.</returns>
|
||||
public T? GetArgument<T>(string name)
|
||||
{
|
||||
if (this.Arguments is null || !this.Arguments.TryGetValue(name, out var value))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (value is T typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
return jsonElement.Deserialize<T>();
|
||||
}
|
||||
|
||||
// Try to convert via JSON serialization
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result as a specific type, deserializing from JSON if necessary.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize to.</typeparam>
|
||||
/// <returns>The result value, or default if not available.</returns>
|
||||
public T? GetResult<T>()
|
||||
{
|
||||
if (this.ResultContent?.Result is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (this.ResultContent.Result is T typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
if (this.ResultContent.Result is JsonElement jsonElement)
|
||||
{
|
||||
return jsonElement.Deserialize<T>();
|
||||
}
|
||||
|
||||
// Try to convert via JSON serialization
|
||||
var json = JsonSerializer.Serialize(this.ResultContent.Result);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public class TextTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderText;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only match TextContent, allowing other content templates to handle
|
||||
/// FunctionCallContent, FunctionResultContent, etc.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context) => context.Content is TextContent;
|
||||
|
||||
private RenderFragment RenderText(ContentContext content) => builder =>
|
||||
{
|
||||
if (content.Content is TextContent textContent)
|
||||
{
|
||||
builder.AddContent(0, textContent.Text);
|
||||
}
|
||||
};
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class is apparently never instantiated
|
||||
internal sealed class DefaultMessageTemplate : MessageTemplateBase
|
||||
#pragma warning restore CA1812 // Internal class is apparently never instantiated
|
||||
{
|
||||
public override bool When(MessageContext context) => true;
|
||||
|
||||
// Buffer to convert response updates to chat messages.
|
||||
private readonly List<ChatMessage> _buffer = [];
|
||||
|
||||
public DefaultMessageTemplate()
|
||||
{
|
||||
this.ChildContent = this.SelectTemplate;
|
||||
}
|
||||
|
||||
private RenderFragment SelectTemplate(MessageContext messageContext)
|
||||
{
|
||||
if (messageContext.ChatMessage is not null)
|
||||
{
|
||||
var getRenderContents = messageContext.RenderContents();
|
||||
// Return a render fragment that checks visibility at render time
|
||||
// This is important because message contents may change during streaming
|
||||
return CreateRenderMessage(
|
||||
messageContext.ChatMessage,
|
||||
getRenderContents);
|
||||
}
|
||||
else
|
||||
{
|
||||
var getRenderContents = messageContext.RenderContents();
|
||||
var updates = messageContext.ResponseUpdates ?? [];
|
||||
if (updates.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("MessageContext must have either a ChatMessage or at least one ResponseUpdate.");
|
||||
}
|
||||
this._buffer.Clear();
|
||||
this._buffer.AddMessages(updates);
|
||||
if (this._buffer.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException("DefaultMessageTemplate only supports a single ResponseUpdate.");
|
||||
}
|
||||
|
||||
return CreateRenderMessage(
|
||||
this._buffer[0],
|
||||
getRenderContents);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a message has any visible content that should be displayed.
|
||||
/// Messages that only contain FunctionResultContent are internal tool messages
|
||||
/// and should not be rendered as chat bubbles.
|
||||
/// FunctionCallContent is considered visible because content templates can render them
|
||||
/// (e.g., WeatherCallTemplate renders weather tool calls as cards).
|
||||
/// </summary>
|
||||
private static bool HasVisibleContent(ChatMessage message)
|
||||
{
|
||||
if (message.Contents.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var content in message.Contents)
|
||||
{
|
||||
// TextContent is visible
|
||||
if (content is TextContent textContent && !string.IsNullOrWhiteSpace(textContent.Text))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// FunctionCallContent is visible - content templates can render them
|
||||
// (e.g., WeatherCallTemplate renders weather tool calls as cards)
|
||||
if (content is FunctionCallContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Other content types that are not function results are visible
|
||||
if (content is not FunctionResultContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static RenderFragment CreateRenderMessage(
|
||||
ChatMessage message,
|
||||
RenderFragment getRenderContents)
|
||||
{
|
||||
var roleClass = $"{message.Role}-message";
|
||||
return builder =>
|
||||
{
|
||||
// Check visibility at render time, not at template creation time
|
||||
// This is important because message contents may change during streaming
|
||||
if (!HasVisibleContent(message))
|
||||
{
|
||||
return; // Don't render messages without visible content
|
||||
}
|
||||
|
||||
builder.OpenElement(0, "div");
|
||||
if (!string.IsNullOrEmpty(message.MessageId))
|
||||
{
|
||||
builder.AddAttribute(1, "id", message.MessageId);
|
||||
}
|
||||
builder.AddAttribute(2, "class", $"chat-message {roleClass}");
|
||||
builder.AddContent(3, getRenderContents);
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public class MessageContext
|
||||
{
|
||||
private readonly MessageListContext _messageListContext;
|
||||
private RenderFragment? _contentsRenderer;
|
||||
private MessageTemplateBase? _template;
|
||||
private readonly Dictionary<AIContent, RenderFragment> _contentRenderers = [];
|
||||
|
||||
internal MessageContext(ChatMessage message, MessageListContext messageListContext)
|
||||
{
|
||||
this.ChatMessage = message;
|
||||
this._messageListContext = messageListContext;
|
||||
}
|
||||
|
||||
public ChatMessage? ChatMessage { get; init; }
|
||||
|
||||
public IList<ChatResponseUpdate>? ResponseUpdates { get; init; }
|
||||
|
||||
public RenderFragment RenderContents() => this.GetOrCreateContentsRenderer();
|
||||
|
||||
internal void SetTemplate(MessageTemplateBase template)
|
||||
{
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
private RenderFragment GetOrCreateContentsRenderer()
|
||||
{
|
||||
if (this._contentsRenderer != null)
|
||||
{
|
||||
return this._contentsRenderer;
|
||||
}
|
||||
|
||||
if (this._template == null)
|
||||
{
|
||||
throw new InvalidOperationException("Message template has not been set for this message context.");
|
||||
}
|
||||
|
||||
this._contentsRenderer = builder =>
|
||||
{
|
||||
if (this.ChatMessage != null)
|
||||
{
|
||||
for (int i = 0; i < this.ChatMessage.Contents.Count; i++)
|
||||
{
|
||||
var content = this.ChatMessage.Contents[i];
|
||||
builder.AddContent(0, this.ResolveContentRenderer(content));
|
||||
}
|
||||
}
|
||||
else if (this.ResponseUpdates != null)
|
||||
{
|
||||
for (int i = 0; i < this.ResponseUpdates.Count; i++)
|
||||
{
|
||||
var update = this.ResponseUpdates[i];
|
||||
for (int j = 0; j < update.Contents.Count; j++)
|
||||
{
|
||||
var content = update.Contents[j];
|
||||
if (this._contentRenderers.TryGetValue(content, out var contentRenderer))
|
||||
{
|
||||
builder.AddContent(0, contentRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("MessageContext must have either a ChatMessage or ResponseUpdates to render contents.");
|
||||
}
|
||||
};
|
||||
|
||||
return this._contentsRenderer;
|
||||
}
|
||||
|
||||
private RenderFragment ResolveContentRenderer(AIContent content)
|
||||
{
|
||||
if (this._contentRenderers.TryGetValue(content, out var contentRenderer))
|
||||
{
|
||||
return contentRenderer;
|
||||
}
|
||||
|
||||
Debug.Assert(this._template != null);
|
||||
contentRenderer = this._template.GetContentTemplate(content);
|
||||
if (contentRenderer != null)
|
||||
{
|
||||
this._contentRenderers[content] = contentRenderer;
|
||||
return contentRenderer;
|
||||
}
|
||||
contentRenderer = this._messageListContext.GetContentTemplate(content);
|
||||
this._contentRenderers[content] = contentRenderer;
|
||||
return contentRenderer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
// Roughly lifted from src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs
|
||||
internal static class MessageHelpers
|
||||
{
|
||||
internal static void CoalesceContent(IList<AIContent> contents)
|
||||
{
|
||||
Coalesce<TextContent>(
|
||||
contents,
|
||||
mergeSingle: false,
|
||||
canMerge: null,
|
||||
static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() });
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: false,
|
||||
canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
TextReasoningContent content = new(MergeText(contents, start, end))
|
||||
{
|
||||
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
|
||||
};
|
||||
|
||||
#if DEBUG
|
||||
for (int i = start; i < end - 1; i++)
|
||||
{
|
||||
Debug.Assert(contents[i] is TextReasoningContent { ProtectedData: null }, "Expected all but the last to have a null ProtectedData");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (((TextReasoningContent)contents[end - 1]).ProtectedData is { } protectedData)
|
||||
{
|
||||
content.ProtectedData = protectedData;
|
||||
}
|
||||
|
||||
return content;
|
||||
});
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: false,
|
||||
canMerge: static (r1, r2) => r1.MediaType == r2.MediaType && r1.HasTopLevelMediaType("text") && r1.Name == r2.Name,
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
Debug.Assert(end - start > 1, "Expected multiple contents to merge");
|
||||
|
||||
MemoryStream ms = new();
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
var current = (DataContent)contents[i];
|
||||
#if NET
|
||||
ms.Write(current.Data.Span);
|
||||
#else
|
||||
if (!MemoryMarshal.TryGetArray(current.Data, out var segment))
|
||||
{
|
||||
segment = new(current.Data.ToArray());
|
||||
}
|
||||
|
||||
ms.Write(segment.Array!, segment.Offset, segment.Count);
|
||||
#endif
|
||||
}
|
||||
|
||||
var first = (DataContent)contents[start];
|
||||
return new DataContent(new ReadOnlyMemory<byte>(ms.GetBuffer(), 0, (int)ms.Length), first.MediaType) { Name = first.Name };
|
||||
});
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: true,
|
||||
canMerge: static (r1, r2) => r1.CallId == r2.CallId,
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
var firstContent = (CodeInterpreterToolCallContent)contents[start];
|
||||
|
||||
if (start == end - 1)
|
||||
{
|
||||
if (firstContent.Inputs is not null)
|
||||
{
|
||||
CoalesceContent(firstContent.Inputs);
|
||||
}
|
||||
|
||||
return firstContent;
|
||||
}
|
||||
|
||||
List<AIContent>? inputs = null;
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
(inputs ??= []).AddRange(((CodeInterpreterToolCallContent)contents[i]).Inputs ?? []);
|
||||
}
|
||||
|
||||
if (inputs is not null)
|
||||
{
|
||||
CoalesceContent(inputs);
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
CallId = firstContent.CallId,
|
||||
Inputs = inputs,
|
||||
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
|
||||
};
|
||||
});
|
||||
|
||||
Coalesce(
|
||||
contents,
|
||||
mergeSingle: true,
|
||||
canMerge: static (r1, r2) => r1.CallId is not null && r2.CallId is not null && r1.CallId == r2.CallId,
|
||||
static (contents, start, end) =>
|
||||
{
|
||||
var firstContent = (CodeInterpreterToolResultContent)contents[start];
|
||||
|
||||
if (start == end - 1)
|
||||
{
|
||||
if (firstContent.Outputs is not null)
|
||||
{
|
||||
CoalesceContent(firstContent.Outputs);
|
||||
}
|
||||
|
||||
return firstContent;
|
||||
}
|
||||
|
||||
List<AIContent>? output = null;
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
(output ??= []).AddRange(((CodeInterpreterToolResultContent)contents[i]).Outputs ?? []);
|
||||
}
|
||||
|
||||
if (output is not null)
|
||||
{
|
||||
CoalesceContent(output);
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
CallId = firstContent.CallId,
|
||||
Outputs = output,
|
||||
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
|
||||
};
|
||||
});
|
||||
|
||||
static string MergeText(IList<AIContent> contents, int start, int end)
|
||||
{
|
||||
Debug.Assert(end - start > 1, "Expected multiple contents to merge");
|
||||
|
||||
StringBuilder sb = new();
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
_ = sb.Append(contents[i]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static void Coalesce<TContent>(
|
||||
IList<AIContent> contents,
|
||||
bool mergeSingle,
|
||||
Func<TContent, TContent, bool>? canMerge,
|
||||
Func<IList<AIContent>, int, int, TContent> merge)
|
||||
where TContent : AIContent
|
||||
{
|
||||
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
|
||||
int start = 0;
|
||||
while (start < contents.Count)
|
||||
{
|
||||
if (!TryAsCoalescable(contents[start], out var firstContent))
|
||||
{
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate until we find a non-coalescable item.
|
||||
int i = start + 1;
|
||||
TContent prev = firstContent;
|
||||
while (i < contents.Count && TryAsCoalescable(contents[i], out TContent? next) && (canMerge is null || canMerge(prev, next)))
|
||||
{
|
||||
i++;
|
||||
prev = next;
|
||||
}
|
||||
|
||||
// If there's only one item in the run, and we don't want to merge single items, skip it.
|
||||
if (start == i - 1 && !mergeSingle)
|
||||
{
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store the replacement node and null out all of the nodes that we coalesced.
|
||||
// We can then remove all coalesced nodes in one O(N) operation via RemoveAll.
|
||||
// Leave start positioned at the start of the next run.
|
||||
contents[start] = merge(contents, start, i);
|
||||
|
||||
start++;
|
||||
while (start < i)
|
||||
{
|
||||
contents[start++] = null!;
|
||||
}
|
||||
|
||||
static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent? coalescable)
|
||||
{
|
||||
if (content is TContent tmp && tmp.Annotations is not { Count: > 0 })
|
||||
{
|
||||
coalescable = tmp;
|
||||
return true;
|
||||
}
|
||||
|
||||
coalescable = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all of the null slots left over from the coalescing process.
|
||||
RemoveNullContents(contents);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveNullContents<T>(IList<T> contents)
|
||||
where T : class
|
||||
{
|
||||
if (contents is List<AIContent> contentsList)
|
||||
{
|
||||
_ = contentsList.RemoveAll(u => u is null);
|
||||
}
|
||||
else
|
||||
{
|
||||
int nextSlot = 0;
|
||||
int contentsCount = contents.Count;
|
||||
for (int i = 0; i < contentsCount; i++)
|
||||
{
|
||||
if (contents[i] is { } content)
|
||||
{
|
||||
contents[nextSlot++] = content;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = contentsCount - 1; i >= nextSlot; i--)
|
||||
{
|
||||
contents.RemoveAt(i);
|
||||
}
|
||||
|
||||
Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool ProcessUpdate(ChatResponseUpdate update, List<ChatMessage> messages)
|
||||
{
|
||||
// If there is no message created yet, or if the last update we saw had a different
|
||||
// identifying parts, create a new message.
|
||||
bool isNewMessage = true;
|
||||
if (messages.Count != 0)
|
||||
{
|
||||
var lastMessage = messages[messages.Count - 1];
|
||||
isNewMessage =
|
||||
NotEmptyOrEqual(update.AuthorName, lastMessage.AuthorName) ||
|
||||
NotEmptyOrEqual(update.MessageId, lastMessage.MessageId) ||
|
||||
NotNullOrEqual(update.Role, lastMessage.Role);
|
||||
}
|
||||
|
||||
// Get the message to target, either a new one or the last ones.
|
||||
ChatMessage message;
|
||||
if (isNewMessage)
|
||||
{
|
||||
message = new(ChatRole.Assistant, []);
|
||||
messages.Add(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
message = messages[messages.Count - 1];
|
||||
}
|
||||
|
||||
// Some members on ChatResponseUpdate map to members of ChatMessage.
|
||||
// Incorporate those into the latest message; in cases where the message
|
||||
// stores a single value, prefer the latest update's value over anything
|
||||
// stored in the message.
|
||||
|
||||
if (update.AuthorName is not null)
|
||||
{
|
||||
message.AuthorName = update.AuthorName;
|
||||
}
|
||||
|
||||
if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt))
|
||||
{
|
||||
message.CreatedAt = update.CreatedAt;
|
||||
}
|
||||
|
||||
if (update.Role is ChatRole role)
|
||||
{
|
||||
message.Role = role;
|
||||
}
|
||||
|
||||
if (update.MessageId is { Length: > 0 })
|
||||
{
|
||||
// Note that this must come after the message checks earlier, as they depend
|
||||
// on this value for change detection.
|
||||
message.MessageId = update.MessageId;
|
||||
}
|
||||
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
message.Contents.Add(content);
|
||||
}
|
||||
|
||||
return isNewMessage;
|
||||
}
|
||||
|
||||
/// <summary>Gets whether both strings are not null/empty and not the same as each other.</summary>
|
||||
private static bool NotEmptyOrEqual(string? s1, string? s2) =>
|
||||
s1 is { Length: > 0 } str1 && s2 is { Length: > 0 } str2 && str1 != str2;
|
||||
|
||||
/// <summary>Gets whether two roles are not null and not the same as each other.</summary>
|
||||
private static bool NotNullOrEqual(ChatRole? r1, ChatRole? r2) =>
|
||||
r1.HasValue && r2.HasValue && r1.Value != r2.Value;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class is apparently never instantiated
|
||||
internal sealed partial class MessageList : IComponent, IDisposable
|
||||
#pragma warning restore CA1812 // Internal class is apparently never instantiated
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private MessageSubscription _messageSubscription;
|
||||
private ResponseUpdateSubscription _responseSubscription;
|
||||
|
||||
[CascadingParameter] public MessageListContext MessageListContext { get; set; } = default!;
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
var previousContext = this.MessageListContext;
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (previousContext != null && this.MessageListContext != previousContext)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(MessageList)} does not support changing the {nameof(this.MessageListContext)} once it has been set.");
|
||||
}
|
||||
|
||||
// Subscribe to message updates and response updates for streaming
|
||||
this._messageSubscription = this.MessageListContext.AgentBoundaryContext.SubscribeToMessageChanges(this.ProcessUpdate);
|
||||
this._responseSubscription = this.MessageListContext.AgentBoundaryContext.SubscribeToResponseUpdates(this.ProcessUpdate);
|
||||
|
||||
// Initial render. This component will only render once since the only parameter is a cascading parameter and it's fixed.
|
||||
this._renderHandle.Render(this.Render);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ProcessUpdate()
|
||||
{
|
||||
// Scan all messages for function calls and results to track invocations.
|
||||
// This allows us to associate results with their calls when results arrive.
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.CompletedMessages)
|
||||
{
|
||||
this.ProcessMessageContents(message);
|
||||
}
|
||||
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.PendingMessages)
|
||||
{
|
||||
this.ProcessMessageContents(message);
|
||||
}
|
||||
|
||||
this._renderHandle.Render(this.Render);
|
||||
}
|
||||
|
||||
private void ProcessMessageContents(ChatMessage message)
|
||||
{
|
||||
foreach (var content in message.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent call)
|
||||
{
|
||||
this.MessageListContext.GetOrCreateInvocation(call);
|
||||
}
|
||||
else if (content is FunctionResultContent result)
|
||||
{
|
||||
this.MessageListContext.AssociateResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(RenderTreeBuilder builder)
|
||||
{
|
||||
// Track all render keys to detect duplicates
|
||||
var allRenderKeys = new HashSet<string?>();
|
||||
|
||||
// Open container div for message list with flex layout
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "messages-container");
|
||||
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.CompletedMessages)
|
||||
{
|
||||
var renderKey = GetUniqueRenderKey(message);
|
||||
|
||||
// Calling GetTemplate will stop template collection on the first message if it
|
||||
// was still ongoing.
|
||||
builder.OpenComponent<ContentBlock>(2);
|
||||
builder.SetKey(renderKey);
|
||||
builder.AddComponentParameter(3, "ChildContent", this.MessageListContext.GetTemplate(message));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
foreach (var message in this.MessageListContext.AgentBoundaryContext.PendingMessages)
|
||||
{
|
||||
var renderKey = GetUniqueRenderKey(message);
|
||||
|
||||
builder.OpenComponent<ContentBlock>(4);
|
||||
builder.SetKey(renderKey);
|
||||
builder.AddComponentParameter(5, "ChildContent", this.MessageListContext.GetTemplate(message));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
// Close container div
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unique render key for a message.
|
||||
/// For tool result messages (role=tool), we use MessageId + CallId to ensure uniqueness
|
||||
/// because the AGUI protocol may reuse MessageId for multiple tool results in the same batch.
|
||||
/// </summary>
|
||||
private static string? GetUniqueRenderKey(ChatMessage message)
|
||||
{
|
||||
// For tool result messages, combine MessageId with CallId to ensure uniqueness
|
||||
// This works around a bug in the AGUI protocol where multiple tool results
|
||||
// can share the same MessageId when processed in the same update batch.
|
||||
if (message.Role == ChatRole.Tool)
|
||||
{
|
||||
var resultContent = message.Contents.OfType<FunctionResultContent>().FirstOrDefault();
|
||||
if (resultContent != null && !string.IsNullOrEmpty(resultContent.CallId))
|
||||
{
|
||||
return $"{message.MessageId}_{resultContent.CallId}";
|
||||
}
|
||||
}
|
||||
|
||||
return message.MessageId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
((IDisposable)this._messageSubscription).Dispose();
|
||||
((IDisposable)this._responseSubscription).Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public sealed partial class MessageListContext
|
||||
{
|
||||
private bool _collectingTemplates;
|
||||
|
||||
private readonly List<MessageTemplateBase> _templates = [];
|
||||
private readonly List<ContentTemplateBase> _contentTemplates = [];
|
||||
|
||||
// We compute a render fragment to render each message only once and cache it here.
|
||||
private readonly Dictionary<ChatMessage, RenderFragment> _templateCache = [];
|
||||
|
||||
// Track function invocations by CallId to associate calls with results.
|
||||
private readonly Dictionary<string, InvocationContext> _invocationMap = [];
|
||||
|
||||
public MessageListContext(IAgentBoundaryContext context)
|
||||
{
|
||||
this.AgentBoundaryContext = context;
|
||||
}
|
||||
|
||||
public IAgentBoundaryContext AgentBoundaryContext { get; }
|
||||
|
||||
public void BeginCollectingTemplates()
|
||||
{
|
||||
// This is triggered by the Messages component before rendering its children.
|
||||
// In this situation we are going to render again the MessageTemplates and
|
||||
// ContentTemplates and since we can't tell if they have changed we have to
|
||||
// recompute all the templates again.
|
||||
this._collectingTemplates = true;
|
||||
this._templates.Clear();
|
||||
this._contentTemplates.Clear();
|
||||
this._templateCache.Clear();
|
||||
}
|
||||
|
||||
public void RegisterTemplate(MessageTemplateBase template)
|
||||
{
|
||||
if (this._collectingTemplates)
|
||||
{
|
||||
this._templates.Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterContentTemplate(ContentTemplateBase template)
|
||||
{
|
||||
if (this._collectingTemplates)
|
||||
{
|
||||
this._contentTemplates.Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an invocation context for the given function call.
|
||||
/// </summary>
|
||||
/// <param name="call">The function call content.</param>
|
||||
/// <returns>The invocation context for this call.</returns>
|
||||
public InvocationContext GetOrCreateInvocation(FunctionCallContent call)
|
||||
{
|
||||
if (!this._invocationMap.TryGetValue(call.CallId, out var context))
|
||||
{
|
||||
context = new InvocationContext(call);
|
||||
this._invocationMap[call.CallId] = context;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Associates a function result with its corresponding call.
|
||||
/// </summary>
|
||||
/// <param name="result">The function result content.</param>
|
||||
public void AssociateResult(FunctionResultContent result)
|
||||
{
|
||||
if (this._invocationMap.TryGetValue(result.CallId, out var context))
|
||||
{
|
||||
context.SetResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an invocation context by call ID.
|
||||
/// </summary>
|
||||
/// <param name="callId">The call ID.</param>
|
||||
/// <returns>The invocation context, or null if not found.</returns>
|
||||
public InvocationContext? GetInvocation(string callId)
|
||||
{
|
||||
return this._invocationMap.TryGetValue(callId, out var context) ? context : null;
|
||||
}
|
||||
|
||||
internal RenderFragment GetTemplate(ChatMessage message)
|
||||
{
|
||||
// We are about to render the first message. If we were collecting templates, stop now.
|
||||
this._collectingTemplates = false;
|
||||
if (this._templateCache.TryGetValue(message, out var cachedTemplate))
|
||||
{
|
||||
return cachedTemplate;
|
||||
}
|
||||
|
||||
var messageContext = new MessageContext(message, this);
|
||||
foreach (var template in this._templates)
|
||||
{
|
||||
if (template.When(messageContext))
|
||||
{
|
||||
var chosen = template;
|
||||
messageContext.SetTemplate(chosen);
|
||||
// We ask the template to create a RenderFragment for the message.
|
||||
// The template will render a wrapper and use the messageContext to
|
||||
// render the contents.
|
||||
// The template might call back through the messageContext to get renderers for
|
||||
// contents if the message template doesn't override the full rendering or
|
||||
// if it doesn't define the rendering for a content type.
|
||||
var renderer = chosen.ChildContent(messageContext);
|
||||
this._templateCache[message] = renderer;
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"No message template found for message of type {message.Role}.");
|
||||
}
|
||||
|
||||
internal RenderFragment GetContentTemplate(AIContent content)
|
||||
{
|
||||
foreach (var template in this._contentTemplates)
|
||||
{
|
||||
var contentContext = new ContentContext(content);
|
||||
if (template.When(contentContext))
|
||||
{
|
||||
return template.ChildContent(contentContext);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"No content template found for content of type {content.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public abstract class MessageTemplateBase : IComponent
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public abstract bool When(MessageContext context);
|
||||
|
||||
[Parameter] public RenderFragment<MessageContext> ChildContent { get; set; } = (message) => builder => { };
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal RenderFragment? GetContentTemplate(AIContent content)
|
||||
{
|
||||
return this.Context.GetContentTemplate(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
public partial class Messages : IComponent
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private RenderFragment? _renderContents;
|
||||
|
||||
private IAgentBoundaryContext? _context;
|
||||
private MessageListContext? _messageListContext;
|
||||
|
||||
[CascadingParameter] public IAgentBoundaryContext? AgentContext { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment MessageTemplates { get; set; } = builder =>
|
||||
{
|
||||
builder.OpenComponent<DefaultMessageTemplate>(0);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ContentTemplates { get; set; } = builder =>
|
||||
{
|
||||
builder.OpenComponent<TextTemplate>(0);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<FunctionCallTemplate>(1);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<FunctionResultTemplate>(2);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<ErrorTemplate>(3);
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<DataContentTemplate>(4);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this._renderHandle = renderHandle;
|
||||
this._renderContents = this.RenderContents;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (this.AgentContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("Messages component must be used within an AgentBoundary.");
|
||||
}
|
||||
|
||||
if (this._context != null && this.AgentContext != this._context)
|
||||
{
|
||||
throw new InvalidOperationException("Messages component cannot change AgentBoundaryContext.");
|
||||
}
|
||||
|
||||
this._context = this.AgentContext;
|
||||
|
||||
if (this._messageListContext == null)
|
||||
{
|
||||
this._messageListContext = new MessageListContext(this.AgentContext);
|
||||
this.Render();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
this._renderHandle.Render(this.RenderCore);
|
||||
}
|
||||
|
||||
private void RenderCore(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<MessageListContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", this._messageListContext);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", this._renderContents);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderContents(RenderTreeBuilder builder)
|
||||
{
|
||||
Debug.Assert(this._messageListContext != null);
|
||||
this._messageListContext.BeginCollectingTemplates();
|
||||
builder.AddContent(1, this.MessageTemplates);
|
||||
builder.AddContent(2, this.ContentTemplates);
|
||||
builder.OpenComponent<MessageList>(3);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
/* ==========================================================================
|
||||
AG-UI Components - Shared Styles
|
||||
These styles provide consistent appearance for AI components across demos.
|
||||
========================================================================== */
|
||||
|
||||
/* ==========================================================================
|
||||
Agent Input Component
|
||||
========================================================================== */
|
||||
|
||||
.agent-input {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-input textarea {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
resize: none;
|
||||
min-height: 44px;
|
||||
max-height: 200px;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.agent-input textarea:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.agent-input textarea::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.agent-input textarea:disabled {
|
||||
background-color: #f9fafb;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.agent-input .send-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-input .send-button:hover:not(:disabled) {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.agent-input .send-button:disabled {
|
||||
background-color: #d1d5db;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.agent-input .send-button svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Agent Suggestions Component
|
||||
========================================================================== */
|
||||
|
||||
.agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.suggestion-button:hover:not(:disabled) {
|
||||
background: #f0f0f0;
|
||||
border-color: #0078d4;
|
||||
color: #0078d4;
|
||||
}
|
||||
|
||||
.suggestion-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Loading Indicator - Triple Dot Animation
|
||||
========================================================================== */
|
||||
|
||||
.agent-loading-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agent-loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: agent-dot-bounce 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.agent-loading-dots span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.agent-loading-dots span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.agent-loading-dots span:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes agent-dot-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline loading indicator for use within messages */
|
||||
.agent-loading-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.agent-loading-inline span {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: #6b7280;
|
||||
border-radius: 50%;
|
||||
animation: agent-dot-bounce 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.agent-loading-inline span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.agent-loading-inline span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.agent-loading-inline span:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Processing State Overlay
|
||||
========================================================================== */
|
||||
|
||||
.agent-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-input-container.processing .agent-input textarea {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Message Styles
|
||||
========================================================================== */
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-assistant {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 80%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-user .message-content {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message-assistant .message-content {
|
||||
background-color: #f3f4f6;
|
||||
color: #1f2937;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Common Chat Layout Styles
|
||||
========================================================================== */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="AGUIDojoClient.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet @rendermode="@renderMode" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="@renderMode" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@code {
|
||||
private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Services
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject IBackgroundColorService BackgroundColorService
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Agentic Chat</PageTitle>
|
||||
|
||||
<div class="chat-layout" style="@GetBackgroundStyle()">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">AGUI WebChat</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversationAsync">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent">
|
||||
<div class="chat-content">
|
||||
<Messages />
|
||||
<AgentLoadingIndicator />
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Type your message..." />
|
||||
</div>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private string? _backgroundColor;
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Change background", new ChatMessage(ChatRole.User, "Change background to light blue")),
|
||||
new Suggestion("Generate sonnet")
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "agentic_chat";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("agentic-chat");
|
||||
BackgroundColorService.ColorChanged += OnColorChanged;
|
||||
}
|
||||
|
||||
private string GetBackgroundStyle()
|
||||
{
|
||||
return _backgroundColor != null ? $"background-color: {_backgroundColor}" : "";
|
||||
}
|
||||
|
||||
private async void OnColorChanged(object? sender, BackgroundColorChangedEventArgs e)
|
||||
{
|
||||
_backgroundColor = e.Color;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void ResetConversationAsync()
|
||||
{
|
||||
// Reset would need to be implemented in AgentBoundary
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BackgroundColorService.ColorChanged -= OnColorChanged;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1rem 2rem 1.5rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.agentic-chat-demo {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
padding: 0 2rem 1.5rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Components.Shared
|
||||
@using System.Text.Json
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<PageTitle>Agentic Generative UI</PageTitle>
|
||||
|
||||
<div class="chat-layout">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">Agentic Generative UI</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversation">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent">
|
||||
<AgentState TState="Plan"
|
||||
CurrentState="@currentPlan"
|
||||
OnSnapshot="@DeserializePlan"
|
||||
OnDelta="@ApplyPlanDelta"
|
||||
CurrentStateChanged="@OnPlanChanged">
|
||||
<div class="chat-content">
|
||||
<Messages>
|
||||
<ContentTemplates>
|
||||
<TaskProgressTemplate />
|
||||
<TextTemplate />
|
||||
<FunctionCallTemplate />
|
||||
<FunctionResultTemplate />
|
||||
<ErrorTemplate />
|
||||
<DataContentTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
<AgentLoadingIndicator />
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Ask me to plan something..." />
|
||||
</div>
|
||||
</AgentState>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private Plan? currentPlan;
|
||||
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Simple plan", new ChatMessage(ChatRole.User, "Please build a plan to go to mars in 5 steps.")),
|
||||
new Suggestion("Complex plan", new ChatMessage(ChatRole.User, "Please build a plan to make pizza in 10 steps."))
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "agentic_generative_ui";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("agentic-generative-ui");
|
||||
}
|
||||
|
||||
private Plan? DeserializePlan(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<Plan>(data.Span);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Plan? ApplyPlanDelta(Plan? current, ReadOnlyMemory<byte> deltaData)
|
||||
{
|
||||
if (current is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var operations = JsonSerializer.Deserialize<List<JsonPatchOperation>>(deltaData.Span);
|
||||
if (operations is not null)
|
||||
{
|
||||
PlanPatcher.Apply(current, operations);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore deserialization errors
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private void OnPlanChanged(Plan? plan)
|
||||
{
|
||||
currentPlan = plan;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ResetConversation()
|
||||
{
|
||||
currentPlan = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
|
||||
::deep .agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
::deep .suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
::deep .suggestion-button:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #0078d4;
|
||||
color: #0078d4;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON Patch operation (RFC 6902).
|
||||
/// </summary>
|
||||
public sealed class JsonPatchOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation to perform (e.g., "replace", "add", "remove").
|
||||
/// </summary>
|
||||
[JsonPropertyName("op")]
|
||||
public string Op { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON Pointer path to the target location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The value for the operation (used with "replace", "add", "test").
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a plan with multiple steps.
|
||||
/// </summary>
|
||||
public sealed class Plan
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of steps in the plan.
|
||||
/// </summary>
|
||||
[JsonPropertyName("steps")]
|
||||
public List<Step> Steps { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of completed steps.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int CompletedCount => this.Steps.Count(s => s.IsCompleted);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of steps.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int TotalCount => this.Steps.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether all steps are completed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsComplete => this.Steps.Count > 0 && this.Steps.All(s => s.IsCompleted);
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Applies JSON Patch operations to a Plan object.
|
||||
/// </summary>
|
||||
public static partial class PlanPatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a list of JSON Patch operations to the given plan.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to modify.</param>
|
||||
/// <param name="operations">The patch operations to apply.</param>
|
||||
public static void Apply(Plan plan, IEnumerable<JsonPatchOperation> operations)
|
||||
{
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
ApplyOperation(plan, operation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyOperation(Plan plan, JsonPatchOperation operation)
|
||||
{
|
||||
// Parse paths like "/steps/0/status" or "/steps/0/description"
|
||||
var match = StepPathRegex().Match(operation.Path);
|
||||
if (!match.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(match.Groups["index"].Value, out var index))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= plan.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var property = match.Groups["property"].Value;
|
||||
var step = plan.Steps[index];
|
||||
|
||||
if (string.Equals(operation.Op, "replace", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(property, "status", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
step.Status = GetStringValue(operation.Value) ?? step.Status;
|
||||
}
|
||||
else if (string.Equals(property, "description", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
step.Description = GetStringValue(operation.Value) ?? step.Description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetStringValue(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
string s => s,
|
||||
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
|
||||
_ => value?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^/steps/(?<index>\d+)/(?<property>\w+)$")]
|
||||
private static partial Regex StepPathRegex();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.AgenticGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single step in a plan.
|
||||
/// </summary>
|
||||
public sealed class Step
|
||||
{
|
||||
/// <summary>
|
||||
/// The description of the step.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The status of the step (pending or completed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "pending";
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this step is completed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsCompleted => string.Equals(this.Status, "completed", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
|
||||
@if (Plan is not null && Plan.Steps.Count > 0)
|
||||
{
|
||||
<div class="task-progress-card @(Plan.IsComplete ? "completed" : "in-progress")">
|
||||
<div class="task-header">
|
||||
<h3>Task Progress</h3>
|
||||
<span class="task-counter">@Plan.CompletedCount/@Plan.TotalCount Complete</span>
|
||||
</div>
|
||||
<div class="task-steps">
|
||||
@for (int i = 0; i < Plan.Steps.Count; i++)
|
||||
{
|
||||
var step = Plan.Steps[i];
|
||||
var isCurrentStep = !step.IsCompleted &&
|
||||
(i == 0 || Plan.Steps[i - 1].IsCompleted);
|
||||
|
||||
<div class="task-step @(step.IsCompleted ? "step-completed" : isCurrentStep ? "step-current" : "step-pending")">
|
||||
<span class="step-icon">
|
||||
@if (step.IsCompleted)
|
||||
{
|
||||
<span class="check-icon">âś“</span>
|
||||
}
|
||||
else if (isCurrentStep)
|
||||
{
|
||||
<span class="current-icon">â—Ź</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="pending-icon">â—‹</span>
|
||||
}
|
||||
</span>
|
||||
<span class="step-description">@step.Description</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
public Plan? Plan { get; set; }
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.task-progress-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin: 8px 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.task-progress-card.completed {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
border-color: #86efac;
|
||||
}
|
||||
|
||||
.task-progress-card.in-progress {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.task-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.task-counter {
|
||||
font-size: 0.875rem;
|
||||
color: #666666;
|
||||
background: #f5f5f5;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.task-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-step.step-completed {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
.task-step.step-current {
|
||||
background: rgba(0, 120, 212, 0.1);
|
||||
border-left: 3px solid #0078d4;
|
||||
animation: pulse-border 2s infinite;
|
||||
}
|
||||
|
||||
.task-step.step-pending {
|
||||
background: #fafafa;
|
||||
border-left: 3px solid #d1d1d1;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%, 100% {
|
||||
border-left-color: #0078d4;
|
||||
box-shadow: 0 0 0 0 rgba(0, 120, 212, 0.4);
|
||||
}
|
||||
50% {
|
||||
border-left-color: #50a0e8;
|
||||
box-shadow: 0 0 8px 0 rgba(0, 120, 212, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.current-icon {
|
||||
color: #0078d4;
|
||||
font-size: 12px;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.pending-icon {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.step-description {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
.step-completed .step-description {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.step-pending .step-description {
|
||||
color: #666666;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Components.Shared
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<PageTitle>Backend Tool Rendering</PageTitle>
|
||||
|
||||
<div class="chat-layout">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">Backend Tool Rendering</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversation">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent">
|
||||
<div class="chat-content">
|
||||
<Messages>
|
||||
<ContentTemplates>
|
||||
<TextTemplate />
|
||||
<WeatherCallTemplate />
|
||||
<FunctionCallTemplate />
|
||||
<FunctionResultTemplate />
|
||||
<ErrorTemplate />
|
||||
<DataContentTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
<AgentLoadingIndicator />
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Ask about the weather..." />
|
||||
</div>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Weather in San Francisco", new ChatMessage(ChatRole.User, "What's the weather like in San Francisco?")),
|
||||
new Suggestion("Weather in New York", new ChatMessage(ChatRole.User, "What's the weather like in New York?")),
|
||||
new Suggestion("Weather in Tokyo", new ChatMessage(ChatRole.User, "What's the weather like in Tokyo?"))
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "backend_tool_rendering";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("backend-tool-rendering");
|
||||
}
|
||||
|
||||
private void ResetConversation()
|
||||
{
|
||||
// Reset would need to be implemented - for now just trigger re-render
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
|
||||
::deep .agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
::deep .suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
::deep .suggestion-button:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #0078d4;
|
||||
color: #0078d4;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Template for rendering weather function call content with its result.
|
||||
/// Uses InvocationContext to access both the call arguments and result.
|
||||
/// </summary>
|
||||
public class WeatherCallTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderWeatherCall;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this template should handle the given content.
|
||||
/// Matches FunctionCallContent for the get_weather function.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
// Only match FunctionCallContent for the get_weather function
|
||||
return context.Content is FunctionCallContent call &&
|
||||
string.Equals(call.Name, "get_weather", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private RenderFragment RenderWeatherCall(ContentContext content) => builder =>
|
||||
{
|
||||
if (content.Content is FunctionCallContent call)
|
||||
{
|
||||
// Get the invocation context which tracks both call and result
|
||||
var invocation = this.Context.GetOrCreateInvocation(call);
|
||||
|
||||
// Provide the invocation context to child components
|
||||
builder.OpenComponent<CascadingValue<InvocationContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", invocation);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", (RenderFragment)(innerBuilder =>
|
||||
{
|
||||
// Render the WeatherCard component which uses InvocationContext
|
||||
innerBuilder.OpenComponent<AGUIDojoClient.Components.Demos.BackendToolRendering.WeatherCard>(0);
|
||||
innerBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using AGUIDojoClient.Components.Shared
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
|
||||
@if (Weather is null)
|
||||
{
|
||||
<div class="weather-card weather-loading">
|
||||
<div class="weather-header">
|
||||
<div class="weather-location">
|
||||
<h3>@Location</h3>
|
||||
<p>Loading weather...</p>
|
||||
</div>
|
||||
<span class="weather-icon skeleton-icon"></span>
|
||||
</div>
|
||||
|
||||
<div class="weather-main">
|
||||
<div class="temperature">
|
||||
<span class="temp-value skeleton-text skeleton-temp"></span>
|
||||
<span class="temp-fahrenheit skeleton-text skeleton-temp-f"></span>
|
||||
</div>
|
||||
<div class="conditions skeleton-text skeleton-conditions"></div>
|
||||
</div>
|
||||
|
||||
<div class="weather-details">
|
||||
<div class="detail-item">
|
||||
<p class="detail-label">Humidity</p>
|
||||
<p class="detail-value skeleton-text skeleton-detail"></p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<p class="detail-label">Wind</p>
|
||||
<p class="detail-value skeleton-text skeleton-detail"></p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<p class="detail-label">Feels Like</p>
|
||||
<p class="detail-value skeleton-text skeleton-detail"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="weather-card @GetConditionClass()">
|
||||
<div class="weather-header">
|
||||
<div class="weather-location">
|
||||
<h3>@Location</h3>
|
||||
<p>Current Weather</p>
|
||||
</div>
|
||||
<span class="weather-icon">@Weather.ConditionIcon</span>
|
||||
</div>
|
||||
|
||||
<div class="weather-main">
|
||||
<div class="temperature">
|
||||
<span class="temp-value">@Weather.Temperature° C</span>
|
||||
<span class="temp-fahrenheit">/ @Weather.TemperatureFahrenheit.ToString("F1")° F</span>
|
||||
</div>
|
||||
<div class="conditions">@Weather.Conditions</div>
|
||||
</div>
|
||||
|
||||
<div class="weather-details">
|
||||
<div class="detail-item">
|
||||
<p class="detail-label">Humidity</p>
|
||||
<p class="detail-value">@(Weather.Humidity)%</p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<p class="detail-label">Wind</p>
|
||||
<p class="detail-value">@Weather.WindSpeed mph</p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<p class="detail-label">Feels Like</p>
|
||||
<p class="detail-value">@(Weather.FeelsLike)°</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
public InvocationContext Invocation { get; set; } = default!;
|
||||
|
||||
private string Location => Invocation?.GetArgument<string>("location") ?? "Unknown Location";
|
||||
|
||||
private WeatherInfo? Weather => Invocation?.HasResult == true
|
||||
? Invocation.GetResult<WeatherInfo>()
|
||||
: null;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Invocation is not null && !Invocation.HasResult)
|
||||
{
|
||||
Invocation.ResultArrived += OnResultArrived;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResultArrived()
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private string GetConditionClass()
|
||||
{
|
||||
if (Weather is null)
|
||||
{
|
||||
return "condition-default";
|
||||
}
|
||||
|
||||
return Weather.Conditions.ToLowerInvariant() switch
|
||||
{
|
||||
"sunny" or "clear" => "condition-sunny",
|
||||
"cloudy" or "overcast" => "condition-cloudy",
|
||||
"rainy" or "rain" => "condition-rainy",
|
||||
"stormy" or "thunderstorm" => "condition-stormy",
|
||||
"snowy" or "snow" => "condition-snowy",
|
||||
"foggy" or "fog" => "condition-foggy",
|
||||
_ => "condition-default"
|
||||
};
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
.weather-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
max-width: 320px;
|
||||
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.4);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.weather-card.condition-sunny {
|
||||
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
|
||||
}
|
||||
|
||||
.weather-card.condition-cloudy {
|
||||
background: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%);
|
||||
}
|
||||
|
||||
.weather-card.condition-rainy {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.weather-card.condition-stormy {
|
||||
background: linear-gradient(135deg, #373b44 0%, #4286f4 100%);
|
||||
}
|
||||
|
||||
.weather-card.condition-snowy {
|
||||
background: linear-gradient(135deg, #e6dada 0%, #274046 100%);
|
||||
}
|
||||
|
||||
.weather-card.condition-foggy {
|
||||
background: linear-gradient(135deg, #606c88 0%, #3f4c6b 100%);
|
||||
}
|
||||
|
||||
.weather-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.weather-location h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weather-location p {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.weather-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.weather-main {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.temperature {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.temp-value {
|
||||
font-size: 3rem;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.temp-fahrenheit {
|
||||
font-size: 1rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.conditions {
|
||||
font-size: 1.1rem;
|
||||
text-transform: capitalize;
|
||||
margin-top: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.weather-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Loading/Skeleton state */
|
||||
.weather-loading {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.skeleton-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.skeleton-temp {
|
||||
width: 80px;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.skeleton-temp-f {
|
||||
width: 60px;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.skeleton-conditions {
|
||||
width: 100px;
|
||||
height: 1.1rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.skeleton-detail {
|
||||
width: 40px;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
|
||||
|
||||
/// <summary>
|
||||
/// Template for rendering create_plan function call content.
|
||||
/// The PlanCard will subscribe to events to track confirm_plan and update_plan_step calls.
|
||||
/// </summary>
|
||||
public class CreatePlanCallTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderCreatePlanCall;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this template should handle the given content.
|
||||
/// Matches FunctionCallContent for the create_plan function.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
// Only match FunctionCallContent for the create_plan function
|
||||
return context.Content is FunctionCallContent call &&
|
||||
string.Equals(call.Name, "create_plan", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private RenderFragment RenderCreatePlanCall(ContentContext content) => builder =>
|
||||
{
|
||||
if (content.Content is FunctionCallContent call)
|
||||
{
|
||||
// Get the invocation context which tracks both call and result
|
||||
var invocation = this.Context.GetOrCreateInvocation(call);
|
||||
|
||||
// Provide both the invocation context and message list context to PlanCard
|
||||
builder.OpenComponent<CascadingValue<InvocationContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", invocation);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", (RenderFragment)(innerBuilder =>
|
||||
{
|
||||
// Also cascade the MessageListContext so PlanCard can track other tool calls
|
||||
innerBuilder.OpenComponent<CascadingValue<MessageListContext>>(0);
|
||||
innerBuilder.AddComponentParameter(1, "Value", this.Context);
|
||||
innerBuilder.AddComponentParameter(2, "IsFixed", true);
|
||||
innerBuilder.AddComponentParameter(3, "ChildContent", (RenderFragment)(cardBuilder =>
|
||||
{
|
||||
// Render the PlanCard component
|
||||
cardBuilder.OpenComponent<PlanCard>(0);
|
||||
cardBuilder.CloseComponent();
|
||||
}));
|
||||
innerBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that prepends instructions for the human-in-the-loop workflow.
|
||||
/// </summary>
|
||||
internal sealed class HumanInTheLoopAgent : DelegatingAIAgent
|
||||
{
|
||||
private static readonly ChatMessage InstructionsMessage = new(
|
||||
ChatRole.System,
|
||||
"""
|
||||
You help users create and execute plans. Follow this workflow:
|
||||
|
||||
1. When asked to create a plan, use the `create_plan` tool with a list of step descriptions.
|
||||
2. IMMEDIATELY after creating a plan, call `confirm_plan` with the plan object to ask for user approval.
|
||||
3. Wait for the user to confirm which steps they want to proceed with.
|
||||
4. Once confirmed, use `update_plan_step` to mark steps as 'completed' as you execute them.
|
||||
|
||||
IMPORTANT:
|
||||
- Always call `confirm_plan` right after `create_plan` - don't skip this step!
|
||||
- The plan parameter for `confirm_plan` should be the exact plan object returned from `create_plan`.
|
||||
- Do NOT start executing steps until the user confirms.
|
||||
- After receiving confirmation, update each selected step to 'completed' status.
|
||||
""");
|
||||
|
||||
public HumanInTheLoopAgent(AIAgent innerAgent)
|
||||
: base(innerAgent)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Prepend instructions message
|
||||
var messagesWithInstructions = messages.Prepend(InstructionsMessage);
|
||||
return base.RunAsync(messagesWithInstructions, thread, options, cancellationToken);
|
||||
}
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Prepend instructions message
|
||||
var messagesWithInstructions = messages.Prepend(InstructionsMessage);
|
||||
return base.RunStreamingAsync(messagesWithInstructions, thread, options, cancellationToken);
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Components.Shared
|
||||
@using System.Text.Json
|
||||
@using System.ComponentModel
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<PageTitle>Human in the Loop</PageTitle>
|
||||
|
||||
<div class="chat-layout">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">Human in the Loop</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversation">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent" OnContextCreated="OnContextCreated">
|
||||
<div class="chat-content">
|
||||
<Messages>
|
||||
<ContentTemplates>
|
||||
<CreatePlanCallTemplate />
|
||||
<TextTemplate />
|
||||
<FunctionCallTemplate />
|
||||
<FunctionResultTemplate />
|
||||
<ErrorTemplate />
|
||||
<DataContentTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
<AgentLoadingIndicator />
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Ask for a plan..." />
|
||||
</div>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private IAgentBoundaryContext? boundaryContext;
|
||||
private Plan? currentPlan;
|
||||
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Simple plan", new ChatMessage(ChatRole.User, "Create a simple 5-step plan for organizing a birthday party")),
|
||||
new Suggestion("Complex plan", new ChatMessage(ChatRole.User, "Create a detailed 10-step plan for launching a new product"))
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "human_in_the_loop";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("human-in-the-loop");
|
||||
}
|
||||
|
||||
private void OnContextCreated(IAgentBoundaryContext context)
|
||||
{
|
||||
boundaryContext = context;
|
||||
|
||||
// Register all three frontend tools for the human-in-the-loop scenario
|
||||
|
||||
// 1. create_plan - creates a plan and stores it
|
||||
var createPlanTool = AIFunctionFactory.Create(
|
||||
(List<string> steps) => CreatePlan(steps),
|
||||
"create_plan",
|
||||
"Create a plan with multiple steps. Call this first before confirm_plan.");
|
||||
|
||||
// 2. confirm_plan - waits for user confirmation via UI, receives the plan to display
|
||||
var confirmPlanTool = AIFunctionFactory.Create(
|
||||
(Plan plan) => ConfirmPlanAsync(context, plan),
|
||||
"confirm_plan",
|
||||
"Present the plan to the user for confirmation. The user can select which steps to proceed with. Pass the plan returned from create_plan.");
|
||||
|
||||
// 3. update_plan_step - updates a step's status
|
||||
var updatePlanStepTool = AIFunctionFactory.Create(
|
||||
(int index, string? description, string? status) => UpdatePlanStep(index, description, status),
|
||||
"update_plan_step",
|
||||
"Update a step in the plan with new description or status. Use status 'completed' to mark a step as done.");
|
||||
|
||||
context.RegisterTools(createPlanTool, confirmPlanTool, updatePlanStepTool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frontend tool that creates a plan with the given steps.
|
||||
/// </summary>
|
||||
[Description("Create a plan with multiple steps.")]
|
||||
private Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
|
||||
{
|
||||
currentPlan = new Plan
|
||||
{
|
||||
Steps = [.. steps.Select(s => new Step { Description = s, Status = "pending" })]
|
||||
};
|
||||
return currentPlan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frontend tool that waits for user confirmation via the UI.
|
||||
/// The PlanCard component will call ProvideResponse when the user confirms/rejects.
|
||||
/// </summary>
|
||||
[Description("Present the plan to the user for confirmation.")]
|
||||
private static async Task<PlanConfirmationResult> ConfirmPlanAsync(
|
||||
IAgentBoundaryContext context,
|
||||
[Description("The plan to present to the user for confirmation.")] Plan plan)
|
||||
{
|
||||
// The plan parameter is received and will be accessible via InvocationContext in the PlanCard
|
||||
// Wait for the PlanCard component to provide the response
|
||||
var response = await context.WaitForResponse("confirm_plan");
|
||||
return (PlanConfirmationResult)response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frontend tool that updates a step in the plan.
|
||||
/// </summary>
|
||||
[Description("Update a step in the plan with new description or status.")]
|
||||
private List<JsonPatchOperation> UpdatePlanStep(
|
||||
[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: 'pending' or 'completed'.")] string? status = null)
|
||||
{
|
||||
var changes = new List<JsonPatchOperation>();
|
||||
|
||||
if (currentPlan is null || index < 0 || index >= currentPlan.Steps.Count)
|
||||
{
|
||||
return changes;
|
||||
}
|
||||
|
||||
if (description is not null)
|
||||
{
|
||||
currentPlan.Steps[index].Description = description;
|
||||
changes.Add(new JsonPatchOperation
|
||||
{
|
||||
Op = "replace",
|
||||
Path = $"/steps/{index}/description",
|
||||
Value = description
|
||||
});
|
||||
}
|
||||
|
||||
if (status is not null)
|
||||
{
|
||||
currentPlan.Steps[index].Status = status.ToLowerInvariant();
|
||||
changes.Add(new JsonPatchOperation
|
||||
{
|
||||
Op = "replace",
|
||||
Path = $"/steps/{index}/status",
|
||||
Value = status.ToLowerInvariant()
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private void ResetConversation()
|
||||
{
|
||||
currentPlan = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/* Copyright (c) Microsoft. All rights reserved. */
|
||||
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
background: white;
|
||||
}
|
||||
|
||||
::deep .agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
::deep .suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #424242;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
::deep .suggestion-button:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #0078d4;
|
||||
color: #0078d4;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON Patch operation.
|
||||
/// </summary>
|
||||
public sealed class JsonPatchOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation type (e.g., "replace", "add", "remove").
|
||||
/// </summary>
|
||||
[JsonPropertyName("op")]
|
||||
public string Op { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON Pointer path to the target location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The value for the operation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a plan with multiple steps.
|
||||
/// </summary>
|
||||
public sealed class Plan
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of steps in the plan.
|
||||
/// </summary>
|
||||
[JsonPropertyName("steps")]
|
||||
public List<Step> Steps { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of completed steps.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int CompletedCount => this.Steps.Count(s => s.IsCompleted);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of steps.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int TotalCount => this.Steps.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether all steps are completed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsComplete => this.Steps.Count > 0 && this.Steps.All(s => s.IsCompleted);
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using System.Text.Json
|
||||
@implements IDisposable
|
||||
|
||||
@if (IsWaitingForPlan)
|
||||
{
|
||||
<div class="plan-card plan-loading">
|
||||
<div class="plan-header">
|
||||
<h3>Loading Plan...</h3>
|
||||
<div class="plan-progress">
|
||||
<span class="skeleton-text skeleton-progress"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-steps">
|
||||
@for (int i = 0; i < 3; i++)
|
||||
{
|
||||
<div class="plan-step skeleton-step">
|
||||
<span class="step-checkbox skeleton-checkbox"></span>
|
||||
<span class="step-description skeleton-text skeleton-description"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="plan-button plan-button-skeleton" disabled>Loading...</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (CurrentPlan is not null && AwaitingConfirmation)
|
||||
{
|
||||
<div class="plan-card plan-active">
|
||||
<div class="plan-header">
|
||||
<h3>Plan Confirmation</h3>
|
||||
<div class="plan-progress">
|
||||
<span class="progress-count">@CurrentPlan.CompletedCount / @CurrentPlan.TotalCount completed</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-steps">
|
||||
@for (int i = 0; i < CurrentPlan.Steps.Count; i++)
|
||||
{
|
||||
var step = CurrentPlan.Steps[i];
|
||||
var index = i;
|
||||
<div class="plan-step @(step.IsCompleted ? "step-completed" : "step-pending") @(selectedSteps.Contains(index) ? "step-selected" : "")">
|
||||
<label class="step-checkbox-label">
|
||||
<input type="checkbox"
|
||||
class="step-checkbox"
|
||||
checked="@selectedSteps.Contains(index)"
|
||||
disabled="@step.IsCompleted"
|
||||
@onchange="() => ToggleStepSelection(index)" />
|
||||
<span class="checkmark @(step.IsCompleted || selectedSteps.Contains(index) ? "checkmark-completed" : "")">
|
||||
@if (step.IsCompleted || selectedSteps.Contains(index))
|
||||
{
|
||||
<span class="check-icon">âś“</span>
|
||||
}
|
||||
</span>
|
||||
</label>
|
||||
<span class="step-description @(step.IsCompleted ? "description-completed" : "")">@step.Description</span>
|
||||
<span class="step-status @(step.IsCompleted ? "status-completed" : "status-pending")">
|
||||
@(step.IsCompleted ? "Done" : "Pending")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="plan-button plan-button-confirm" @onclick="ConfirmPlan" disabled="@(selectedSteps.Count == 0)">
|
||||
Confirm Selected (@selectedSteps.Count)
|
||||
</button>
|
||||
<button class="plan-button plan-button-reject" @onclick="RejectPlan">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (CurrentPlan is not null && !AwaitingConfirmation && !WasRejected)
|
||||
{
|
||||
<div class="plan-card @(CurrentPlan.IsComplete ? "plan-completed" : "plan-executing")">
|
||||
<div class="plan-header">
|
||||
<h3>@(CurrentPlan.IsComplete ? "Plan Completed" : "Executing Plan")</h3>
|
||||
@if (CurrentPlan.IsComplete)
|
||||
{
|
||||
<span class="completion-badge">âś“ All Done</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="plan-progress">
|
||||
<span class="progress-count">@CurrentPlan.CompletedCount / @CurrentPlan.TotalCount completed</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="plan-steps">
|
||||
@foreach (var step in CurrentPlan.Steps)
|
||||
{
|
||||
<div class="plan-step @(step.IsCompleted ? "step-completed" : "step-pending")">
|
||||
<span class="checkmark @(step.IsCompleted ? "checkmark-completed" : "")">
|
||||
@if (step.IsCompleted)
|
||||
{
|
||||
<span class="check-icon">âś“</span>
|
||||
}
|
||||
</span>
|
||||
<span class="step-description @(step.IsCompleted ? "description-completed" : "")">@step.Description</span>
|
||||
<span class="step-status @(step.IsCompleted ? "status-completed" : "status-pending")">
|
||||
@(step.IsCompleted ? "Done" : "Pending")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (WasRejected)
|
||||
{
|
||||
<div class="plan-card plan-rejected">
|
||||
<div class="plan-header">
|
||||
<h3>Plan Rejected</h3>
|
||||
<span class="rejection-badge">âś— Cancelled</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
public InvocationContext Invocation { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MessageListContext? MessageListContext { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public IAgentBoundaryContext? BoundaryContext { get; set; }
|
||||
|
||||
private Plan? CurrentPlan;
|
||||
private HashSet<int> selectedSteps = new();
|
||||
private bool AwaitingConfirmation;
|
||||
private bool WasRejected;
|
||||
private bool IsWaitingForPlan => CurrentPlan is null && !WasRejected;
|
||||
private ResponseUpdateSubscription? _responseSubscription;
|
||||
private string? _confirmPlanCallId;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// The plan comes from the create_plan function result
|
||||
if (Invocation?.HasResult == true)
|
||||
{
|
||||
TryParsePlanFromResult();
|
||||
}
|
||||
else if (Invocation is not null)
|
||||
{
|
||||
// Subscribe to wait for the result
|
||||
Invocation.ResultArrived += OnCreatePlanResultArrived;
|
||||
}
|
||||
|
||||
// Subscribe to response updates to detect confirm_plan and update_plan_step calls
|
||||
if (BoundaryContext is not null)
|
||||
{
|
||||
_responseSubscription = BoundaryContext.SubscribeToResponseUpdates(OnResponseUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCreatePlanResultArrived()
|
||||
{
|
||||
TryParsePlanFromResult();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void TryParsePlanFromResult()
|
||||
{
|
||||
var plan = Invocation?.GetResult<Plan>();
|
||||
if (plan is not null)
|
||||
{
|
||||
CurrentPlan = plan;
|
||||
InitializeSelectedSteps();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSelectedSteps()
|
||||
{
|
||||
// Select all pending steps by default
|
||||
if (CurrentPlan is not null)
|
||||
{
|
||||
selectedSteps.Clear();
|
||||
for (int i = 0; i < CurrentPlan.Steps.Count; i++)
|
||||
{
|
||||
if (!CurrentPlan.Steps[i].IsCompleted)
|
||||
{
|
||||
selectedSteps.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResponseUpdate()
|
||||
{
|
||||
// Check the current update for tool calls
|
||||
var update = BoundaryContext?.CurrentUpdate;
|
||||
if (update is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.Contents is not null)
|
||||
{
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent call)
|
||||
{
|
||||
if (string.Equals(call.Name, "confirm_plan", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleConfirmPlanCall(call);
|
||||
}
|
||||
else if (string.Equals(call.Name, "update_plan_step", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleUpdatePlanStepCall(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleConfirmPlanCall(FunctionCallContent call)
|
||||
{
|
||||
// When confirm_plan is called, show the confirmation UI
|
||||
if (!AwaitingConfirmation && !WasRejected && _confirmPlanCallId is null)
|
||||
{
|
||||
_confirmPlanCallId = call.CallId;
|
||||
AwaitingConfirmation = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUpdatePlanStepCall(FunctionCallContent call)
|
||||
{
|
||||
if (CurrentPlan is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the index and status from arguments
|
||||
if (call.Arguments is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int? index = null;
|
||||
string? status = null;
|
||||
string? description = null;
|
||||
|
||||
if (call.Arguments.TryGetValue("index", out var indexObj))
|
||||
{
|
||||
if (indexObj is int i) index = i;
|
||||
else if (indexObj is long l) index = (int)l;
|
||||
else if (indexObj is JsonElement je && je.ValueKind == JsonValueKind.Number) index = je.GetInt32();
|
||||
}
|
||||
|
||||
if (call.Arguments.TryGetValue("status", out var statusObj))
|
||||
{
|
||||
if (statusObj is string s) status = s;
|
||||
else if (statusObj is JsonElement je && je.ValueKind == JsonValueKind.String) status = je.GetString();
|
||||
}
|
||||
|
||||
if (call.Arguments.TryGetValue("description", out var descObj))
|
||||
{
|
||||
if (descObj is string s) description = s;
|
||||
else if (descObj is JsonElement je && je.ValueKind == JsonValueKind.String) description = je.GetString();
|
||||
}
|
||||
|
||||
// Apply the update
|
||||
if (index.HasValue && index.Value >= 0 && index.Value < CurrentPlan.Steps.Count)
|
||||
{
|
||||
if (status is not null)
|
||||
{
|
||||
CurrentPlan.Steps[index.Value].Status = status.ToLowerInvariant();
|
||||
}
|
||||
if (description is not null)
|
||||
{
|
||||
CurrentPlan.Steps[index.Value].Description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleStepSelection(int index)
|
||||
{
|
||||
if (selectedSteps.Contains(index))
|
||||
{
|
||||
selectedSteps.Remove(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedSteps.Add(index);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ConfirmPlan()
|
||||
{
|
||||
AwaitingConfirmation = false;
|
||||
var result = new PlanConfirmationResult
|
||||
{
|
||||
Confirmed = true,
|
||||
SelectedStepIndices = selectedSteps.ToList()
|
||||
};
|
||||
BoundaryContext?.ProvideResponse("confirm_plan", result);
|
||||
}
|
||||
|
||||
private void RejectPlan()
|
||||
{
|
||||
WasRejected = true;
|
||||
AwaitingConfirmation = false;
|
||||
var result = new PlanConfirmationResult
|
||||
{
|
||||
Confirmed = false,
|
||||
SelectedStepIndices = []
|
||||
};
|
||||
BoundaryContext?.ProvideResponse("confirm_plan", result);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Invocation is not null)
|
||||
{
|
||||
Invocation.ResultArrived -= OnCreatePlanResultArrived;
|
||||
}
|
||||
_responseSubscription?.Dispose();
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
.plan-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.4);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.plan-card.plan-active {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.plan-card.plan-completed {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
.plan-card.plan-rejected {
|
||||
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
|
||||
}
|
||||
|
||||
.plan-card.plan-loading {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.plan-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.plan-progress .progress-count {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.completion-badge,
|
||||
.rejection-badge {
|
||||
font-size: 0.9rem;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.completion-badge {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.rejection-badge {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.plan-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.plan-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.plan-step.step-selected {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.plan-step.step-completed {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.step-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step-checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-checkbox:checked + .checkmark {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.checkmark-completed {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
flex: 1;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.description-completed {
|
||||
opacity: 0.7;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.plan-button {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.plan-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.plan-button-confirm {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-button-confirm:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.plan-button-reject {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-button-reject:hover {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.plan-button-skeleton {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Loading/Skeleton state */
|
||||
.skeleton-text {
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.skeleton-progress {
|
||||
width: 100px;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.skeleton-step {
|
||||
padding: 14px 12px;
|
||||
}
|
||||
|
||||
.skeleton-checkbox {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0.2) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s infinite;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skeleton-description {
|
||||
width: 200px;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
|
||||
|
||||
/// <summary>
|
||||
/// Result of user's plan confirmation decision.
|
||||
/// </summary>
|
||||
public class PlanConfirmationResult
|
||||
{
|
||||
public bool Confirmed { get; set; }
|
||||
public List<int> SelectedStepIndices { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.HumanInTheLoop;
|
||||
|
||||
/// <summary>
|
||||
/// Applies JSON Patch operations to a Plan.
|
||||
/// Uses hardcoded path parsing for the expected paths:
|
||||
/// - /steps/{index}/status
|
||||
/// - /steps/{index}/description
|
||||
/// </summary>
|
||||
public static partial class PlanPatcher
|
||||
{
|
||||
// Regex to match paths like /steps/0/status or /steps/1/description
|
||||
[GeneratedRegex(@"^/steps/(\d+)/(status|description)$")]
|
||||
private static partial Regex StepPropertyPathRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Applies a JSON Patch operation to the plan.
|
||||
/// Only supports "replace" operations on /steps/{index}/status and /steps/{index}/description paths.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to modify.</param>
|
||||
/// <param name="operation">The patch operation to apply.</param>
|
||||
/// <returns>True if the operation was applied successfully, false otherwise.</returns>
|
||||
public static bool ApplyPatch(Plan plan, JsonPatchOperation operation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plan);
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
|
||||
// Only support "replace" operations
|
||||
if (!string.Equals(operation.Op, "replace", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var match = StepPropertyPathRegex().Match(operation.Path);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(match.Groups[1].Value, out int stepIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stepIndex < 0 || stepIndex >= plan.Steps.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var propertyName = match.Groups[2].Value;
|
||||
var step = plan.Steps[stepIndex];
|
||||
|
||||
switch (propertyName.ToUpperInvariant())
|
||||
{
|
||||
case "STATUS":
|
||||
step.Status = operation.Value?.ToString() ?? "pending";
|
||||
return true;
|
||||
case "DESCRIPTION":
|
||||
step.Description = operation.Value?.ToString() ?? string.Empty;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies multiple JSON Patch operations to the plan.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to modify.</param>
|
||||
/// <param name="operations">The patch operations to apply.</param>
|
||||
/// <returns>The number of operations successfully applied.</returns>
|
||||
public static int ApplyPatches(Plan plan, IEnumerable<JsonPatchOperation> operations)
|
||||
{
|
||||
int appliedCount = 0;
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
if (ApplyPatch(plan, operation))
|
||||
{
|
||||
appliedCount++;
|
||||
}
|
||||
}
|
||||
return appliedCount;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user