Skip to content

Commit 12fc19b

Browse files
authored
DevUI: support having both an agent and a workflow with the same id in discovery (#2023)
1 parent 7a45929 commit 12fc19b

File tree

3 files changed

+174
-180
lines changed

3 files changed

+174
-180
lines changed
Lines changed: 135 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System.Runtime.CompilerServices;
34
using System.Text.Json;
45

56
using Microsoft.Agents.AI.DevUI.Entities;
67
using Microsoft.Agents.AI.Hosting;
8+
using Microsoft.Agents.AI.Workflows;
79

810
namespace Microsoft.Agents.AI.DevUI;
911

@@ -56,79 +58,19 @@ private static async Task<IResult> ListEntitiesAsync(
5658
{
5759
var entities = new List<EntityInfo>();
5860

59-
// Discover agents from the agent catalog
60-
if (agentCatalog is not null)
61+
// Discover agents
62+
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
6163
{
62-
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
63-
{
64-
if (agent.GetType().Name == "WorkflowHostAgent")
65-
{
66-
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
67-
// and workflows are handled below.
68-
continue;
69-
}
70-
71-
entities.Add(new EntityInfo(
72-
Id: agent.Name ?? agent.Id,
73-
Type: "agent",
74-
Name: agent.Name ?? agent.Id,
75-
Description: agent.Description,
76-
Framework: "agent-framework",
77-
Tools: null,
78-
Metadata: []
79-
)
80-
{
81-
Source = "in_memory"
82-
});
83-
}
64+
entities.Add(agentInfo);
8465
}
8566

86-
// Discover workflows from the workflow catalog
87-
if (workflowCatalog is not null)
67+
// Discover workflows
68+
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
8869
{
89-
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
90-
{
91-
// Extract executor IDs from the workflow structure
92-
var executorIds = new HashSet<string> { workflow.StartExecutorId };
93-
var reflectedEdges = workflow.ReflectEdges();
94-
foreach (var (sourceId, edgeSet) in reflectedEdges)
95-
{
96-
executorIds.Add(sourceId);
97-
foreach (var edge in edgeSet)
98-
{
99-
foreach (var sinkId in edge.Connection.SinkIds)
100-
{
101-
executorIds.Add(sinkId);
102-
}
103-
}
104-
}
105-
106-
// Create a default input schema (string type)
107-
var defaultInputSchema = new Dictionary<string, object>
108-
{
109-
["type"] = "string"
110-
};
111-
112-
entities.Add(new EntityInfo(
113-
Id: workflow.Name ?? workflow.StartExecutorId,
114-
Type: "workflow",
115-
Name: workflow.Name ?? workflow.StartExecutorId,
116-
Description: workflow.Description,
117-
Framework: "agent-framework",
118-
Tools: [.. executorIds],
119-
Metadata: []
120-
)
121-
{
122-
Source = "in_memory",
123-
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
124-
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
125-
InputTypeName = "string",
126-
StartExecutorId = workflow.StartExecutorId
127-
});
128-
}
70+
entities.Add(workflowInfo);
12971
}
13072

131-
return Results.Json(new DiscoveryResponse(entities), EntitiesJsonContext.Default.DiscoveryResponse);
73+
return Results.Json(new DiscoveryResponse([.. entities]), EntitiesJsonContext.Default.DiscoveryResponse);
13274
}
13375
catch (Exception ex)
13476
{
@@ -141,93 +83,26 @@ private static async Task<IResult> ListEntitiesAsync(
14183

14284
private static async Task<IResult> GetEntityInfoAsync(
14385
string entityId,
86+
string? type,
14487
AgentCatalog? agentCatalog,
14588
WorkflowCatalog? workflowCatalog,
14689
CancellationToken cancellationToken)
14790
{
14891
try
14992
{
150-
// Try to find the entity among discovered agents
151-
if (agentCatalog is not null)
93+
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
15294
{
153-
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
95+
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
15496
{
155-
if (agent.GetType().Name == "WorkflowHostAgent")
156-
{
157-
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
158-
// and workflows are handled below.
159-
continue;
160-
}
161-
162-
if (string.Equals(agent.Name, entityId, StringComparison.OrdinalIgnoreCase) ||
163-
string.Equals(agent.Id, entityId, StringComparison.OrdinalIgnoreCase))
164-
{
165-
var entityInfo = new EntityInfo(
166-
Id: agent.Name ?? agent.Id,
167-
Type: "agent",
168-
Name: agent.Name ?? agent.Id,
169-
Description: agent.Description,
170-
Framework: "agent-framework",
171-
Tools: null,
172-
Metadata: []
173-
)
174-
{
175-
Source = "in_memory"
176-
};
177-
178-
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
179-
}
97+
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
18098
}
18199
}
182100

183-
// Try to find the entity among discovered workflows
184-
if (workflowCatalog is not null)
101+
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
185102
{
186-
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
103+
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
187104
{
188-
var workflowId = workflow.Name ?? workflow.StartExecutorId;
189-
if (string.Equals(workflowId, entityId, StringComparison.OrdinalIgnoreCase))
190-
{
191-
// Extract executor IDs from the workflow structure
192-
var executorIds = new HashSet<string> { workflow.StartExecutorId };
193-
var reflectedEdges = workflow.ReflectEdges();
194-
foreach (var (sourceId, edgeSet) in reflectedEdges)
195-
{
196-
executorIds.Add(sourceId);
197-
foreach (var edge in edgeSet)
198-
{
199-
foreach (var sinkId in edge.Connection.SinkIds)
200-
{
201-
executorIds.Add(sinkId);
202-
}
203-
}
204-
}
205-
206-
// Create a default input schema (string type)
207-
var defaultInputSchema = new Dictionary<string, object>
208-
{
209-
["type"] = "string"
210-
};
211-
212-
var entityInfo = new EntityInfo(
213-
Id: workflowId,
214-
Type: "workflow",
215-
Name: workflow.Name ?? workflow.StartExecutorId,
216-
Description: workflow.Description,
217-
Framework: "agent-framework",
218-
Tools: [.. executorIds],
219-
Metadata: []
220-
)
221-
{
222-
Source = "in_memory",
223-
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
224-
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
225-
InputTypeName = "Input",
226-
StartExecutorId = workflow.StartExecutorId
227-
};
228-
229-
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
230-
}
105+
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
231106
}
232107
}
233108

@@ -241,4 +116,123 @@ private static async Task<IResult> GetEntityInfoAsync(
241116
title: "Error getting entity info");
242117
}
243118
}
119+
120+
private static async IAsyncEnumerable<EntityInfo> DiscoverAgentsAsync(
121+
AgentCatalog? agentCatalog,
122+
string? entityIdFilter,
123+
[EnumeratorCancellation] CancellationToken cancellationToken)
124+
{
125+
if (agentCatalog is null)
126+
{
127+
yield break;
128+
}
129+
130+
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
131+
{
132+
// If filtering by entity ID, skip non-matching agents
133+
if (entityIdFilter is not null &&
134+
!string.Equals(agent.Name, entityIdFilter, StringComparison.OrdinalIgnoreCase) &&
135+
!string.Equals(agent.Id, entityIdFilter, StringComparison.OrdinalIgnoreCase))
136+
{
137+
continue;
138+
}
139+
140+
yield return CreateAgentEntityInfo(agent);
141+
142+
// If we found the entity we're looking for, we're done
143+
if (entityIdFilter is not null)
144+
{
145+
yield break;
146+
}
147+
}
148+
}
149+
150+
private static async IAsyncEnumerable<EntityInfo> DiscoverWorkflowsAsync(
151+
WorkflowCatalog? workflowCatalog,
152+
string? entityIdFilter,
153+
[EnumeratorCancellation] CancellationToken cancellationToken)
154+
{
155+
if (workflowCatalog is null)
156+
{
157+
yield break;
158+
}
159+
160+
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
161+
{
162+
var workflowId = workflow.Name ?? workflow.StartExecutorId;
163+
164+
// If filtering by entity ID, skip non-matching workflows
165+
if (entityIdFilter is not null && !string.Equals(workflowId, entityIdFilter, StringComparison.OrdinalIgnoreCase))
166+
{
167+
continue;
168+
}
169+
170+
yield return CreateWorkflowEntityInfo(workflow);
171+
172+
// If we found the entity we're looking for, we're done
173+
if (entityIdFilter is not null)
174+
{
175+
yield break;
176+
}
177+
}
178+
}
179+
180+
private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
181+
{
182+
var entityId = agent.Name ?? agent.Id;
183+
return new EntityInfo(
184+
Id: entityId,
185+
Type: "agent",
186+
Name: entityId,
187+
Description: agent.Description,
188+
Framework: "agent-framework",
189+
Tools: null,
190+
Metadata: []
191+
)
192+
{
193+
Source = "in_memory"
194+
};
195+
}
196+
197+
private static EntityInfo CreateWorkflowEntityInfo(Workflow workflow)
198+
{
199+
// Extract executor IDs from the workflow structure
200+
var executorIds = new HashSet<string> { workflow.StartExecutorId };
201+
var reflectedEdges = workflow.ReflectEdges();
202+
foreach (var (sourceId, edgeSet) in reflectedEdges)
203+
{
204+
executorIds.Add(sourceId);
205+
foreach (var edge in edgeSet)
206+
{
207+
foreach (var sinkId in edge.Connection.SinkIds)
208+
{
209+
executorIds.Add(sinkId);
210+
}
211+
}
212+
}
213+
214+
// Create a default input schema (string type)
215+
var defaultInputSchema = new Dictionary<string, object>
216+
{
217+
["type"] = "string"
218+
};
219+
220+
var workflowId = workflow.Name ?? workflow.StartExecutorId;
221+
return new EntityInfo(
222+
Id: workflowId,
223+
Type: "workflow",
224+
Name: workflowId,
225+
Description: workflow.Description,
226+
Framework: "agent-framework",
227+
Tools: [.. executorIds],
228+
Metadata: []
229+
)
230+
{
231+
Source = "in_memory",
232+
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
233+
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
234+
InputTypeName = "string",
235+
StartExecutorId = workflow.StartExecutorId
236+
};
237+
}
244238
}

0 commit comments

Comments
 (0)