mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
a71f768331
* delete the AIAgent.DisplayName property * use agent name as a first value for activity display name * Update dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
304 lines
12 KiB
C#
304 lines
12 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Text.Json;
|
|
using Microsoft.Agents.AI.DevUI.Entities;
|
|
using Microsoft.Agents.AI.Workflows;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.DevUI;
|
|
|
|
/// <summary>
|
|
/// Provides extension methods for mapping entity discovery and management endpoints to an <see cref="IEndpointRouteBuilder"/>.
|
|
/// </summary>
|
|
internal static class EntitiesApiExtensions
|
|
{
|
|
/// <summary>
|
|
/// Maps HTTP API endpoints for entity discovery and management.
|
|
/// </summary>
|
|
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the routes to.</param>
|
|
/// <returns>The <see cref="IEndpointRouteBuilder"/> for method chaining.</returns>
|
|
/// <remarks>
|
|
/// This extension method registers the following endpoints:
|
|
/// <list type="bullet">
|
|
/// <item><description>GET /v1/entities - List all registered entities (agents and workflows)</description></item>
|
|
/// <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="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("", (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", (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")
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
return group;
|
|
}
|
|
|
|
private static async Task<IResult> ListEntitiesAsync(
|
|
IEnumerable<AIAgent> agents,
|
|
IEnumerable<Workflow> workflows,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var entities = new Dictionary<string, EntityInfo>();
|
|
|
|
// Discover agents
|
|
foreach (var agentInfo in DiscoverAgents(agents, entityIdFilter: null))
|
|
{
|
|
entities[agentInfo.Id] = agentInfo;
|
|
}
|
|
|
|
// Discover workflows
|
|
foreach (var workflowInfo in DiscoverWorkflows(workflows, entityIdFilter: null))
|
|
{
|
|
entities[workflowInfo.Id] = workflowInfo;
|
|
}
|
|
|
|
return Results.Json(new DiscoveryResponse([.. entities.Values.OrderBy(e => e.Id)]), EntitiesJsonContext.Default.DiscoveryResponse);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Results.Problem(
|
|
detail: ex.Message,
|
|
statusCode: StatusCodes.Status500InternalServerError,
|
|
title: "Error listing entities");
|
|
}
|
|
}
|
|
|
|
private static async Task<IResult> GetEntityInfoAsync(
|
|
string entityId,
|
|
string? type,
|
|
IEnumerable<AIAgent> agents,
|
|
IEnumerable<Workflow> workflows,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreach (var workflowInfo in DiscoverWorkflows(workflows, entityId))
|
|
{
|
|
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
|
|
}
|
|
}
|
|
|
|
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreach (var agentInfo in DiscoverAgents(agents, entityId))
|
|
{
|
|
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
|
|
}
|
|
}
|
|
|
|
return Results.NotFound(new { error = new { message = $"Entity '{entityId}' not found.", type = "invalid_request_error" } });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Results.Problem(
|
|
detail: ex.Message,
|
|
statusCode: StatusCodes.Status500InternalServerError,
|
|
title: "Error getting entity info");
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<EntityInfo> DiscoverAgents(IEnumerable<AIAgent> agents, string? entityIdFilter)
|
|
{
|
|
foreach (var agent in agents)
|
|
{
|
|
// If filtering by entity ID, skip non-matching agents
|
|
if (entityIdFilter is not null &&
|
|
!string.Equals(agent.Name, entityIdFilter, StringComparison.OrdinalIgnoreCase) &&
|
|
!string.Equals(agent.Id, entityIdFilter, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return CreateAgentEntityInfo(agent);
|
|
|
|
// If we found the entity we're looking for, we're done
|
|
if (entityIdFilter is not null)
|
|
{
|
|
yield break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<EntityInfo> DiscoverWorkflows(IEnumerable<Workflow> workflows, string? entityIdFilter)
|
|
{
|
|
foreach (var workflow in workflows)
|
|
{
|
|
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
|
|
|
// If filtering by entity ID, skip non-matching workflows
|
|
if (entityIdFilter is not null && !string.Equals(workflowId, entityIdFilter, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return CreateWorkflowEntityInfo(workflow);
|
|
|
|
// If we found the entity we're looking for, we're done
|
|
if (entityIdFilter is not null)
|
|
{
|
|
yield break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
|
|
{
|
|
var entityId = agent.Name ?? agent.Id;
|
|
|
|
// Extract tools and other metadata using GetService
|
|
List<string> tools = [];
|
|
var metadata = new Dictionary<string, JsonElement>();
|
|
|
|
// Try to get ChatOptions from the agent which may contain tools
|
|
if (agent.GetService<ChatOptions>() is { Tools: { Count: > 0 } agentTools })
|
|
{
|
|
tools = agentTools
|
|
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
|
|
.Select(tool => tool.Name!)
|
|
.Distinct()
|
|
.ToList();
|
|
}
|
|
|
|
// Extract agent-specific fields (top-level properties for compatibility with Python)
|
|
string? instructions = null;
|
|
string? modelId = null;
|
|
string? chatClientType = null;
|
|
|
|
// Get instructions from ChatClientAgent
|
|
if (agent is ChatClientAgent chatAgent && !string.IsNullOrWhiteSpace(chatAgent.Instructions))
|
|
{
|
|
instructions = chatAgent.Instructions;
|
|
}
|
|
|
|
// Get IChatClient to extract metadata
|
|
IChatClient? chatClient = agent.GetService<IChatClient>();
|
|
if (chatClient != null)
|
|
{
|
|
// Get chat client type
|
|
chatClientType = chatClient.GetType().Name;
|
|
|
|
// Get model ID from ChatClientMetadata
|
|
if (chatClient.GetService<ChatClientMetadata>() is { } chatClientMetadata)
|
|
{
|
|
modelId = chatClientMetadata.DefaultModelId;
|
|
|
|
// Add additional metadata for compatibility
|
|
if (!string.IsNullOrWhiteSpace(chatClientMetadata.ProviderName))
|
|
{
|
|
metadata["chat_client_provider"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderName, EntitiesJsonContext.Default.String);
|
|
}
|
|
|
|
if (chatClientMetadata.ProviderUri is not null)
|
|
{
|
|
metadata["provider_uri"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderUri.ToString(), EntitiesJsonContext.Default.String);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add provider name from AIAgentMetadata if available
|
|
if (agent.GetService<AIAgentMetadata>() is { } agentMetadata && !string.IsNullOrWhiteSpace(agentMetadata.ProviderName))
|
|
{
|
|
metadata["provider_name"] = JsonSerializer.SerializeToElement(agentMetadata.ProviderName, EntitiesJsonContext.Default.String);
|
|
}
|
|
|
|
// Add agent type information to metadata (in addition to chat_client_type)
|
|
var agentTypeName = agent.GetType().Name;
|
|
metadata["agent_type"] = JsonSerializer.SerializeToElement(agentTypeName, EntitiesJsonContext.Default.String);
|
|
|
|
return new EntityInfo(
|
|
Id: entityId,
|
|
Type: "agent",
|
|
Name: agent.Name ?? agent.Id,
|
|
Description: agent.Description,
|
|
Framework: "agent_framework",
|
|
Tools: tools,
|
|
Metadata: metadata
|
|
)
|
|
{
|
|
Source = "in_memory",
|
|
Instructions = instructions,
|
|
ModelId = modelId,
|
|
ChatClientType = chatClientType,
|
|
Executors = [], // Agents have empty executors list (workflows use this field)
|
|
};
|
|
}
|
|
|
|
private static EntityInfo CreateWorkflowEntityInfo(Workflow workflow)
|
|
{
|
|
// Extract executor IDs from the workflow structure
|
|
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
|
var reflectedEdges = workflow.ReflectEdges();
|
|
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
|
{
|
|
executorIds.Add(sourceId);
|
|
foreach (var edge in edgeSet)
|
|
{
|
|
foreach (var sinkId in edge.Connection.SinkIds)
|
|
{
|
|
executorIds.Add(sinkId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a default input schema (string type)
|
|
var defaultInputSchema = new Dictionary<string, string>
|
|
{
|
|
["type"] = "string"
|
|
};
|
|
|
|
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
|
return new EntityInfo(
|
|
Id: workflowId,
|
|
Type: "workflow",
|
|
Name: workflowId,
|
|
Description: workflow.Description,
|
|
Framework: "agent_framework",
|
|
Tools: [],
|
|
Metadata: []
|
|
)
|
|
{
|
|
Source = "in_memory",
|
|
Executors = [.. executorIds], // Workflows use Executors instead of Tools
|
|
WorkflowDump = JsonSerializer.SerializeToElement(
|
|
workflow.ToDevUIDict(),
|
|
EntitiesJsonContext.Default.DictionaryStringJsonElement),
|
|
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema, EntitiesJsonContext.Default.DictionaryStringString),
|
|
InputTypeName = "string",
|
|
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);
|
|
}
|
|
}
|