Compare commits

..
Author SHA1 Message Date
Korolev DmitryandGitHub 4072c66b5f Merge branch 'main' into dmkorolev/threads 2025-11-19 12:58:45 +01:00
Giles OdigweandGitHub d5165e2532 Python: Added Foundry Sample for A2A + SharePoint Samples (#2313)
* a2a + sharepoint samples

* small fixes
2025-11-19 11:13:47 +00:00
f83c39f924 Python: fix: resolve string annotations in FunctionExecutor (#2308)
* fix: resolve string annotations in FunctionExecutor

Enhance type hint validation in FunctionExecutor by importing `typing` and
using `get_type_hints` to correctly resolve annotations.

This fixes validation failures when `from __future__ import annotations`
is enabled, which stores annotations as strings.

Fixes #1808

* Update python/packages/core/tests/workflow/test_function_executor_future.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ran pre commit

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 07:26:44 +00:00
claude89757andGitHub 037349ff90 Python: fix Langfuse observability to capture ChatAgent system instructions (#2316)
Fix bug where ChatAgent system instructions were not captured in Langfuse
traces due to incorrect attribute access.

The observability code was attempting to retrieve instructions using
getattr(self, "instructions", None), but ChatAgent stores instructions
in self.chat_options.instructions. This caused system_instructions to
always be None in Langfuse traces.

Changed both _trace_agent_run and _trace_agent_run_stream functions
to correctly retrieve instructions from chat_options.instructions.

Fixes affect:
- Line 1123: _trace_agent_run (non-streaming)
- Line 1192: _trace_agent_run_stream (streaming)
2025-11-19 07:08:27 +00:00
Eduard van ValkenburgGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>eavanvalkenburgcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>CopilotVictor Dibia
293abf5b56 Python: fix for Incomplete URL substring sanitization (#2274)
* Potential fix for code scanning alert no. 29: Incomplete URL substring sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Python: Fix URL parsing to handle trailing punctuation in deployment progress detection (#2296)

* Initial plan

* Fix URL parsing to handle trailing punctuation correctly

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* updated lock

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
2025-11-18 22:16:58 +00:00
Evan MattsonandGitHub e2d2299a4f Python: Improve WorkflowBuilder doc strings with code samples (#1960)
* Improve WorkflowBuilder doc strings with code samples

* Cleanup
2025-11-18 22:13:17 +00:00
Eduard van ValkenburgandGitHub 8a7260140a Python: Anthropic foundry (#2302)
* added anthropic foundry sample

* updated readme

* typo
2025-11-18 16:03:40 +00:00
Korolev DmitryandGitHub 1da9107f4a .NET: Improve AIAgent and Workflow registrations for DevUI integration (#2227)
* wip

* resolve non-agent workflows as well!

* add tests for devui registrations and resolving

* fixes

* devui for net8 as well!

* simplify TFM

* update tfm...

* tfm rules....

* wip

* roll

* verify entities are registered with a devui call

* tests

* add a proper support for non-keyed workflows

* resolve default aiagent registration

* sort usings :)

* cleanup tests
2025-11-18 15:38:00 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Chris
03b74bfad4 Bump js-yaml from 4.1.0 to 4.1.1 in /python/packages/devui/frontend (#2230)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
2025-11-18 13:47:27 +00:00
Roger BarretoandGitHub f6cd329a32 .NET: Post bugbash updates (#2279)
* Add missing README.md

* Address bugbash comments

* Address bugbash issues and suggestions
2025-11-18 09:50:22 +00:00
Korolev Dmitry 0c02824853 rm 2025-11-17 21:53:35 +01:00
Korolev Dmitry 836d22b205 comments 2025-11-17 21:51:52 +01:00
Korolev Dmitry 19b6f3a5d9 unblock 2025-11-17 21:50:47 +01:00
Korolev Dmitry 3b80c9e50d via agent thread store 2025-11-17 19:03:21 +01:00
54 changed files with 2279 additions and 1208 deletions
+4 -3
View File
@@ -103,11 +103,11 @@
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.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.1_UsingFunctionTools/FoundryAgents_Step03.1_UsingFunctionTools.csproj" />
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI.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" />
@@ -191,7 +191,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" />
@@ -374,6 +374,7 @@
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.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.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" />
@@ -2,7 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI;
namespace AgentWebChat.AgentHost;
@@ -10,24 +10,24 @@ internal static class ActorFrameworkWebApplicationExtensions
{
public static void MapAgentDiscovery(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string path)
{
var routeGroup = endpoints.MapGroup(path);
routeGroup.MapGet("/", async (
AgentCatalog agentCatalog,
CancellationToken cancellationToken) =>
{
var results = new List<AgentDiscoveryCard>();
await foreach (var result in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(new AgentDiscoveryCard
{
Name = result.Name!,
Description = result.Description,
});
}
var registeredAIAgents = endpoints.ServiceProvider.GetKeyedServices<AIAgent>(KeyedService.AnyKey);
return Results.Ok(results);
})
.WithName("GetAgents");
var routeGroup = endpoints.MapGroup(path);
routeGroup.MapGet("/", async (CancellationToken cancellationToken) =>
{
var results = new List<AgentDiscoveryCard>();
foreach (var result in registeredAIAgents)
{
results.Add(new AgentDiscoveryCard
{
Name = result.Name!,
Description = result.Description,
});
}
return Results.Ok(results);
})
.WithName("GetAgents");
}
internal sealed class AgentDiscoveryCard
@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
@@ -5,6 +5,7 @@ using AgentWebChat.AgentHost;
using AgentWebChat.AgentHost.Custom;
using AgentWebChat.AgentHost.Utilities;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DevUI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
@@ -21,6 +22,13 @@ builder.Services.AddProblemDetails();
// Configure the chat model and our agent.
builder.AddKeyedChatClient("chat-model");
// Add DevUI services
builder.AddDevUI();
// Add OpenAI services
builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
var pirateAgentBuilder = builder.AddAIAgent(
"pirate",
instructions: "You are a pirate. Speak like a pirate",
@@ -95,8 +103,48 @@ var scienceConcurrentWorkflow = builder.AddWorkflow("science-concurrent-workflow
return AgentWorkflowBuilder.BuildConcurrent(workflowName: key, agents: agents);
}).AddAsAIAgent();
builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
builder.AddWorkflow("nonAgentWorkflow", (sp, key) =>
{
List<IHostedAgentBuilder> usedAgents = [pirateAgentBuilder, chemistryAgent];
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents);
});
builder.Services.AddKeyedSingleton("NonAgentAndNonmatchingDINameWorkflow", (sp, key) =>
{
List<IHostedAgentBuilder> usedAgents = [pirateAgentBuilder, chemistryAgent];
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
return AgentWorkflowBuilder.BuildSequential(workflowName: "random-name", agents: agents);
});
builder.Services.AddSingleton<AIAgent>(sp =>
{
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
return new ChatClientAgent(chatClient, name: "default-agent", instructions: "you are a default agent.");
});
builder.Services.AddKeyedSingleton<AIAgent>("my-di-nonmatching-agent", (sp, name) =>
{
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
return new ChatClientAgent(
chatClient,
name: "some-random-name", // demonstrating registration can be different for DI and actual agent
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
});
builder.Services.AddKeyedSingleton<AIAgent>("my-di-matchingname-agent", (sp, name) =>
{
if (name is not string nameStr)
{
throw new NotSupportedException("Name should be passed as a key");
}
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
return new ChatClientAgent(
chatClient,
name: nameStr, // demonstrating registration with the same name
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
});
var app = builder.Build();
@@ -118,7 +166,10 @@ app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard
// Url = "http://localhost:5390/a2a/knights-and-knaves"
});
app.MapDevUI();
app.MapOpenAIResponses();
app.MapOpenAIConversations();
app.MapOpenAIChatCompletions(pirateAgentBuilder);
app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder);
@@ -9,7 +9,9 @@ var azOpenAiResourceGroup = builder.AddParameterFromConfiguration("AzureOpenAIRe
var chatModel = builder.AddAIModel("chat-model").AsAzureOpenAI("gpt-4o", o => o.AsExisting(azOpenAiResource, azOpenAiResourceGroup));
var agentHost = builder.AddProject<Projects.AgentWebChat_AgentHost>("agenthost")
.WithReference(chatModel);
.WithHttpEndpoint(name: "devui")
.WithUrlForEndpoint("devui", (url) => new() { Url = "/devui", DisplayText = "Dev UI" })
.WithReference(chatModel);
builder.AddProject<Projects.AgentWebChat_Web>("webfrontend")
.WithExternalHttpEndpoints()
@@ -11,14 +11,15 @@ using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string JokerInstructions = "You are good at telling jokes.";
const string JokerInstructionsV1 = "You are good at telling jokes.";
const string JokerInstructionsV2 = "You are extremely hilarious at telling jokes.";
const string JokerName = "JokerAgent";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Define the agent you want to create. (Prompt Agent in this case)
AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructions });
AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructionsV1 });
// Azure.AI.Agents SDK creates and manages agent by name and versions.
// You can create a server side agent version with the Azure.AI.Agents SDK client below.
@@ -32,8 +33,8 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName:
// You can retrieve an AIAgent for an already created server side agent version.
AIAgent jokerAgentV1 = aiProjectClient.GetAIAgent(agentVersion);
// You can also create another AIAgent version (V2) by providing the same name with a different definition.
AIAgent jokerAgentV2 = aiProjectClient.CreateAIAgent(name: JokerName, model: deploymentName, instructions: JokerInstructions + "V2");
// You can also create another AIAgent version (V2) by providing the same name with a different definition/instruction.
AIAgent jokerAgentV2 = aiProjectClient.CreateAIAgent(name: JokerName, model: deploymentName, instructions: JokerInstructionsV2);
// You can also get the AIAgent latest version by just providing its name.
AIAgent jokerAgentLatest = aiProjectClient.GetAIAgent(name: JokerName);
@@ -43,11 +44,7 @@ AgentVersion latestVersion = jokerAgentLatest.GetService<AgentVersion>()!;
Console.WriteLine($"Latest agent version id: {latestVersion.Id}");
// Once you have the AIAgent, you can invoke it like any other AIAgent.
AgentThread thread = jokerAgentLatest.GetNewThread();
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", thread));
// This will use the same thread to continue the conversation.
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", thread));
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate."));
// Cleanup by agent name removes both agent versions created (jokerAgentV1 + jokerAgentV2).
await aiProjectClient.Agents.DeleteAgentAsync(jokerAgentV1.Name);
@@ -26,13 +26,8 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName:
// You can retrieve an AIAgent for a already created server side agent version.
AIAgent jokerAgent = aiProjectClient.GetAIAgent(agentVersion);
// Invoke the agent and output the text result.
AgentThread thread = jokerAgent.GetNewThread();
Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", thread));
// Invoke the agent with streaming support.
thread = jokerAgent.GetNewThread();
await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate."))
{
Console.WriteLine(update);
}
@@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="OpenAPISpec.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,354 +0,0 @@
{
"openapi": "3.0.1",
"info": {
"title": "Github Versions API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.github.com"
}
],
"components": {
"schemas": {
"basic-error": {
"title": "Basic Error",
"description": "Basic Error",
"type": "object",
"properties": {
"message": {
"type": "string"
},
"documentation_url": {
"type": "string"
},
"url": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"label": {
"title": "Label",
"description": "Color-coded labels help you categorize and filter your issues (just like labels in Gmail).",
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for the label.",
"type": "integer",
"format": "int64",
"example": 208045946
},
"node_id": {
"type": "string",
"example": "MDU6TGFiZWwyMDgwNDU5NDY="
},
"url": {
"description": "URL for the label",
"example": "https://api.github.com/repositories/42/labels/bug",
"type": "string",
"format": "uri"
},
"name": {
"description": "The name of the label.",
"example": "bug",
"type": "string"
},
"description": {
"description": "Optional description of the label, such as its purpose.",
"type": "string",
"example": "Something isn't working",
"nullable": true
},
"color": {
"description": "6-character hex code, without the leading #, identifying the color",
"example": "FFFFFF",
"type": "string"
},
"default": {
"description": "Whether this label comes by default in a new repository.",
"type": "boolean",
"example": true
}
},
"required": [
"id",
"node_id",
"url",
"name",
"description",
"color",
"default"
]
},
"tag": {
"title": "Tag",
"description": "Tag",
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "v0.1"
},
"commit": {
"type": "object",
"properties": {
"sha": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"sha",
"url"
]
},
"zipball_url": {
"type": "string",
"format": "uri",
"example": "https://github.com/octocat/Hello-World/zipball/v0.1"
},
"tarball_url": {
"type": "string",
"format": "uri",
"example": "https://github.com/octocat/Hello-World/tarball/v0.1"
},
"node_id": {
"type": "string"
}
},
"required": [
"name",
"node_id",
"commit",
"zipball_url",
"tarball_url"
]
}
},
"examples": {
"label-items": {
"value": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "f29513",
"default": true
},
{
"id": 208045947,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement",
"name": "enhancement",
"description": "New feature or request",
"color": "a2eeef",
"default": false
}
]
},
"tag-items": {
"value": [
{
"name": "v0.1",
"commit": {
"sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc",
"url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
},
"zipball_url": "https://github.com/octocat/Hello-World/zipball/v0.1",
"tarball_url": "https://github.com/octocat/Hello-World/tarball/v0.1",
"node_id": "MDQ6VXNlcjE="
}
]
}
},
"parameters": {
"owner": {
"name": "owner",
"description": "The account owner of the repository. The name is not case sensitive.",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
"repo": {
"name": "repo",
"description": "The name of the repository without the `.git` extension. The name is not case sensitive.",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
"per-page": {
"name": "per_page",
"description": "The number of results per page (max 100). For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
"in": "query",
"schema": {
"type": "integer",
"default": 30
}
},
"page": {
"name": "page",
"description": "The page number of the results to fetch. For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
"in": "query",
"schema": {
"type": "integer",
"default": 1
}
}
},
"responses": {
"not_found": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/basic-error"
}
}
}
}
},
"headers": {
"link": {
"example": "<https://api.github.com/resource?page=2>; rel=\"next\", <https://api.github.com/resource?page=5>; rel=\"last\"",
"schema": {
"type": "string"
}
}
}
},
"paths": {
"/repos/{owner}/{repo}/tags": {
"get": {
"summary": "List repository tags",
"description": "",
"tags": [
"repos"
],
"operationId": "repos/list-tags",
"externalDocs": {
"description": "API method documentation",
"url": "https://docs.github.com/rest/repos/repos#list-repository-tags"
},
"parameters": [
{
"$ref": "#/components/parameters/owner"
},
{
"$ref": "#/components/parameters/repo"
},
{
"$ref": "#/components/parameters/per-page"
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/tag"
}
},
"examples": {
"default": {
"$ref": "#/components/examples/tag-items"
}
}
}
},
"headers": {
"Link": {
"$ref": "#/components/headers/link"
}
}
}
},
"x-github": {
"githubCloudOnly": false,
"enabledForGitHubApps": true,
"category": "repos",
"subcategory": "repos"
}
}
},
"/repos/{owner}/{repo}/labels": {
"get": {
"summary": "List labels for a repository",
"description": "Lists all labels for a repository.",
"tags": [
"issues"
],
"operationId": "issues/list-labels-for-repo",
"externalDocs": {
"description": "API method documentation",
"url": "https://docs.github.com/rest/issues/labels#list-labels-for-a-repository"
},
"parameters": [
{
"$ref": "#/components/parameters/owner"
},
{
"$ref": "#/components/parameters/repo"
},
{
"$ref": "#/components/parameters/per-page"
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/label"
}
},
"examples": {
"default": {
"$ref": "#/components/examples/label-items"
}
}
}
},
"headers": {
"Link": {
"$ref": "#/components/headers/link"
}
}
},
"404": {
"$ref": "#/components/responses/not_found"
}
},
"x-github": {
"githubCloudOnly": false,
"enabledForGitHubApps": true,
"category": "issues",
"subcategory": "labels"
}
}
}
}
}
@@ -1,38 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use an agent with function tools provided via an OpenAPI spec.
// It uses functionality from Semantic Kernel to parse the OpenAPI spec and create function tools to use with the Agent Framework Agent.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Load the OpenAPI Spec from a file.
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("github", "OpenAPISpec.json");
// Convert the Semantic Kernel plugin to Agent Framework function tools.
// This requires a dummy Kernel instance, since KernelFunctions cannot execute without one.
Kernel kernel = new();
List<AITool> tools = plugin.Select(x => x.WithKernel(kernel)).Cast<AITool>().ToList();
const string AssistantInstructions = "You are a helpful assistant that can query GitHub repositories.";
const string AssistantName = "GitHubAssistant";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: tools);
// Run the agent with the OpenAPI function tools.
AgentThread thread = agent.GetNewThread();
Console.WriteLine(await agent.RunAsync("Please list the names, colors and descriptions of all the labels available in the microsoft/agent-framework repository on github.", thread));
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -1,49 +0,0 @@
# Using Function Tools from OpenAPI Specifications
This sample demonstrates how to create function tools from an OpenAPI specification and use them with AI agents.
## What this sample demonstrates
- Loading OpenAPI specifications from files
- Converting OpenAPI specifications to Semantic Kernel plugins
- Converting Semantic Kernel plugins to AI function tools
- Using OpenAPI-based function tools with AI agents
- Managing agent lifecycle (creation and deletion)
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Azure Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Run the sample
Navigate to the FoundryAgents sample directory and run:
```powershell
cd dotnet/samples/GettingStarted/FoundryAgents
dotnet run --project .\FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI
```
## Expected behavior
The sample will:
1. Load the OpenAPI specification from OpenAPISpec.json (GitHub API)
2. Convert the OpenAPI spec to Semantic Kernel plugins
3. Create an agent named "GitHubAssistant" with the OpenAPI-based function tools
4. Run the agent with a prompt to query GitHub repositories
5. The agent will invoke the appropriate OpenAPI function tools to retrieve data
6. Clean up resources by deleting the agent
@@ -26,18 +26,26 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential(
AITool tool = AIFunctionFactory.Create(GetWeather);
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [tool]);
var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [tool]);
// Getting an already existing agent by name with tools.
/*
* IMPORTANT: Since agents that are stored in the server only know the definition of the function tools (JSON Schema),
* you need to provided all invocable function tools when retrieving the agent so it can invoke them automatically.
* If no invocable tools are provided, the function calling needs to handled manually.
*/
var existingAgent = await aiProjectClient.GetAIAgentAsync(name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentThread thread = agent.GetNewThread();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
AgentThread thread = existingAgent.GetNewThread();
Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
thread = agent.GetNewThread();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
thread = existingAgent.GetNewThread();
await foreach (AgentRunResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
}
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(existingAgent.Name);
@@ -25,7 +25,7 @@ const string AssistantName = "WeatherAssistant";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather));
ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather)));
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [approvalTool]);
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -16,5 +16,11 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\walkway.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -8,7 +8,7 @@ using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o";
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o";
const string VisionInstructions = "You are a helpful agent that can analyze images";
const string VisionName = "VisionAgent";
@@ -21,7 +21,7 @@ AIAgent agent = aiProjectClient.CreateAIAgent(name: VisionName, model: deploymen
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
new DataContent(File.ReadAllBytes("assets/walkway.jpg"), "image/jpeg")
]);
AgentThread thread = agent.GetNewThread();
@@ -55,7 +55,7 @@ AgentRunResponse response = await agentOption1.RunAsync("I need to solve the equ
// AgentRunResponse response = await agentOption2.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
// Get the CodeInterpreterToolCallContent
CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType<CodeInterpreterToolCallContent>().SingleOrDefault();
CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType<CodeInterpreterToolCallContent>().FirstOrDefault();
if (toolCallContent?.Inputs is not null)
{
DataContent? codeInput = toolCallContent.Inputs.OfType<DataContent>().FirstOrDefault();
@@ -15,8 +15,8 @@ internal sealed class Program
{
private static async Task Main(string[] args)
{
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "computer-use-preview";
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "computer-use-preview";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
@@ -24,8 +24,8 @@ Before you begin, ensure you have the following prerequisites:
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="computer-use-preview" # Optional, defaults to computer-use-preview
$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="computer-use-preview" # Optional, defaults to computer-use-preview
```
## Run the sample
@@ -25,8 +25,7 @@ Before you begin, ensure you have the following prerequisites:
|[Basics](./FoundryAgents_Step01.1_Basics/)|This sample demonstrates how to create and manage AI agents with versioning|
|[Running a simple agent](./FoundryAgents_Step01.2_Running/)|This sample demonstrates how to create and run a basic Foundry agent|
|[Multi-turn conversation](./FoundryAgents_Step02_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a Foundry agent|
|[Using function tools](./FoundryAgents_Step03.1_UsingFunctionTools/)|This sample demonstrates how to use function tools with a Foundry agent|
|[Using OpenAPI function tools](./FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a Foundry agent|
|[Using function tools](./FoundryAgents_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with a Foundry agent|
|[Using function tools with approvals](./FoundryAgents_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution|
|[Structured output](./FoundryAgents_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a Foundry agent|
|[Persisted conversations](./FoundryAgents_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later|
@@ -1,10 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
@@ -27,21 +24,26 @@ internal static class EntitiesApiExtensions
/// <item><description>GET /v1/entities/{entityId}/info - Get detailed information about a specific entity</description></item>
/// </list>
/// The endpoints are compatible with the Python DevUI frontend and automatically discover entities
/// from the registered <see cref="AgentCatalog"/> and <see cref="WorkflowCatalog"/> services.
/// from the registered <see cref="AIAgent">agents</see> and <see cref="Workflow">workflows</see> in the dependency injection container.
/// </remarks>
public static IEndpointConventionBuilder MapEntities(this IEndpointRouteBuilder endpoints)
{
var registeredAIAgents = GetRegisteredEntities<AIAgent>(endpoints.ServiceProvider);
var registeredWorkflows = GetRegisteredEntities<Workflow>(endpoints.ServiceProvider);
var group = endpoints.MapGroup("/v1/entities")
.WithTags("Entities");
// List all entities
group.MapGet("", ListEntitiesAsync)
group.MapGet("", (CancellationToken cancellationToken)
=> ListEntitiesAsync(registeredAIAgents, registeredWorkflows, cancellationToken))
.WithName("ListEntities")
.WithSummary("List all registered entities (agents and workflows)")
.Produces<DiscoveryResponse>(StatusCodes.Status200OK, contentType: "application/json");
// Get detailed entity information
group.MapGet("{entityId}/info", GetEntityInfoAsync)
group.MapGet("{entityId}/info", (string entityId, string? type, CancellationToken cancellationToken)
=> GetEntityInfoAsync(entityId, type, registeredAIAgents, registeredWorkflows, cancellationToken))
.WithName("GetEntityInfo")
.WithSummary("Get detailed information about a specific entity")
.Produces<EntityInfo>(StatusCodes.Status200OK, contentType: "application/json")
@@ -51,8 +53,8 @@ internal static class EntitiesApiExtensions
}
private static async Task<IResult> ListEntitiesAsync(
AgentCatalog? agentCatalog,
WorkflowCatalog? workflowCatalog,
IEnumerable<AIAgent> agents,
IEnumerable<Workflow> workflows,
CancellationToken cancellationToken)
{
try
@@ -60,13 +62,13 @@ internal static class EntitiesApiExtensions
var entities = new Dictionary<string, EntityInfo>();
// Discover agents
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
foreach (var agentInfo in DiscoverAgents(agents, entityIdFilter: null))
{
entities[agentInfo.Id] = agentInfo;
}
// Discover workflows
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
foreach (var workflowInfo in DiscoverWorkflows(workflows, entityIdFilter: null))
{
entities[workflowInfo.Id] = workflowInfo;
}
@@ -85,15 +87,15 @@ internal static class EntitiesApiExtensions
private static async Task<IResult> GetEntityInfoAsync(
string entityId,
string? type,
AgentCatalog? agentCatalog,
WorkflowCatalog? workflowCatalog,
IEnumerable<AIAgent> agents,
IEnumerable<Workflow> workflows,
CancellationToken cancellationToken)
{
try
{
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
{
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
foreach (var workflowInfo in DiscoverWorkflows(workflows, entityId))
{
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
}
@@ -101,7 +103,7 @@ internal static class EntitiesApiExtensions
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
{
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
foreach (var agentInfo in DiscoverAgents(agents, entityId))
{
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
}
@@ -118,17 +120,9 @@ internal static class EntitiesApiExtensions
}
}
private static async IAsyncEnumerable<EntityInfo> DiscoverAgentsAsync(
AgentCatalog? agentCatalog,
string? entityIdFilter,
[EnumeratorCancellation] CancellationToken cancellationToken)
private static IEnumerable<EntityInfo> DiscoverAgents(IEnumerable<AIAgent> agents, string? entityIdFilter)
{
if (agentCatalog is null)
{
yield break;
}
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
foreach (var agent in agents)
{
// If filtering by entity ID, skip non-matching agents
if (entityIdFilter is not null &&
@@ -148,17 +142,9 @@ internal static class EntitiesApiExtensions
}
}
private static async IAsyncEnumerable<EntityInfo> DiscoverWorkflowsAsync(
WorkflowCatalog? workflowCatalog,
string? entityIdFilter,
[EnumeratorCancellation] CancellationToken cancellationToken)
private static IEnumerable<EntityInfo> DiscoverWorkflows(IEnumerable<Workflow> workflows, string? entityIdFilter)
{
if (workflowCatalog is null)
{
yield break;
}
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
foreach (var workflow in workflows)
{
var workflowId = workflow.Name ?? workflow.StartExecutorId;
@@ -304,4 +290,14 @@ internal static class EntitiesApiExtensions
StartExecutorId = workflow.StartExecutorId
};
}
private static IEnumerable<T> GetRegisteredEntities<T>(IServiceProvider serviceProvider)
{
var keyedEntities = serviceProvider.GetKeyedServices<T>(KeyedService.AnyKey);
var defaultEntities = serviceProvider.GetServices<T>() ?? [];
return keyedEntities
.Concat(defaultEntities)
.Where(entity => entity is not null);
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Extensions.Hosting;
/// <summary>
/// Extension methods for <see cref="IHostApplicationBuilder"/> to configure DevUI.
/// </summary>
public static class MicrosoftAgentAIDevUIHostApplicationBuilderExtensions
{
/// <summary>
/// Adds DevUI services to the host application builder.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
/// <returns>The <see cref="IHostApplicationBuilder"/> for method chaining.</returns>
public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddDevUI();
return builder;
}
}
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugCoreTargetFrameworks)</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.Agents.AI.DevUI</RootNamespace>
@@ -12,6 +13,10 @@
<NoWarn>$(NoWarn);CS1591;CA1852;CA1050;RCS1037;RCS1036;RCS1124;RCS1021;RCS1146;RCS1211;CA2007;CA1308;IL2026;IL3050;CA1812</NoWarn>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<!-- Import nuget packaging properties -->
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
@@ -33,4 +38,7 @@
<Description>Provides Microsoft Agent Framework support for developer UI.</Description>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.DevUI.UnitTests"/>
</ItemGroup>
</Project>
@@ -24,9 +24,15 @@ var builder = WebApplication.CreateBuilder(args);
// Register your agents
builder.AddAIAgent("assistant", "You are a helpful assistant.");
// Register DevUI services
if (builder.Environment.IsDevelopment())
{
builder.AddDevUI();
}
// Register services for OpenAI responses and conversations (also required for DevUI)
builder.Services.AddOpenAIResponses();
builder.Services.AddOpenAIConversations();
builder.AddOpenAIResponses();
builder.AddOpenAIConversations();
var app = builder.Build();
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/> to configure DevUI.
/// </summary>
public static class MicrosoftAgentAIDevUIServiceCollectionsExtensions
{
/// <summary>
/// Adds services required for DevUI integration.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <returns>The <see cref="IServiceCollection"/> for method chaining.</returns>
public static IServiceCollection AddDevUI(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// a factory that tries to construct an AIAgent from Workflow,
// even if workflow was not explicitly registered as an AIAgent.
#pragma warning disable IDE0001 // Simplify Names
services.AddKeyedSingleton<AIAgent>(KeyedService.AnyKey, (sp, key) =>
{
var keyAsStr = key as string;
Throw.IfNullOrEmpty(keyAsStr);
var workflow = sp.GetKeyedService<Workflow>(keyAsStr);
if (workflow is not null)
{
return workflow.AsAgent(name: workflow.Name);
}
// another thing we can do is resolve a non-keyed workflow.
// however, we can't rely on anything than key to be equal to the workflow.Name.
// so we try: if we fail, we return null.
workflow = sp.GetService<Workflow>();
if (workflow is not null && workflow.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true)
{
return workflow.AsAgent(name: workflow.Name);
}
// and it's possible to lookup at the default-registered AIAgent
// with the condition of same name as the key.
var agent = sp.GetService<AIAgent>();
if (agent is not null && agent.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true)
{
return agent;
}
return null!;
});
#pragma warning restore IDE0001 // Simplify Names
return services;
}
}
@@ -34,6 +34,7 @@ internal sealed partial class IdGenerator
this._random = randomSeed.HasValue ? new Random(randomSeed.Value) : null;
this.ResponseId = responseId ?? NewId("resp", random: this._random);
this.ConversationId = conversationId ?? NewId("conv", random: this._random);
this.IsNewConversation = conversationId is null;
this._partitionId = GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
}
@@ -59,6 +60,11 @@ internal sealed partial class IdGenerator
/// </summary>
public string ConversationId { get; }
/// <summary>
/// Gets a value indicating whether this is a new conversation.
/// </summary>
public bool IsNewConversation { get; }
/// <summary>
/// Generates a new ID.
/// </summary>
@@ -26,6 +26,11 @@ internal sealed class AgentInvocationContext(IdGenerator idGenerator, JsonSerial
/// </summary>
public string ConversationId => this.IdGenerator.ConversationId;
/// <summary>
/// Returns true, if conversation is new.
/// </summary>
public bool IsNewConversation => this.IdGenerator.IsNewConversation;
/// <summary>
/// Gets the JSON serializer options.
/// </summary>
@@ -63,7 +63,11 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
return ValueTask.FromResult<ResponseError?>(new ResponseError
{
Code = "agent_not_found",
Message = $"Agent '{agentName}' not found. Ensure the agent is registered with AddAIAgent()."
Message = $"""
Agent '{agentName}' not found.
Ensure the agent is registered with '{agentName}' name in the dependency injection container.
We recommend using 'builder.AddAIAgent()' for simplicity.
"""
});
}
@@ -77,10 +81,13 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string agentName = GetAgentName(request)!;
AIAgent agent = this._serviceProvider.GetRequiredKeyedService<AIAgent>(agentName);
string conversationId = context.ConversationId;
var agent = this._serviceProvider.GetRequiredKeyedService<AIAgent>(agentName);
var threadStore = this._serviceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
var chatOptions = new ChatOptions
{
ConversationId = request.Conversation?.Id,
Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = request.MaxOutputTokens,
@@ -90,16 +97,25 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
var options = new ChatClientAgentRunOptions(chatOptions);
var messages = new List<ChatMessage>();
AgentThread thread = !context.IsNewConversation && threadStore is not null
? await threadStore.GetThreadAsync(agent, conversationId, cancellationToken).ConfigureAwait(false)
: agent.GetNewThread();
foreach (var inputMessage in request.Input.GetInputMessages())
{
messages.Add(inputMessage.ToChatMessage());
}
await foreach (var streamingEvent in agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken)
await foreach (var streamingEvent in agent.RunStreamingAsync(messages, thread, options: options, cancellationToken: cancellationToken)
.ToStreamingResponseAsync(request, context, cancellationToken).ConfigureAwait(false))
{
yield return streamingEvent;
}
if (threadStore is not null && thread is not null)
{
await threadStore.SaveThreadAsync(agent, conversationId, thread, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
@@ -1,38 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
namespace Microsoft.Agents.AI.Hosting;
/// <summary>
/// Provides a catalog of registered AI agents within the hosting environment.
/// </summary>
/// <remarks>
/// The agent catalog allows enumeration of all registered agents in the dependency injection container.
/// This is useful for scenarios where you need to discover and interact with multiple agents programmatically.
/// </remarks>
public abstract class AgentCatalog
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentCatalog"/> class.
/// </summary>
protected AgentCatalog()
{
}
/// <summary>
/// Asynchronously retrieves all registered AI agents from the catalog.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// An asynchronous enumerable of <see cref="AIAgent"/> instances representing all registered agents.
/// The enumeration will only include agents that are successfully resolved from the service provider.
/// </returns>
/// <remarks>
/// This method enumerates through all registered agent names and attempts to resolve each agent
/// from the dependency injection container. Only successfully resolved agents are yielded.
/// The enumeration is lazy and agents are resolved on-demand during iteration.
/// </remarks>
public abstract IAsyncEnumerable<AIAgent> GetAgentsAsync(CancellationToken cancellationToken = default);
}
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Hosting.Local;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -126,31 +125,9 @@ public static class AgentHostingServiceCollectionExtensions
return agent;
});
// Register the agent by name for discovery.
var agentHostBuilder = GetAgentRegistry(services);
agentHostBuilder.AgentNames.Add(name);
return new HostedAgentBuilder(name, services);
}
private static LocalAgentRegistry GetAgentRegistry(IServiceCollection services)
{
var descriptor = services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalAgentRegistry)));
if (descriptor?.ImplementationInstance is not LocalAgentRegistry instance)
{
instance = new LocalAgentRegistry();
ConfigureHostBuilder(services, instance);
}
return instance;
}
private static void ConfigureHostBuilder(IServiceCollection services, LocalAgentRegistry agentHostBuilderContext)
{
services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
services.AddSingleton<AgentCatalog, LocalAgentCatalog>();
}
private static IList<AITool> GetRegisteredToolsForAgent(IServiceProvider serviceProvider, string agentName)
{
var registry = serviceProvider.GetService<LocalAgentToolRegistry>();
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using Microsoft.Agents.AI.Hosting.Local;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -47,28 +45,6 @@ public static class HostApplicationBuilderWorkflowExtensions
return workflow;
});
// Register the workflow by name for discovery.
var workflowRegistry = GetWorkflowRegistry(builder);
workflowRegistry.WorkflowNames.Add(name);
return new HostedWorkflowBuilder(name, builder);
}
private static LocalWorkflowRegistry GetWorkflowRegistry(IHostApplicationBuilder builder)
{
var descriptor = builder.Services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalWorkflowRegistry)));
if (descriptor?.ImplementationInstance is not LocalWorkflowRegistry instance)
{
instance = new LocalWorkflowRegistry();
ConfigureHostBuilder(builder, instance);
}
return instance;
}
private static void ConfigureHostBuilder(IHostApplicationBuilder builder, LocalWorkflowRegistry agentHostBuilderContext)
{
builder.Services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
builder.Services.AddSingleton<WorkflowCatalog, LocalWorkflowCatalog>();
}
}
@@ -1,37 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.Local;
// Implementation of an AgentCatalog which enumerates agents registered in the local service provider.
internal sealed class LocalAgentCatalog : AgentCatalog
{
public readonly HashSet<string> _registeredAgents;
private readonly IServiceProvider _serviceProvider;
public LocalAgentCatalog(LocalAgentRegistry agentHostBuilder, IServiceProvider serviceProvider)
{
this._registeredAgents = [.. agentHostBuilder.AgentNames];
this._serviceProvider = serviceProvider;
}
public override async IAsyncEnumerable<AIAgent> GetAgentsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask.ConfigureAwait(false);
foreach (var name in this._registeredAgents)
{
var agent = this._serviceProvider.GetKeyedService<AIAgent>(name);
if (agent is not null)
{
yield return agent;
}
}
}
}
@@ -1,10 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Agents.AI.Hosting.Local;
internal sealed class LocalAgentRegistry
{
public HashSet<string> AgentNames { get; } = [];
}
@@ -1,37 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.Local;
internal sealed class LocalWorkflowCatalog : WorkflowCatalog
{
public readonly HashSet<string> _registeredWorkflows;
private readonly IServiceProvider _serviceProvider;
public LocalWorkflowCatalog(LocalWorkflowRegistry workflowRegistry, IServiceProvider serviceProvider)
{
this._registeredWorkflows = [.. workflowRegistry.WorkflowNames];
this._serviceProvider = serviceProvider;
}
public override async IAsyncEnumerable<Workflow> GetWorkflowsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask.ConfigureAwait(false);
foreach (var name in this._registeredWorkflows)
{
var workflow = this._serviceProvider.GetKeyedService<Workflow>(name);
if (workflow is not null)
{
yield return workflow;
}
}
}
}
@@ -1,10 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Agents.AI.Hosting.Local;
internal sealed class LocalWorkflowRegistry
{
public HashSet<string> WorkflowNames { get; } = [];
}
@@ -0,0 +1,222 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
/// <summary>
/// Unit tests for DevUI service collection extensions.
/// Tests verify that workflows and agents can be resolved even when registered non-conventionally.
/// </summary>
public class DevUIExtensionsTests
{
/// <summary>
/// Verifies that AddDevUI throws ArgumentNullException when services collection is null.
/// </summary>
[Fact]
public void AddDevUI_NullServices_ThrowsArgumentNullException()
{
IServiceCollection services = null!;
Assert.Throws<ArgumentNullException>(() => services.AddDevUI());
}
/// <summary>
/// Verifies that GetRequiredKeyedService throws for non-existent keys.
/// </summary>
[Fact]
public void AddDevUI_GetRequiredKeyedServiceNonExistent_ThrowsInvalidOperationException()
{
// Arrange
var services = new ServiceCollection();
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act & Assert
Assert.Throws<InvalidOperationException>(() => serviceProvider.GetRequiredKeyedService<AIAgent>("non-existent"));
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_WorkflowWithName_CanBeResolved_AsAIAgent()
{
// Arrange
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow", workflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act
var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow");
// Assert
Assert.NotNull(resolvedWorkflowAsAgent);
Assert.Null(resolvedWorkflowAsAgent.Name);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_MultipleWorkflowsWithName_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow1 = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
var workflow2 = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow1", workflow1);
services.AddKeyedSingleton("workflow2", workflow2);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedWorkflow1AsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow1");
Assert.NotNull(resolvedWorkflow1AsAgent);
Assert.Null(resolvedWorkflow1AsAgent.Name);
var resolvedWorkflow2AsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow2");
Assert.NotNull(resolvedWorkflow2AsAgent);
Assert.Null(resolvedWorkflow2AsAgent.Name);
Assert.False(resolvedWorkflow1AsAgent == resolvedWorkflow2AsAgent);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_NonKeyedWorkflow_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow", workflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow");
Assert.NotNull(resolvedWorkflowAsAgent);
Assert.Null(resolvedWorkflowAsAgent.Name);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_NonKeyedWorkflow_PlusKeyedWorkflow_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential("standardname", agent1, agent2);
var keyedWorkflow = AgentWorkflowBuilder.BuildSequential("keyedname", agent1, agent2);
services.AddSingleton(workflow);
services.AddKeyedSingleton("keyed", keyedWorkflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// resolve a workflow with the same name as workflow's name (which is registered without a key)
var standardAgent = serviceProvider.GetKeyedService<AIAgent>("standardname");
Assert.NotNull(standardAgent);
Assert.Equal("standardname", standardAgent.Name);
var keyedAgent = serviceProvider.GetKeyedService<AIAgent>("keyed");
Assert.NotNull(keyedAgent);
Assert.Equal("keyedname", keyedAgent.Name);
var nonExisting = serviceProvider.GetKeyedService<AIAgent>("random-non-existing!!!");
Assert.Null(nonExisting);
}
/// <summary>
/// Verifies that an agent registered with a different key than its name can be resolved by key.
/// </summary>
[Fact]
public void AddDevUI_AgentRegisteredWithDifferentKey_CanBeResolvedByKey()
{
// Arrange
var services = new ServiceCollection();
const string AgentName = "actual-agent-name";
const string RegistrationKey = "different-key";
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", AgentName);
services.AddKeyedSingleton<AIAgent>(RegistrationKey, agent);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act
var resolvedAgent = serviceProvider.GetKeyedService<AIAgent>(RegistrationKey);
// Assert
Assert.NotNull(resolvedAgent);
// The resolved agent should have the agent's name, not the registration key
Assert.Equal(AgentName, resolvedAgent.Name);
}
/// <summary>
/// Verifies that an agent registered with a different key than its name can be resolved by key.
/// </summary>
[Fact]
public void AddDevUI_Keyed_AndStandard_BothCanBeResolved()
{
// Arrange
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var defaultAgent = new ChatClientAgent(mockChatClient.Object, "default", "default");
var keyedAgent = new ChatClientAgent(mockChatClient.Object, "keyed", "keyed");
services.AddSingleton<AIAgent>(defaultAgent);
services.AddKeyedSingleton<AIAgent>("keyed-registration", keyedAgent);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedKeyedAgent = serviceProvider.GetKeyedService<AIAgent>("keyed-registration");
Assert.NotNull(resolvedKeyedAgent);
Assert.Equal("keyed", resolvedKeyedAgent.Name);
// resolving default agent based on its name, not on the registration-key
var resolvedDefaultAgent = serviceProvider.GetKeyedService<AIAgent>("default");
Assert.NotNull(resolvedDefaultAgent);
Assert.Equal("default", resolvedDefaultAgent.Name);
}
/// <summary>
/// Verifies that the DevUI fallback handler error message includes helpful information.
/// </summary>
[Fact]
public void AddDevUI_InvalidResolution_ErrorMessageIsInformative()
{
// Arrange
var services = new ServiceCollection();
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
const string InvalidKey = "invalid-key-name";
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => serviceProvider.GetRequiredKeyedService<AIAgent>(InvalidKey));
}
}
@@ -0,0 +1,285 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Workflows;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
public class DevUIIntegrationTests
{
private sealed class NoOpExecutor(string id) : Executor(id)
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<object>(
(msg, ctx) => ctx.SendMessageAsync(msg));
}
[Fact]
public async Task TestServerWithDevUI_ResolvesRequestToWorkflow_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", "agent-name");
builder.Services.AddKeyedSingleton<AIAgent>("registration-key", agent);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var resolvedAgent = app.Services.GetKeyedService<AIAgent>("registration-key");
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
Assert.NotNull(discoveryResponse);
Assert.Single(discoveryResponse.Entities);
Assert.Equal("agent-name", discoveryResponse.Entities[0].Name);
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMultipleAIAgents_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-one");
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-two");
var agent3 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-three");
builder.Services.AddKeyedSingleton<AIAgent>("key-1", agent1);
builder.Services.AddKeyedSingleton<AIAgent>("key-2", agent2);
builder.Services.AddKeyedSingleton<AIAgent>("key-3", agent3);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-three" && e.Type == "agent");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesAIAgents_WithKeyedAndDefaultRegistrationAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agentKeyed1 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-one");
var agentKeyed2 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-two");
var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-agent");
builder.Services.AddKeyedSingleton<AIAgent>("key-1", agentKeyed1);
builder.Services.AddKeyedSingleton<AIAgent>("key-2", agentKeyed2);
builder.Services.AddSingleton<AIAgent>(agentDefault);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-agent" && e.Type == "agent");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMultipleWorkflows_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var workflow1 = new WorkflowBuilder("executor-1")
.WithName("workflow-one")
.WithDescription("First workflow")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflow2 = new WorkflowBuilder("executor-2")
.WithName("workflow-two")
.WithDescription("Second workflow")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflow3 = new WorkflowBuilder("executor-3")
.WithName("workflow-three")
.WithDescription("Third workflow")
.BindExecutor(new NoOpExecutor("executor-3"))
.Build();
builder.Services.AddKeyedSingleton("key-1", workflow1);
builder.Services.AddKeyedSingleton("key-2", workflow2);
builder.Services.AddKeyedSingleton("key-3", workflow3);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-three" && e.Type == "workflow");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesWorkflows_WithKeyedAndDefaultRegistrationAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var workflowKeyed1 = new WorkflowBuilder("executor-1")
.WithName("keyed-workflow-one")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflowKeyed2 = new WorkflowBuilder("executor-2")
.WithName("keyed-workflow-two")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflowDefault = new WorkflowBuilder("executor-default")
.WithName("default-workflow")
.BindExecutor(new NoOpExecutor("executor-default"))
.Build();
builder.Services.AddKeyedSingleton("key-1", workflowKeyed1);
builder.Services.AddKeyedSingleton("key-2", workflowKeyed2);
builder.Services.AddSingleton(workflowDefault);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-workflow" && e.Type == "workflow");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMixedAgentsAndWorkflows_AllRegistrationsAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
// Create AIAgents
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-one");
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-two");
var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-mixed-agent");
// Create Workflows
var workflow1 = new WorkflowBuilder("executor-1")
.WithName("mixed-workflow-one")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflow2 = new WorkflowBuilder("executor-2")
.WithName("mixed-workflow-two")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflowDefault = new WorkflowBuilder("executor-default")
.WithName("default-mixed-workflow")
.BindExecutor(new NoOpExecutor("executor-default"))
.Build();
// Register all
builder.Services.AddKeyedSingleton<AIAgent>("agent-key-1", agent1);
builder.Services.AddKeyedSingleton<AIAgent>("agent-key-2", agent2);
builder.Services.AddSingleton<AIAgent>(agentDefault);
builder.Services.AddKeyedSingleton("workflow-key-1", workflow1);
builder.Services.AddKeyedSingleton("workflow-key-2", workflow2);
builder.Services.AddSingleton(workflowDefault);
builder.Services.AddDevUI();
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(6, discoveryResponse.Entities.Count);
// Verify agents
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-agent" && e.Type == "agent");
// Verify workflows
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-workflow" && e.Type == "workflow");
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugCoreTargetFrameworks)</TargetFrameworks>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" VersionOverride="8.0.21" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' != 'net8.0'" />
<PackageReference Include="OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
{
"profiles": {
"Microsoft.Agents.AI.DevUI.UnitTests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:63009;http://localhost:63010"
}
}
}
@@ -0,0 +1,12 @@
{
"profiles": {
"Microsoft.Agents.AI.Hosting.A2A.UnitTests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52186;http://localhost:52187"
}
}
}
@@ -17,6 +17,7 @@ Design Pattern:
import asyncio
import inspect
import typing
from collections.abc import Awaitable, Callable
from typing import Any, overload
@@ -218,15 +219,16 @@ def _validate_function_signature(func: Callable[..., Any]) -> tuple[type, Any, l
if message_param.annotation == inspect.Parameter.empty:
raise ValueError(f"Function instance {func.__name__} must have a type annotation for the message parameter")
message_type = message_param.annotation
type_hints = typing.get_type_hints(func)
message_type = type_hints.get(message_param.name, message_param.annotation)
# Check if there's a context parameter
if len(params) == 2:
ctx_param = params[1]
ctx_annotation = type_hints.get(ctx_param.name, ctx_param.annotation)
output_types, workflow_output_types = validate_workflow_context_annotation(
ctx_param.annotation, f"parameter '{ctx_param.name}'", "Function instance"
ctx_annotation, f"parameter '{ctx_param.name}'", "Function instance"
)
ctx_annotation = ctx_param.annotation
else:
# No context parameter (only valid for function executors)
output_types, workflow_output_types = [], []
@@ -39,7 +39,40 @@ logger = logging.getLogger(__name__)
class WorkflowBuilder:
"""A builder class for constructing workflows.
This class provides methods to add edges and set the starting executor for the workflow.
This class provides a fluent API for defining workflow graphs by connecting executors
with edges and configuring execution parameters. Call :meth:`build` to create an
immutable :class:`Workflow` instance.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class UpperCaseExecutor(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text.upper())
class ReverseExecutor(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(text[::-1])
# Build a workflow
workflow = (
WorkflowBuilder()
.add_edge(UpperCaseExecutor(id="upper"), ReverseExecutor(id="reverse"))
.set_start_executor("upper")
.build()
)
# Run the workflow
events = await workflow.run("hello")
print(events.get_outputs()) # ['OLLEH']
"""
def __init__(
@@ -51,7 +84,7 @@ class WorkflowBuilder:
"""Initialize the WorkflowBuilder with an empty list of edges and no starting executor.
Args:
max_iterations: Maximum number of iterations for workflow convergence.
max_iterations: Maximum number of iterations for workflow convergence. Default is 100.
name: Optional human-readable name for the workflow.
description: Optional description of what the workflow does.
"""
@@ -164,10 +197,22 @@ class WorkflowBuilder:
id: A unique identifier for the executor. If None, the agent's name will be used if available.
Returns:
The WorkflowBuilder instance (for method chaining).
Self: The WorkflowBuilder instance for method chaining.
Raises:
ValueError: If the provided id or agent name conflicts with an existing executor.
Example:
.. code-block:: python
from agent_framework import WorkflowBuilder
from agent_framework_anthropic import AnthropicAgent
# Create an agent
agent = AnthropicAgent(name="writer", model="claude-3-5-sonnet-20241022")
# Add the agent to a workflow
workflow = WorkflowBuilder().add_agent(agent, output_response=True).set_start_executor(agent).build()
"""
executor = self._maybe_wrap_agent(
agent, agent_thread=agent_thread, output_response=output_response, executor_id=id
@@ -184,12 +229,53 @@ class WorkflowBuilder:
"""Add a directed edge between two executors.
The output types of the source and the input types of the target must be compatible.
Messages sent by the source executor will be routed to the target executor.
Args:
source: The source executor of the edge.
target: The target executor of the edge.
condition: An optional condition function that determines whether the edge
should be traversed based on the message type.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class ProcessorA(Executor):
@handler
async def process(self, data: str, ctx: WorkflowContext[int]) -> None:
await ctx.send_message(len(data))
class ProcessorB(Executor):
@handler
async def process(self, count: int, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(f"Processed {count} characters")
# Connect executors with an edge
workflow = (
WorkflowBuilder().add_edge(ProcessorA(id="a"), ProcessorB(id="b")).set_start_executor("a").build()
)
# With a condition
def only_large_numbers(msg: int) -> bool:
return msg > 100
workflow = (
WorkflowBuilder()
.add_edge(ProcessorA(id="a"), ProcessorB(id="b"), condition=only_large_numbers)
.set_start_executor("a")
.build()
)
"""
# TODO(@taochen): Support executor factories for lazy initialization
source_exec = self._maybe_wrap_agent(source)
@@ -204,13 +290,50 @@ class WorkflowBuilder:
source: Executor | AgentProtocol,
targets: Sequence[Executor | AgentProtocol],
) -> Self:
"""Add multiple edges to the workflow where messages from the source will be sent to all target.
"""Add multiple edges to the workflow where messages from the source will be sent to all targets.
The output types of the source and the input types of the targets must be compatible.
Messages from the source will be broadcast to all target executors concurrently.
Args:
source: The source executor of the edges.
targets: A list of target executors for the edges.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class DataSource(Executor):
@handler
async def generate(self, count: int, ctx: WorkflowContext[str]) -> None:
for i in range(count):
await ctx.send_message(f"data_{i}")
class ValidatorA(Executor):
@handler
async def validate(self, data: str, ctx: WorkflowContext) -> None:
print(f"ValidatorA: {data}")
class ValidatorB(Executor):
@handler
async def validate(self, data: str, ctx: WorkflowContext) -> None:
print(f"ValidatorB: {data}")
# Broadcast to multiple validators
workflow = (
WorkflowBuilder()
.add_fan_out_edges(DataSource(id="source"), [ValidatorA(id="val_a"), ValidatorB(id="val_b")])
.set_start_executor("source")
.build()
)
"""
source_exec = self._maybe_wrap_agent(source)
target_execs = [self._maybe_wrap_agent(t) for t in targets]
@@ -241,6 +364,53 @@ class WorkflowBuilder:
Args:
source: The source executor of the edges.
cases: A list of case objects that determine the target executor for each message.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler, Case, Default
from dataclasses import dataclass
@dataclass
class Result:
score: int
class Evaluator(Executor):
@handler
async def evaluate(self, text: str, ctx: WorkflowContext[Result]) -> None:
await ctx.send_message(Result(score=len(text)))
class HighScoreHandler(Executor):
@handler
async def handle(self, result: Result, ctx: WorkflowContext) -> None:
print(f"High score: {result.score}")
class LowScoreHandler(Executor):
@handler
async def handle(self, result: Result, ctx: WorkflowContext) -> None:
print(f"Low score: {result.score}")
# Route based on score value
workflow = (
WorkflowBuilder()
.add_switch_case_edge_group(
Evaluator(id="eval"),
[
Case(condition=lambda r: r.score > 10, target=HighScoreHandler(id="high")),
Default(target=LowScoreHandler(id="low")),
],
)
.set_start_executor("eval")
.build()
)
"""
source_exec = self._maybe_wrap_agent(source)
source_id = self._add_executor(source_exec)
@@ -270,13 +440,67 @@ class WorkflowBuilder:
Messages from the source executor will be sent to multiple target executors based on
the provided selection function.
The selection function should take a message and the name of the target executors,
and return a list of indices indicating which target executors should receive the message.
The selection function should take a message and a list of target executor IDs,
and return a list of executor IDs indicating which target executors should receive the message.
Args:
source: The source executor of the edges.
targets: A list of target executors for the edges.
selection_func: A function that selects target executors for messages.
Takes (message, list[executor_id]) and returns list[executor_id].
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
from dataclasses import dataclass
@dataclass
class Task:
priority: str
data: str
class TaskDispatcher(Executor):
@handler
async def dispatch(self, text: str, ctx: WorkflowContext[Task]) -> None:
priority = "high" if len(text) > 10 else "low"
await ctx.send_message(Task(priority=priority, data=text))
class WorkerA(Executor):
@handler
async def process(self, task: Task, ctx: WorkflowContext) -> None:
print(f"WorkerA processing: {task.data}")
class WorkerB(Executor):
@handler
async def process(self, task: Task, ctx: WorkflowContext) -> None:
print(f"WorkerB processing: {task.data}")
# Select workers based on task priority
def select_workers(task: Task, executor_ids: list[str]) -> list[str]:
if task.priority == "high":
return executor_ids # Send to all workers
return [executor_ids[0]] # Send to first worker only
workflow = (
WorkflowBuilder()
.add_multi_selection_edge_group(
TaskDispatcher(id="dispatcher"),
[WorkerA(id="worker_a"), WorkerB(id="worker_b")],
selection_func=select_workers,
)
.set_start_executor("dispatcher")
.build()
)
"""
source_exec = self._maybe_wrap_agent(source)
target_execs = [self._maybe_wrap_agent(t) for t in targets]
@@ -298,31 +522,42 @@ class WorkflowBuilder:
The target executor will receive a list of messages aggregated from all source executors.
Thus the input types of the target executor must be compatible with a list of the output
types of the source executors. For example:
class Target(Executor):
@handler
def handle_messages(self, messages: list[Message]) -> None:
# Process the aggregated messages from all sources
class Source(Executor):
@handler(output_type=[Message])
def handle_message(self, message: Message) -> None:
# Send a message to the target executor
self.send_message(message)
workflow = (
WorkflowBuilder()
.add_fan_in_edges(
[Source(id="source1"), Source(id="source2")],
Target(id="target")
)
.build()
)
types of the source executors.
Args:
sources: A list of source executors for the edges.
target: The target executor for the edges.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class Producer(Executor):
@handler
async def produce(self, seed: int, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(f"result_{seed}")
class Aggregator(Executor):
@handler
async def aggregate(self, results: list[str], ctx: WorkflowContext[Never, str]) -> None:
combined = ", ".join(results)
await ctx.yield_output(f"Combined: {combined}")
# Collect results from multiple producers
workflow = (
WorkflowBuilder()
.add_fan_in_edges([Producer(id="prod_1"), Producer(id="prod_2")], Aggregator(id="agg"))
.set_start_executor("prod_1")
.build()
)
"""
source_execs = [self._maybe_wrap_agent(s) for s in sources]
target_exec = self._maybe_wrap_agent(target)
@@ -342,6 +577,42 @@ class WorkflowBuilder:
Args:
executors: A list of executors to be added to the chain.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class Step1(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text.upper())
class Step2(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text[::-1])
class Step3(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(f"Final: {text}")
# Chain executors in sequence
workflow = (
WorkflowBuilder()
.add_chain([Step1(id="step1"), Step2(id="step2"), Step3(id="step3")])
.set_start_executor("step1")
.build()
)
"""
# Wrap each candidate first to ensure stable IDs before adding edges
wrapped: list[Executor] = [self._maybe_wrap_agent(e) for e in executors]
@@ -352,8 +623,46 @@ class WorkflowBuilder:
def set_start_executor(self, executor: Executor | AgentProtocol | str) -> Self:
"""Set the starting executor for the workflow.
The start executor is the entry point for the workflow. When the workflow is executed,
the initial message will be sent to this executor.
Args:
executor: The starting executor, which can be an Executor instance or its ID.
executor: The starting executor, which can be an Executor instance, AgentProtocol instance,
or the string ID of an executor previously added to the workflow.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class EntryPoint(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text.upper())
class Processor(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(text)
# Set by executor instance
entry = EntryPoint(id="entry")
workflow = WorkflowBuilder().add_edge(entry, Processor(id="proc")).set_start_executor(entry).build()
# Set by executor ID string
workflow = (
WorkflowBuilder()
.add_edge(EntryPoint(id="entry"), Processor(id="proc"))
.set_start_executor("entry")
.build()
)
"""
if isinstance(executor, str):
self._start_executor = executor
@@ -370,8 +679,43 @@ class WorkflowBuilder:
def set_max_iterations(self, max_iterations: int) -> Self:
"""Set the maximum number of iterations for the workflow.
When a workflow contains cycles, this limit prevents infinite loops by capping
the total number of executor invocations. The default is 100 iterations.
Args:
max_iterations: The maximum number of iterations the workflow will run for convergence.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class StepA(Executor):
@handler
async def process(self, count: int, ctx: WorkflowContext[int]) -> None:
if count < 10:
await ctx.send_message(count + 1)
class StepB(Executor):
@handler
async def process(self, count: int, ctx: WorkflowContext[int]) -> None:
await ctx.send_message(count)
# Set a custom iteration limit for workflow with cycles
workflow = (
WorkflowBuilder()
.set_max_iterations(500)
.add_edge(StepA(id="step_a"), StepB(id="step_b"))
.add_edge(StepB(id="step_b"), StepA(id="step_a")) # Cycle
.set_start_executor("step_a")
.build()
)
"""
self._max_iterations = max_iterations
return self
@@ -381,8 +725,48 @@ class WorkflowBuilder:
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> Self:
"""Enable checkpointing with the specified storage.
Checkpointing allows workflows to save their state periodically, enabling
pause/resume functionality and recovery from failures. The checkpoint storage
implementation determines where checkpoints are persisted.
Args:
checkpoint_storage: The checkpoint storage to use.
checkpoint_storage: The checkpoint storage implementation to use.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework import FileCheckpointStorage
class ProcessorA(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text.upper())
class ProcessorB(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(text)
# Enable checkpointing with file-based storage
storage = FileCheckpointStorage("./checkpoints")
workflow = (
WorkflowBuilder()
.add_edge(ProcessorA(id="proc_a"), ProcessorB(id="proc_b"))
.set_start_executor("proc_a")
.with_checkpointing(storage)
.build()
)
# Run with checkpoint saving
events = await workflow.run("input")
"""
self._checkpoint_storage = checkpoint_storage
return self
@@ -390,15 +774,43 @@ class WorkflowBuilder:
def build(self) -> Workflow:
"""Build and return the constructed workflow.
This method performs validation before building the workflow.
This method performs validation before building the workflow to ensure:
- A starting executor has been set
- All edges connect valid executors
- The graph is properly connected
- Type compatibility between connected executors
Returns:
A Workflow instance with the defined edges and starting executor.
Workflow: An immutable Workflow instance ready for execution.
Raises:
ValueError: If starting executor is not set.
WorkflowValidationError: If workflow validation fails (includes EdgeDuplicationError,
TypeCompatibilityError, and GraphConnectivityError subclasses).
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class MyExecutor(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(text.upper())
# Build and execute a workflow
workflow = WorkflowBuilder().set_start_executor(MyExecutor(id="executor")).build()
# The workflow is now immutable and ready to run
events = await workflow.run("hello")
print(events.get_outputs()) # ['HELLO']
# Workflows can be reused multiple times
events2 = await workflow.run("world")
print(events2.get_outputs()) # ['WORLD']
"""
# Create workflow build span that includes validation and workflow creation
with create_workflow_span(OtelAttr.WORKFLOW_BUILD_SPAN) as span:
@@ -1120,7 +1120,7 @@ def _trace_agent_run(
span=span,
provider_name=provider_name,
messages=messages,
system_instructions=getattr(self, "instructions", None),
system_instructions=getattr(getattr(self, "chat_options", None), "instructions", None),
)
try:
response = await run_func(self, messages=messages, thread=thread, **kwargs)
@@ -1189,7 +1189,7 @@ def _trace_agent_run_stream(
span=span,
provider_name=provider_name,
messages=messages,
system_instructions=getattr(self, "instructions", None),
system_instructions=getattr(getattr(self, "chat_options", None), "instructions", None),
)
try:
async for update in run_streaming_func(self, messages=messages, thread=thread, **kwargs):
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from typing import Any
from agent_framework import FunctionExecutor, WorkflowContext, executor
class TestFunctionExecutorFutureAnnotations:
"""Test suite for FunctionExecutor with from __future__ import annotations."""
def test_executor_decorator_future_annotations(self):
"""Test @executor decorator works with stringified annotations."""
@executor(id="future_test")
async def process_future(value: int, ctx: WorkflowContext[int]) -> None:
await ctx.send_message(value * 2)
assert isinstance(process_future, FunctionExecutor)
assert process_future.id == "future_test"
assert int in process_future._handlers
# Check spec
spec = process_future._handler_specs[0]
assert spec["message_type"] is int
assert spec["output_types"] == [int]
def test_executor_decorator_future_annotations_complex(self):
"""Test @executor decorator works with complex stringified annotations."""
@executor
async def process_complex(data: dict[str, Any], ctx: WorkflowContext[list[str]]) -> None:
await ctx.send_message(["done"])
assert isinstance(process_complex, FunctionExecutor)
spec = process_complex._handler_specs[0]
assert spec["message_type"] == dict[str, Any]
assert spec["output_types"] == [list[str]]
@@ -10,6 +10,7 @@ import uuid
from collections.abc import AsyncGenerator
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse
from .models._discovery_models import Deployment, DeploymentConfig, DeploymentEvent
@@ -467,11 +468,18 @@ CMD ["devui", "/app/entity", "--mode", "{config.ui_mode}", "--host", "0.0.0.0",
await event_queue.put(
DeploymentEvent(type="deploy.progress", message=f"Docker build: {line_text}")
)
elif "https://" in line_text and ".azurecontainerapps.io" in line_text:
# Deployment URL detected
await event_queue.put(
DeploymentEvent(type="deploy.progress", message="Deployment URL generated!")
)
elif "https://" in line_text:
# Try to extract all URLs and check if any is on azurecontainerapps.io
urls = re.findall(r'https://[^\s<>"]+', line_text)
for url in urls:
# Strip common trailing punctuation to ensure clean URL parsing
url_clean = url.rstrip(".,;:!?'\")}]")
host = urlparse(url_clean).hostname
if host and (host == "azurecontainerapps.io" or host.endswith(".azurecontainerapps.io")):
await event_queue.put(
DeploymentEvent(type="deploy.progress", message="Deployment URL generated!")
)
break
# Wait for process to complete
return_code = await process.wait()
+394 -29
View File
@@ -24,7 +24,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.3":
"@babel/core@^7.28.3":
version "7.28.3"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz"
integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==
@@ -168,6 +168,153 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@emnapi/core@^1.4.3", "@emnapi/core@^1.4.5":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4"
integrity sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==
dependencies:
"@emnapi/wasi-threads" "1.1.0"
tslib "^2.4.0"
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.4.5":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791"
integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.4":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
dependencies:
tslib "^2.4.0"
"@esbuild/aix-ppc64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
"@esbuild/android-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
"@esbuild/android-arm@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
"@esbuild/android-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
"@esbuild/darwin-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae"
integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
"@esbuild/darwin-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
"@esbuild/freebsd-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
"@esbuild/freebsd-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
"@esbuild/linux-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
"@esbuild/linux-arm@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
"@esbuild/linux-ia32@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
"@esbuild/linux-loong64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
"@esbuild/linux-mips64el@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
"@esbuild/linux-ppc64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
"@esbuild/linux-riscv64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
"@esbuild/linux-s390x@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
"@esbuild/linux-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f"
integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
"@esbuild/netbsd-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
"@esbuild/netbsd-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
"@esbuild/openbsd-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
"@esbuild/openbsd-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
"@esbuild/openharmony-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
"@esbuild/sunos-x64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
"@esbuild/win32-arm64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
"@esbuild/win32-ia32@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
"@esbuild/win32-x64@0.25.9":
version "0.25.9"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz"
@@ -221,7 +368,7 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@^9.33.0", "@eslint/js@9.33.0":
"@eslint/js@9.33.0", "@eslint/js@^9.33.0":
version "9.33.0"
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz"
integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==
@@ -335,6 +482,15 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@napi-rs/wasm-runtime@^0.2.12":
version "0.2.12"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
dependencies:
"@emnapi/core" "^1.4.3"
"@emnapi/runtime" "^1.4.3"
"@tybys/wasm-util" "^0.10.0"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -343,7 +499,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -693,6 +849,101 @@
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz"
integrity sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==
"@rollup/rollup-android-arm-eabi@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz#6e236cd2fd29bb01a300ad4ff6ed0f1a17550e69"
integrity sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g==
"@rollup/rollup-android-arm64@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz#808f2c9c7e68161add613ebcb0eac5a058a0df3c"
integrity sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA==
"@rollup/rollup-darwin-arm64@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz#fa41e413c8e73d61039d6375b234595f24b1e5e3"
integrity sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ==
"@rollup/rollup-darwin-x64@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz#9aac64e886435493f2e3a0aa5e4aad098a90814c"
integrity sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ==
"@rollup/rollup-freebsd-arm64@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz#9fc804264f7b7a7cdad3747950299f990163be1f"
integrity sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw==
"@rollup/rollup-freebsd-x64@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz#933feaff864feb03bbbcd0c18ea351ade957cf79"
integrity sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA==
"@rollup/rollup-linux-arm-gnueabihf@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz#02915e6b2c55fe5961c27404aba2d9c8ef48ac6c"
integrity sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==
"@rollup/rollup-linux-arm-musleabihf@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz#1afef33191b26e76ae7f0d0dc767efc6be1285ce"
integrity sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==
"@rollup/rollup-linux-arm64-gnu@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz#6e7f38fb99d14143de3ce33204e6cd61e1c2c780"
integrity sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==
"@rollup/rollup-linux-arm64-musl@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz#25ab09f14bbcba85a604bcee2962d2486db90794"
integrity sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==
"@rollup/rollup-linux-loongarch64-gnu@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz#d3e3a3fd61e21b2753094391dee9b515a2bc9ecd"
integrity sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==
"@rollup/rollup-linux-ppc64-gnu@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz#6b44445e2bd5866692010de241bf18d2ae8b0cb8"
integrity sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==
"@rollup/rollup-linux-riscv64-gnu@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz#3ff412d20d3b157e6aadabf84788e8c5cb221ba7"
integrity sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==
"@rollup/rollup-linux-riscv64-musl@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz#104f451497d53d82a49c6d08c13c59f5f30eed57"
integrity sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==
"@rollup/rollup-linux-s390x-gnu@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz#d04de7b21d181f30750760cb3553946306506172"
integrity sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==
"@rollup/rollup-linux-x64-gnu@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz#a6ba88ff7480940a435b1e67ddbb3f207a7ae02f"
integrity sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==
"@rollup/rollup-linux-x64-musl@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz#c912c8ffa0c242ed3175cd91cdeaef98109afa54"
integrity sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==
"@rollup/rollup-win32-arm64-msvc@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz#ca5eaae89443554b461bb359112a056528cfdac0"
integrity sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==
"@rollup/rollup-win32-ia32-msvc@4.47.1":
version "4.47.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz#34e76172515fb4b374eb990d59f54faff938246e"
integrity sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ==
"@rollup/rollup-win32-x64-msvc@4.47.1":
version "4.47.1"
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz"
@@ -711,6 +962,68 @@
source-map-js "^1.2.1"
tailwindcss "4.1.12"
"@tailwindcss/oxide-android-arm64@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz#27920fe61fa2743afe8a8ca296fa640b609d17d5"
integrity sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==
"@tailwindcss/oxide-darwin-arm64@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz#e8bd4798f26ec1d012bf0683aeb77449f71505cd"
integrity sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==
"@tailwindcss/oxide-darwin-x64@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz#8ddb7e5ddfd9b049ec84a2bda99f2b04a86859f5"
integrity sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==
"@tailwindcss/oxide-freebsd-x64@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz#da1c0b16b7a5f95a1e400f299a3ec94fb6fd40ac"
integrity sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz#34e558aa6e869c6fe9867cb78ed7ba651b9fcaa4"
integrity sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==
"@tailwindcss/oxide-linux-arm64-gnu@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz#0a00a8146ab6215f81b2d385056c991441bf390e"
integrity sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==
"@tailwindcss/oxide-linux-arm64-musl@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz#b138f494068884ae0d8c343dc1904b22f5e98dc6"
integrity sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==
"@tailwindcss/oxide-linux-x64-gnu@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz#5b9d5f23b15cdb714639f5b9741c0df5d610f794"
integrity sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==
"@tailwindcss/oxide-linux-x64-musl@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz#f68ec530d3ca6875ea9015bcd5dd0762ee5e2f5d"
integrity sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==
"@tailwindcss/oxide-wasm32-wasi@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz#9fd15a1ebde6076c42c445c5e305c31673ead965"
integrity sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==
dependencies:
"@emnapi/core" "^1.4.5"
"@emnapi/runtime" "^1.4.5"
"@emnapi/wasi-threads" "^1.0.4"
"@napi-rs/wasm-runtime" "^0.2.12"
"@tybys/wasm-util" "^0.10.0"
tslib "^2.8.0"
"@tailwindcss/oxide-win32-arm64-msvc@4.1.12":
version "4.1.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz#938bcc6a82e1120ea4fe2ce94be0a8cdf3ae92c7"
integrity sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==
"@tailwindcss/oxide-win32-x64-msvc@4.1.12":
version "4.1.12"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz"
@@ -746,6 +1059,13 @@
"@tailwindcss/oxide" "4.1.12"
tailwindcss "4.1.12"
"@tybys/wasm-util@^0.10.0":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
dependencies:
tslib "^2.4.0"
"@types/babel__core@^7.20.5":
version "7.20.5"
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
@@ -818,7 +1138,7 @@
"@types/d3-interpolate" "*"
"@types/d3-selection" "*"
"@types/estree@^1.0.6", "@types/estree@1.0.8":
"@types/estree@1.0.8", "@types/estree@^1.0.6":
version "1.0.8"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -828,19 +1148,19 @@
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0":
"@types/node@^24.3.0":
version "24.3.0"
resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz"
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
dependencies:
undici-types "~7.10.0"
"@types/react-dom@*", "@types/react-dom@^19.1.7":
"@types/react-dom@^19.1.7":
version "19.1.7"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz"
integrity sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==
"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.10", "@types/react@>=16.8", "@types/react@>=18.0.0":
"@types/react@^19.1.10":
version "19.1.10"
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz"
integrity sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==
@@ -862,7 +1182,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^8.40.0", "@typescript-eslint/parser@8.40.0":
"@typescript-eslint/parser@8.40.0":
version "8.40.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz"
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
@@ -890,7 +1210,7 @@
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/tsconfig-utils@^8.40.0", "@typescript-eslint/tsconfig-utils@8.40.0":
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
version "8.40.0"
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz"
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
@@ -906,7 +1226,7 @@
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@8.40.0":
"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0":
version "8.40.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz"
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
@@ -986,7 +1306,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -1047,7 +1367,7 @@ braces@^3.0.3:
dependencies:
fill-range "^7.1.1"
browserslist@^4.24.0, "browserslist@>= 4.21.0":
browserslist@^4.24.0:
version "4.25.3"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz"
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
@@ -1143,7 +1463,7 @@ csstype@^3.0.2:
resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
d3-drag@^3.0.0, "d3-drag@2 - 3":
"d3-drag@2 - 3", d3-drag@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
@@ -1156,14 +1476,14 @@ d3-drag@^3.0.0, "d3-drag@2 - 3":
resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
d3-interpolate@^3.0.1, "d3-interpolate@1 - 3":
"d3-interpolate@1 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3:
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
@@ -1300,7 +1620,7 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.33.0, eslint@>=8.40:
eslint@^9.33.0:
version "9.33.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz"
integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==
@@ -1546,7 +1866,7 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jiti@*, jiti@^2.5.1, jiti@>=1.21.0:
jiti@^2.5.1:
version "2.5.1"
resolved "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz"
integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==
@@ -1557,9 +1877,9 @@ js-tokens@^4.0.0:
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@@ -1603,12 +1923,57 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lightningcss-darwin-arm64@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae"
integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==
lightningcss-darwin-x64@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22"
integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==
lightningcss-freebsd-x64@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4"
integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==
lightningcss-linux-arm-gnueabihf@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908"
integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==
lightningcss-linux-arm64-gnu@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009"
integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==
lightningcss-linux-arm64-musl@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe"
integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==
lightningcss-linux-x64-gnu@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157"
integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==
lightningcss-linux-x64-musl@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26"
integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==
lightningcss-win32-arm64-msvc@1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039"
integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==
lightningcss-win32-x64-msvc@1.30.1:
version "1.30.1"
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz"
integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==
lightningcss@^1.21.0, lightningcss@1.30.1:
lightningcss@1.30.1:
version "1.30.1"
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz"
integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==
@@ -1779,7 +2144,7 @@ picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
"picomatch@^3 || ^4", picomatch@^4.0.3:
picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
@@ -1808,7 +2173,7 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react-dom@^19.1.1, react-dom@>=16.8.0, react-dom@>=17:
react-dom@^19.1.1:
version "19.1.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz"
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
@@ -1847,7 +2212,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
get-nonce "^1.0.0"
tslib "^2.0.0"
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^19.1.1, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18.0.0:
react@^19.1.1:
version "19.1.1"
resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz"
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
@@ -1947,7 +2312,7 @@ tailwind-merge@^3.3.1:
resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz"
integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==
tailwindcss@^4.1.12, tailwindcss@4.1.12:
tailwindcss@4.1.12, tailwindcss@^4.1.12:
version "4.1.12"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz"
integrity sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==
@@ -1989,7 +2354,7 @@ ts-api-utils@^2.1.0:
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
tslib@^2.0.0, tslib@^2.1.0:
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -2016,7 +2381,7 @@ typescript-eslint@^8.39.1:
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/utils" "8.40.0"
typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@~5.8.3:
typescript@~5.8.3:
version "5.8.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
@@ -2056,12 +2421,12 @@ use-sidecar@^1.1.3:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@^1.2.2, use-sync-external-store@>=1.2.0:
use-sync-external-store@^1.2.2:
version "1.5.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.1.11:
vite@^7.1.11:
version "7.1.12"
resolved "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz"
integrity sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==
@@ -9,10 +9,16 @@ This folder contains examples demonstrating how to use Anthropic's Claude models
| [`anthropic_basic.py`](anthropic_basic.py) | Demonstrates how to setup a simple agent using the AnthropicClient, with both streaming and non-streaming responses. |
| [`anthropic_advanced.py`](anthropic_advanced.py) | Shows advanced usage of the AnthropicClient, including hosted tools and `thinking`. |
| [`anthropic_skills.py`](anthropic_skills.py) | Illustrates how to use Anthropic-managed Skills with an agent, including the Code Interpreter tool and file generation and saving. |
| [`anthropic_foundry.py`](anthropic_foundry.py) | Example of using Foundry's Anthropic integration with the Agent Framework. |
## Environment Variables
Set the following environment variables before running the examples:
- `ANTHROPIC_API_KEY`: Your Anthropic API key (get one from [Anthropic Console](https://console.anthropic.com/))
- `ANTHROPIC_MODEL`: The Claude model to use (e.g., `claude-haiku-4-5`, `claude-sonnet-4-5-20250929`)
- `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use (e.g., `claude-haiku-4-5`, `claude-sonnet-4-5-20250929`)
Or, for Foundry:
- `ANTHROPIC_FOUNDRY_API_KEY`: Your Foundry Anthropic API key
- `ANTHROPIC_FOUNDRY_ENDPOINT`: The endpoint URL for your Foundry Anthropic resource
- `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use in Foundry (e.g., `claude-haiku-4-5`)
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import HostedMCPTool, HostedWebSearchTool, TextReasoningContent, UsageContent
from agent_framework.anthropic import AnthropicClient
from anthropic import AsyncAnthropicFoundry
"""
Anthropic Foundry Chat Agent Example
This sample demonstrates using Anthropic with:
- Setting up an Anthropic-based agent with hosted tools.
- Using the `thinking` feature.
- Displaying both thinking and usage information during streaming responses.
This example requires `anthropic>=0.74.0` and an endpoint in Foundry for Anthropic.
To use the Foundry integration ensure you have the following environment variables set:
- ANTHROPIC_FOUNDRY_API_KEY
Alternatively you can pass in a azure_ad_token_provider function to the AsyncAnthropicFoundry constructor.
- ANTHROPIC_FOUNDRY_ENDPOINT
Should be something like https://<your-resource-name>.services.ai.azure.com/anthropic/
- ANTHROPIC_CHAT_MODEL_ID
Should be something like claude-haiku-4-5
"""
async def main() -> None:
"""Example of streaming response (get results as they are generated)."""
agent = AnthropicClient(anthropic_client=AsyncAnthropicFoundry()).create_agent(
name="DocsAgent",
instructions="You are a helpful agent for both Microsoft docs questions and general questions.",
tools=[
HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
HostedWebSearchTool(),
],
# anthropic needs a value for the max_tokens parameter
# we set it to 1024, but you can override like this:
max_tokens=20000,
additional_chat_options={"thinking": {"type": "enabled", "budget_tokens": 10000}},
)
query = "Can you compare Python decorators with C# attributes?"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run_stream(query):
for content in chunk.contents:
if isinstance(content, TextReasoningContent):
print(f"\033[32m{content.text}\033[0m", end="", flush=True)
if isinstance(content, UsageContent):
print(f"\n\033[34m[Usage so far: {content.details}]\033[0m\n", end="", flush=True)
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -8,6 +8,7 @@ This folder contains examples demonstrating different ways to create and use age
|------|-------------|
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIClient`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. |
| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. |
| [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. |
| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. |
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. |
| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. |
@@ -19,6 +20,7 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. |
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. |
| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. |
| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. |
| [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use the `ImageGenTool` with Azure AI agents to generate images based on text prompts. |
| [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. |
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.azure import AzureAIClient
from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent with Agent-to-Agent (A2A) Example
This sample demonstrates usage of AzureAIClient with Agent-to-Agent (A2A) capabilities
to enable communication with other agents using the A2A protocol.
Prerequisites:
1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables.
2. Ensure you have an A2A connection configured in your Azure AI project
and set A2A_PROJECT_CONNECTION_ID environment variable.
"""
async def main() -> None:
async with (
AzureCliCredential() as credential,
AzureAIClient(async_credential=credential).create_agent(
name="MyA2AAgent",
instructions="""You are a helpful assistant that can communicate with other agents.
Use the A2A tool when you need to interact with other agents to complete tasks
or gather information from specialized agents.""",
tools={
"type": "a2a_preview",
"project_connection_id": os.environ["A2A_PROJECT_CONNECTION_ID"],
},
) as agent,
):
query = "What can the secondary agent do?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Result: {result}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,43 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.azure import AzureAIClient
from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent with SharePoint Example
This sample demonstrates usage of AzureAIClient with SharePoint
to search through SharePoint content and answer user questions about it.
Prerequisites:
1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables.
2. Ensure you have a SharePoint connection configured in your Azure AI project
and set SHAREPOINT_PROJECT_CONNECTION_ID environment variable.
"""
async def main() -> None:
async with (
AzureCliCredential() as credential,
AzureAIClient(async_credential=credential).create_agent(
name="MySharePointAgent",
instructions="""You are a helpful agent that can use SharePoint tools to assist users.
Use the available SharePoint tools to answer questions and perform tasks.""",
tools={
"type": "sharepoint_grounding_preview",
"sharepoint_grounding_preview": {
"project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"]
},
},
) as agent,
):
query = "What is Contoso whistleblower policy?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Result: {result}\n")
if __name__ == "__main__":
asyncio.run(main())
+398 -394
View File
File diff suppressed because it is too large Load Diff