// 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;
namespace Microsoft.Agents.AI.DevUI;
///
/// Provides extension methods for mapping entity discovery and management endpoints to an .
///
internal static class EntitiesApiExtensions
{
///
/// Maps HTTP API endpoints for entity discovery and management.
///
/// The to add the routes to.
/// The for method chaining.
///
/// This extension method registers the following endpoints:
///
/// - GET /v1/entities - List all registered entities (agents and workflows)
/// - GET /v1/entities/{entityId}/info - Get detailed information about a specific entity
///
/// The endpoints are compatible with the Python DevUI frontend and automatically discover entities
/// from the registered and services.
///
public static IEndpointConventionBuilder MapEntities(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/v1/entities")
.WithTags("Entities");
// List all entities
group.MapGet("", ListEntitiesAsync)
.WithName("ListEntities")
.WithSummary("List all registered entities (agents and workflows)")
.Produces(StatusCodes.Status200OK, contentType: "application/json");
// Get detailed entity information
group.MapGet("{entityId}/info", GetEntityInfoAsync)
.WithName("GetEntityInfo")
.WithSummary("Get detailed information about a specific entity")
.Produces(StatusCodes.Status200OK, contentType: "application/json")
.Produces(StatusCodes.Status404NotFound);
return group;
}
private static async Task ListEntitiesAsync(
AgentCatalog? agentCatalog,
WorkflowCatalog? workflowCatalog,
CancellationToken cancellationToken)
{
try
{
var entities = new Dictionary();
// Discover agents
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
{
entities[agentInfo.Id] = agentInfo;
}
// Discover workflows
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
{
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 GetEntityInfoAsync(
string entityId,
string? type,
AgentCatalog? agentCatalog,
WorkflowCatalog? workflowCatalog,
CancellationToken cancellationToken)
{
try
{
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
{
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
{
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
}
}
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
{
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
{
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 async IAsyncEnumerable DiscoverAgentsAsync(
AgentCatalog? agentCatalog,
string? entityIdFilter,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
if (agentCatalog is null)
{
yield break;
}
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
{
// 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 async IAsyncEnumerable DiscoverWorkflowsAsync(
WorkflowCatalog? workflowCatalog,
string? entityIdFilter,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
if (workflowCatalog is null)
{
yield break;
}
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
{
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 tools = [];
var metadata = new Dictionary();
// Try to get ChatOptions from the agent which may contain tools
if (agent.GetService() 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();
if (chatClient != null)
{
// Get chat client type
chatClientType = chatClient.GetType().Name;
// Get model ID from ChatClientMetadata
if (chatClient.GetService() 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() 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.DisplayName,
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 { 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
{
["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
};
}
}