mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add DevUI to AgentFramework (#781)
* add initial backend service code for devui * add tests * add frontendcode * ui updates * update readme * ui updates and tweaks * update ui bundle * improve ui, add react flow base * add react flow ui, fix background * update ui, fix introspection bug * update readme * update ui build * add support for multimodal input - both backend and frontend * update ui build * refactor as main framework package * backend and tests refactor * ui build update * ui build update and refactor * update pyproject.toml, update uv.lock * update ui build * ui update to fit oai responses types * add backend updat and readme update * mypy and other fixes * add intial dev guide * update ui and fix workflow bug * update ui build, add thread support * type fixes * update workflow view * update uv.lock * fix workflow iport errors * lint and other fixes * mypy fixes * minor update * update ui build * refactor to use oai dependencies directly, update examples to samples, improve typing * readme update * update ui and ui build * fix workflow pyright error * update ui, fix issues with run workflow placement, miniamp menu, etc * make samples integrate serve --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
adb6dcd2af
commit
1ef24d3e91
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* API client for DevUI backend
|
||||
* Handles agents, workflows, streaming, and session management
|
||||
*/
|
||||
|
||||
import type {
|
||||
AgentInfo,
|
||||
HealthResponse,
|
||||
RunAgentRequest,
|
||||
RunWorkflowRequest,
|
||||
ThreadInfo,
|
||||
} from "@/types";
|
||||
import type { AgentFrameworkRequest } from "@/types/agent-framework";
|
||||
import type { ExtendedResponseStreamEvent } from "@/types/openai";
|
||||
|
||||
// Backend API response types to match Python Pydantic models
|
||||
interface EntityInfo {
|
||||
id: string;
|
||||
type: "agent" | "workflow";
|
||||
name: string;
|
||||
description?: string;
|
||||
framework: string;
|
||||
tools?: (string | Record<string, unknown>)[];
|
||||
metadata: Record<string, unknown>;
|
||||
executors?: string[];
|
||||
workflow_dump?: Record<string, unknown>;
|
||||
input_schema?: Record<string, unknown>;
|
||||
input_type_name?: string;
|
||||
start_executor_id?: string;
|
||||
}
|
||||
|
||||
interface DiscoveryResponse {
|
||||
entities: EntityInfo[];
|
||||
}
|
||||
|
||||
interface ThreadApiResponse {
|
||||
id: string;
|
||||
object: "thread";
|
||||
created_at: number;
|
||||
metadata: { agent_id: string };
|
||||
}
|
||||
|
||||
interface ThreadListResponse {
|
||||
object: "list";
|
||||
data: ThreadApiObject[];
|
||||
}
|
||||
|
||||
interface ThreadApiObject {
|
||||
id: string;
|
||||
object: "thread";
|
||||
agent_id: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL !== undefined
|
||||
? import.meta.env.VITE_API_BASE_URL
|
||||
: "http://localhost:8080";
|
||||
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string = API_BASE_URL) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`API request failed: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Health check
|
||||
async getHealth(): Promise<HealthResponse> {
|
||||
return this.request<HealthResponse>("/health");
|
||||
}
|
||||
|
||||
// Entity discovery using new unified endpoint
|
||||
async getEntities(): Promise<{
|
||||
entities: (AgentInfo | import("@/types").WorkflowInfo)[];
|
||||
agents: AgentInfo[];
|
||||
workflows: import("@/types").WorkflowInfo[];
|
||||
}> {
|
||||
const response = await this.request<DiscoveryResponse>("/v1/entities");
|
||||
|
||||
// Separate agents and workflows
|
||||
const agents: AgentInfo[] = [];
|
||||
const workflows: import("@/types").WorkflowInfo[] = [];
|
||||
|
||||
response.entities.forEach((entity) => {
|
||||
if (entity.type === "agent") {
|
||||
agents.push({
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
type: "agent",
|
||||
source: "directory", // Default source
|
||||
tools: (entity.tools || []).map((tool) =>
|
||||
typeof tool === "string" ? tool : JSON.stringify(tool)
|
||||
),
|
||||
has_env: false, // Default value
|
||||
module_path:
|
||||
typeof entity.metadata?.module_path === "string"
|
||||
? entity.metadata.module_path
|
||||
: undefined,
|
||||
});
|
||||
} else if (entity.type === "workflow") {
|
||||
const firstTool = entity.tools?.[0];
|
||||
const startExecutorId = typeof firstTool === "string" ? firstTool : "";
|
||||
|
||||
workflows.push({
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
type: "workflow",
|
||||
source: "directory",
|
||||
executors: (entity.tools || []).map((tool) =>
|
||||
typeof tool === "string" ? tool : JSON.stringify(tool)
|
||||
),
|
||||
has_env: false,
|
||||
module_path:
|
||||
typeof entity.metadata?.module_path === "string"
|
||||
? entity.metadata.module_path
|
||||
: undefined,
|
||||
input_schema:
|
||||
(entity.input_schema as unknown as import("@/types").JSONSchema) || {
|
||||
type: "string",
|
||||
}, // Default schema
|
||||
input_type_name: entity.input_type_name || "Input",
|
||||
start_executor_id: startExecutorId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { entities: [...agents, ...workflows], agents, workflows };
|
||||
}
|
||||
|
||||
// Legacy methods for compatibility
|
||||
async getAgents(): Promise<AgentInfo[]> {
|
||||
const { agents } = await this.getEntities();
|
||||
return agents;
|
||||
}
|
||||
|
||||
async getWorkflows(): Promise<import("@/types").WorkflowInfo[]> {
|
||||
const { workflows } = await this.getEntities();
|
||||
return workflows;
|
||||
}
|
||||
|
||||
async getAgentInfo(agentId: string): Promise<AgentInfo> {
|
||||
// Get detailed entity info from unified endpoint
|
||||
return this.request<AgentInfo>(`/v1/entities/${agentId}/info`);
|
||||
}
|
||||
|
||||
async getWorkflowInfo(
|
||||
workflowId: string
|
||||
): Promise<import("@/types").WorkflowInfo> {
|
||||
// Get detailed entity info from unified endpoint
|
||||
return this.request<import("@/types").WorkflowInfo>(
|
||||
`/v1/entities/${workflowId}/info`
|
||||
);
|
||||
}
|
||||
|
||||
// Thread management using real /v1/threads endpoints
|
||||
async createThread(agentId: string): Promise<ThreadInfo> {
|
||||
const response = await this.request<ThreadApiResponse>("/v1/threads", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ agent_id: agentId }),
|
||||
});
|
||||
|
||||
return {
|
||||
id: response.id,
|
||||
agent_id: agentId,
|
||||
created_at: new Date(response.created_at * 1000).toISOString(),
|
||||
message_count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async getThreads(agentId: string): Promise<ThreadInfo[]> {
|
||||
const response = await this.request<ThreadListResponse>(
|
||||
`/v1/threads?agent_id=${agentId}`
|
||||
);
|
||||
return response.data.map((thread: ThreadApiObject) => ({
|
||||
id: thread.id,
|
||||
agent_id: thread.agent_id,
|
||||
created_at: thread.created_at || new Date().toISOString(),
|
||||
message_count: 0, // We don't track this yet
|
||||
}));
|
||||
}
|
||||
|
||||
async deleteThread(threadId: string): Promise<boolean> {
|
||||
try {
|
||||
await this.request(`/v1/threads/${threadId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getThreadMessages(
|
||||
threadId: string
|
||||
): Promise<import("@/types").ChatMessage[]> {
|
||||
try {
|
||||
const response = await this.request<{ data: unknown[] }>(
|
||||
`/v1/threads/${threadId}/messages`
|
||||
);
|
||||
|
||||
// Convert API messages to ChatMessage format, handling missing fields
|
||||
return response.data.map((msg: unknown, index: number) => {
|
||||
const msgObj = msg as Record<string, unknown>;
|
||||
const role = msgObj.role as string;
|
||||
return {
|
||||
id: (msgObj.message_id as string) || `restored-${index}`,
|
||||
role:
|
||||
role === "user" ||
|
||||
role === "assistant" ||
|
||||
role === "system" ||
|
||||
role === "tool"
|
||||
? role
|
||||
: "user",
|
||||
contents:
|
||||
(msgObj.contents as import("@/types/agent-framework").Contents[]) ||
|
||||
[],
|
||||
timestamp: (msgObj.timestamp as string) || new Date().toISOString(),
|
||||
author_name: msgObj.author_name as string | undefined,
|
||||
message_id: msgObj.message_id as string | undefined,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to get thread messages:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI-compatible streaming methods using /v1/responses endpoint
|
||||
|
||||
// Stream agent execution using pure OpenAI format
|
||||
async *streamAgentExecutionOpenAI(
|
||||
agentId: string,
|
||||
request: RunAgentRequest
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
model: "agent-framework",
|
||||
input: request.input, // Direct OpenAI ResponseInputParam
|
||||
stream: true,
|
||||
extra_body: {
|
||||
entity_id: agentId,
|
||||
thread_id: request.thread_id,
|
||||
},
|
||||
};
|
||||
|
||||
return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest);
|
||||
}
|
||||
|
||||
// Stream agent execution using direct OpenAI format
|
||||
async *streamAgentExecutionOpenAIDirect(
|
||||
_agentId: string,
|
||||
openAIRequest: AgentFrameworkRequest
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/v1/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI streaming request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error("Response body is not readable");
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE events
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const dataStr = line.slice(6);
|
||||
|
||||
// Handle [DONE] signal
|
||||
if (dataStr === "[DONE]") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const openAIEvent: ExtendedResponseStreamEvent =
|
||||
JSON.parse(dataStr);
|
||||
yield openAIEvent; // Direct pass-through - no conversion!
|
||||
} catch (e) {
|
||||
console.error("Failed to parse OpenAI SSE event:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Stream workflow execution using OpenAI format - direct event pass-through
|
||||
async *streamWorkflowExecutionOpenAI(
|
||||
workflowId: string,
|
||||
request: RunWorkflowRequest
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
// Convert to OpenAI format
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
model: "agent-framework", // Placeholder model name
|
||||
input: "", // Empty string for workflows - actual data is in extra_body.input_data
|
||||
stream: true,
|
||||
extra_body: {
|
||||
entity_id: workflowId,
|
||||
input_data: request.input_data, // Preserve structured data
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/v1/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI streaming request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error("Response body is not readable");
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE events
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const dataStr = line.slice(6);
|
||||
|
||||
// Handle [DONE] signal
|
||||
if (dataStr === "[DONE]") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const openAIEvent: ExtendedResponseStreamEvent =
|
||||
JSON.parse(dataStr);
|
||||
yield openAIEvent; // Direct pass-through - no conversion!
|
||||
} catch (e) {
|
||||
console.error("Failed to parse OpenAI SSE event:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVED: Legacy streaming methods - use streamAgentExecutionOpenAI and streamWorkflowExecutionOpenAI instead
|
||||
|
||||
// Non-streaming execution (for testing)
|
||||
async runAgent(
|
||||
agentId: string,
|
||||
request: RunAgentRequest
|
||||
): Promise<{
|
||||
thread_id: string;
|
||||
result: unknown[];
|
||||
message_count: number;
|
||||
}> {
|
||||
return this.request(`/agents/${agentId}/run`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
async runWorkflow(
|
||||
workflowId: string,
|
||||
request: RunWorkflowRequest
|
||||
): Promise<{
|
||||
result: string;
|
||||
events: number;
|
||||
message_count: number;
|
||||
}> {
|
||||
return this.request(`/workflows/${workflowId}/run`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiClient = new ApiClient();
|
||||
export { ApiClient };
|
||||
Reference in New Issue
Block a user