mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: DevUI fixes : Add multimodal input support for workflows and refactor chat input (#2593)
* show app version in devui .NET: Python: Improved Versioning for DevUI Fixes #2059 * feat: Add multimodal input support for workflows and refactor chat input This PR adds support for multimodal content (images, files) in workflow inputs and refactors the chat input into a reusable component. ## Multimodal Workflow Support - Add `isChatMessageSchema()` to detect ChatMessage input schemas - Update `RunWorkflowButton` to use `ChatMessageInput` for ChatMessage workflows - Wrap multimodal content in OpenAI message format for backend processing - Add `_is_openai_multimodal_format()` to detect OpenAI ResponseInputParam - Update `_parse_workflow_input()` to route multimodal input through existing `_convert_input_to_chat_message()` converter ## Reusable ChatMessageInput Component - Extract chat input logic from agent-view into `ChatMessageInput` component - Support file upload, drag & drop, paste handling, and attachments - Add `useDragDrop` hook for parent-level drag handling with full-area drop zones - Refactor agent-view to use the new shared component ## Other Improvements - Add `isStreaming` prop to executor nodes for animation control - Clean up unused imports and state variables in agent-view - Add tests for multimodal workflow input handling Fixes workflow input not receiving images when using AgentExecutor nodes. * add self loop edge, fix #2470 * fix test
This commit is contained in:
committed by
GitHub
Unverified
parent
6835161f2d
commit
411ee7a60f
@@ -21,6 +21,7 @@ import {
|
||||
markStreamingCompleted,
|
||||
clearStreamingState,
|
||||
} from "./streaming-state";
|
||||
import { isAbortError } from "@/hooks";
|
||||
|
||||
// Backend API response type - polymorphic entity that can be agent or workflow
|
||||
// This matches the Python Pydantic EntityInfo model which has all fields optional
|
||||
@@ -60,7 +61,7 @@ interface ConversationApiResponse {
|
||||
id: string;
|
||||
object: "conversation";
|
||||
created_at: number;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const DEFAULT_API_BASE_URL =
|
||||
@@ -411,6 +412,14 @@ class ApiClient {
|
||||
return this.request<{ data: unknown[]; has_more: boolean }>(url);
|
||||
}
|
||||
|
||||
async getConversationItem(
|
||||
conversationId: string,
|
||||
itemId: string
|
||||
): Promise<unknown> {
|
||||
const url = `/v1/conversations/${conversationId}/items/${itemId}`;
|
||||
return this.request<unknown>(url);
|
||||
}
|
||||
|
||||
async deleteConversationItem(
|
||||
conversationId: string,
|
||||
itemId: string
|
||||
@@ -430,6 +439,7 @@ class ApiClient {
|
||||
private async *streamOpenAIResponse(
|
||||
openAIRequest: AgentFrameworkRequest,
|
||||
conversationId?: string,
|
||||
signal?: AbortSignal,
|
||||
resumeResponseId?: string
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
// Check if OpenAI proxy mode is enabled
|
||||
@@ -518,6 +528,7 @@ class ApiClient {
|
||||
response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal,
|
||||
});
|
||||
} else {
|
||||
const url = `${this.baseUrl}/v1/responses`;
|
||||
@@ -540,6 +551,7 @@ class ApiClient {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(openAIRequest),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -591,6 +603,11 @@ class ApiClient {
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
// Check if the request was aborted
|
||||
if (signal?.aborted) {
|
||||
throw new DOMException('Request aborted', 'AbortError');
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
@@ -702,6 +719,14 @@ class ApiClient {
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Don't retry on abort
|
||||
if (isAbortError(error)) {
|
||||
if (conversationId) {
|
||||
markStreamingCompleted(conversationId); // Clean up state
|
||||
}
|
||||
throw error; // Re-throw abort error without retrying
|
||||
}
|
||||
|
||||
// Don't retry on auth errors or client errors
|
||||
if (errorMessage === "UNAUTHORIZED" || errorMessage.startsWith("CLIENT_ERROR:")) {
|
||||
throw error; // Re-throw without retrying
|
||||
@@ -729,6 +754,7 @@ class ApiClient {
|
||||
async *streamAgentExecutionOpenAI(
|
||||
agentId: string,
|
||||
request: RunAgentRequest,
|
||||
signal?: AbortSignal,
|
||||
resumeResponseId?: string
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
@@ -738,7 +764,7 @@ class ApiClient {
|
||||
conversation: request.conversation_id, // OpenAI standard conversation param
|
||||
};
|
||||
|
||||
return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest, request.conversation_id, resumeResponseId);
|
||||
return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest, request.conversation_id, signal, resumeResponseId);
|
||||
}
|
||||
|
||||
// Stream agent execution using direct OpenAI format
|
||||
@@ -746,18 +772,21 @@ class ApiClient {
|
||||
_agentId: string,
|
||||
openAIRequest: AgentFrameworkRequest,
|
||||
conversationId?: string,
|
||||
signal?: AbortSignal,
|
||||
resumeResponseId?: string
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
// Proxy mode handling is now inside streamOpenAIResponse
|
||||
yield* this.streamOpenAIResponse(openAIRequest, conversationId, resumeResponseId);
|
||||
yield* this.streamOpenAIResponse(openAIRequest, conversationId, signal, resumeResponseId);
|
||||
}
|
||||
|
||||
// Stream workflow execution using OpenAI format
|
||||
async *streamWorkflowExecutionOpenAI(
|
||||
workflowId: string,
|
||||
request: RunWorkflowRequest
|
||||
request: RunWorkflowRequest,
|
||||
signal?: AbortSignal
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
// Convert to OpenAI format - use metadata.entity_id for routing
|
||||
// input_data is serialized as JSON string - backend will parse and detect format
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
metadata: { entity_id: workflowId }, // Entity ID in metadata for routing
|
||||
input: JSON.stringify(request.input_data || {}), // Serialize workflow input as JSON string
|
||||
@@ -768,7 +797,7 @@ class ApiClient {
|
||||
: undefined, // Pass checkpoint_id if provided
|
||||
};
|
||||
|
||||
yield* this.streamOpenAIResponse(openAIRequest, request.conversation_id);
|
||||
yield* this.streamOpenAIResponse(openAIRequest, request.conversation_id, signal);
|
||||
}
|
||||
|
||||
// REMOVED: Legacy streaming methods - use streamAgentExecutionOpenAI and streamWorkflowExecutionOpenAI instead
|
||||
@@ -888,15 +917,16 @@ class ApiClient {
|
||||
has_more: boolean;
|
||||
}>(url);
|
||||
|
||||
// Transform conversations to WorkflowSession format (no checkpoint counting)
|
||||
// Transform conversations to WorkflowSession format
|
||||
const sessions = response.data.map((conv) => ({
|
||||
conversation_id: conv.id,
|
||||
entity_id: conv.metadata?.entity_id || entityId,
|
||||
entity_id: (conv.metadata?.entity_id as string) || entityId,
|
||||
created_at: conv.created_at,
|
||||
metadata: {
|
||||
name: conv.metadata?.name || `Session ${new Date(conv.created_at * 1000).toLocaleString()}`,
|
||||
description: conv.metadata?.description,
|
||||
name: (conv.metadata?.name as string) || `Session ${new Date(conv.created_at * 1000).toLocaleString()}`,
|
||||
description: conv.metadata?.description as string | undefined,
|
||||
type: "workflow_session" as const,
|
||||
checkpoint_summary: conv.metadata?.checkpoint_summary as { count: number; latest_iteration: number; has_pending_hil: boolean; pending_hil_count: number } | undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user