diff --git a/src/backend/v3/api/router.py b/src/backend/v3/api/router.py index 9fd66e2e2..674ec602c 100644 --- a/src/backend/v3/api/router.py +++ b/src/backend/v3/api/router.py @@ -424,6 +424,36 @@ async def user_clarification( ) # Set the approval in the orchestration config if user_id and human_feedback.request_id: + ### validate rai + if human_feedback.answer != None or human_feedback.answer !="": + if not await rai_success(human_feedback.answer, False): + track_event_if_configured( + "RAI failed", + { + "status": "Plan Clarification ", + "description": human_feedback.answer, + "request_id": human_feedback.request_id, + }, + ) + raise HTTPException( + status_code=400, + detail={ + "error_type": "RAI_VALIDATION_FAILED", + "message": "Content Safety Check Failed", + "description": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.", + "suggestions": [ + "Remove any potentially harmful, inappropriate, or unsafe content", + "Use more professional and constructive language", + "Focus on legitimate business or educational objectives", + "Ensure your request complies with content policies", + ], + "user_action": "Please revise your request and try again", + }, + ) + + + + if ( orchestration_config and human_feedback.request_id in orchestration_config.clarifications diff --git a/src/backend/v3/models/messages.py b/src/backend/v3/models/messages.py index e09a78c89..489b21dda 100644 --- a/src/backend/v3/models/messages.py +++ b/src/backend/v3/models/messages.py @@ -101,6 +101,8 @@ class UserClarificationResponse: """Response for user clarification from the frontend.""" request_id: str answer: str = "" + plan_id: str = "" + m_plan_id: str = "" @dataclass(slots=True) class FinalResultMessage: diff --git a/src/frontend/src/api/apiService.tsx b/src/frontend/src/api/apiService.tsx index 75fb2e80b..91eeb124e 100644 --- a/src/frontend/src/api/apiService.tsx +++ b/src/frontend/src/api/apiService.tsx @@ -290,14 +290,16 @@ export class APIService { * @returns Promise with response object */ async submitClarification( - planId: string, - sessionId: string, - clarification: string + request_id: string = "", + answer: string = "", + plan_id: string = "", + m_plan_id: string = "" ): Promise<{ status: string; session_id: string }> { const clarificationData: HumanClarification = { - plan_id: planId, - session_id: sessionId, - human_clarification: clarification + request_id, + answer, + plan_id, + m_plan_id }; const response = await apiClient.post( @@ -306,7 +308,7 @@ export class APIService { ); // Invalidate cached data - this._cache.invalidate(new RegExp(`^(plan|steps)_${planId}`)); + this._cache.invalidate(new RegExp(`^(plan|steps)_${plan_id}`)); this._cache.invalidate(new RegExp(`^plans_`)); return response; diff --git a/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx b/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx index 7839f54ff..1080cb1d3 100644 --- a/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx @@ -3,11 +3,8 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypePrism from "rehype-prism"; -interface StreamingAgentMessageProps { - agentMessages: AgentMessageData[]; -} -const StreamingAgentMessage = ({ agentMessages }: StreamingAgentMessageProps) => { +const StreamingAgentMessage = (agentMessages: AgentMessageData[]) => { if (!agentMessages?.length) return null; // Filter out messages with empty content diff --git a/src/frontend/src/models/agentMessage.tsx b/src/frontend/src/models/agentMessage.tsx index 7f5aba96d..9c5e57501 100644 --- a/src/frontend/src/models/agentMessage.tsx +++ b/src/frontend/src/models/agentMessage.tsx @@ -1,4 +1,6 @@ +import { Agent } from 'http'; import { BaseModel } from './plan'; +import { AgentMessageType, AgentType } from './enums'; /** * Represents a message from an agent @@ -20,9 +22,10 @@ export interface AgentMessage extends BaseModel { export interface AgentMessageData { agent: string; + agent_type: AgentMessageType; timestamp: number; steps: any[]; // intentionally always empty next_steps: []; // intentionally always empty raw_content: string; raw_data: string; -} \ No newline at end of file +} diff --git a/src/frontend/src/models/enums.tsx b/src/frontend/src/models/enums.tsx index 4157b4902..415a403b1 100644 --- a/src/frontend/src/models/enums.tsx +++ b/src/frontend/src/models/enums.tsx @@ -250,4 +250,9 @@ export enum WebsocketMessageType { USER_CLARIFICATION_REQUEST = "user_clarification_request", USER_CLARIFICATION_RESPONSE = "user_clarification_response", FINAL_RESULT_MESSAGE = "final_result_message" +} + +export enum AgentMessageType { + HUMAN_AGENT = "Human_Agent", + AI_AGENT = "AI_Agent", } \ No newline at end of file diff --git a/src/frontend/src/models/messages.tsx b/src/frontend/src/models/messages.tsx index 633e38a1e..3c0a2c7e1 100644 --- a/src/frontend/src/models/messages.tsx +++ b/src/frontend/src/models/messages.tsx @@ -65,12 +65,10 @@ export interface HumanFeedback { * Message containing human clarification on a plan */ export interface HumanClarification { - /** Plan identifier */ + request_id: string; + answer: string; plan_id: string; - /** Session identifier */ - session_id: string; - /** Clarification from human */ - human_clarification: string; + m_plan_id: string; } /** @@ -168,4 +166,10 @@ export interface ParsedPlanApprovalRequest { plan_id: string; parsedData: MPlanData; rawData: string; +} + +export interface ParsedUserClarification { + type: WebsocketMessageType.USER_CLARIFICATION_REQUEST; + question: string; + request_id: string; } \ No newline at end of file diff --git a/src/frontend/src/pages/PlanPage.tsx b/src/frontend/src/pages/PlanPage.tsx index 5578627d0..96e15bca3 100644 --- a/src/frontend/src/pages/PlanPage.tsx +++ b/src/frontend/src/pages/PlanPage.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState, useMemo } from "react" import { useParams, useNavigate } from "react-router-dom"; import { Spinner, Text } from "@fluentui/react-components"; import { PlanDataService } from "../services/PlanDataService"; -import { ProcessedPlanData, PlanWithSteps, WebsocketMessageType, MPlanData, AgentMessageData } from "../models"; +import { ProcessedPlanData, PlanWithSteps, WebsocketMessageType, MPlanData, AgentMessageData, AgentMessageType, ParsedUserClarification } from "../models"; import PlanChat from "../components/content/PlanChat"; import PlanPanelRight from "../components/content/PlanPanelRight"; import PlanPanelLeft from "../components/content/PlanPanelLeft"; @@ -42,8 +42,9 @@ const PlanPage: React.FC = () => { const [planData, setPlanData] = useState(null); const [allPlans, setAllPlans] = useState([]); const [loading, setLoading] = useState(true); - const [submittingChatDisableInput, setSubmitting] = useState(false); + const [submittingChatDisableInput, setSubmittingChatDisableInput] = useState(true); const [error, setError] = useState(null); + const [clarificationMessage, setClarificationMessage] = useState(null); const [planApprovalRequest, setPlanApprovalRequest] = useState(null); const [reloadLeftList, setReloadLeftList] = useState(true); @@ -52,8 +53,7 @@ const PlanPage: React.FC = () => { const [wsConnected, setWsConnected] = useState(false); const [streamingMessages, setStreamingMessages] = useState([]); const [streamingMessageBuffer, setStreamingMessageBuffer] = useState(""); - // RAI Error state - const [raiError, setRAIError] = useState(null); + const [agentMessages, setAgentMessages] = useState([]); // Team config state @@ -137,6 +137,19 @@ const PlanPage: React.FC = () => { useEffect(() => { const unsubscribe = webSocketService.on(WebsocketMessageType.USER_CLARIFICATION_REQUEST, (clarificationMessage: any) => { console.log('📋 Clarification Message', clarificationMessage); + const agentMessageData = { + agent: 'ProxyAgent', + agent_type: AgentMessageType.AI_AGENT, + timestamp: clarificationMessage.timestamp || Date.now(), + steps: [], // intentionally always empty + next_steps: [], // intentionally always empty + raw_content: clarificationMessage.data.question || '', + raw_data: clarificationMessage.data || '', + } as AgentMessageData; + console.log('✅ Parsed clarification message:', agentMessageData); + setClarificationMessage(clarificationMessage.data as ParsedUserClarification | null); + setAgentMessages(prev => [...prev, agentMessageData]); + setSubmittingChatDisableInput(false); scrollToBottom(); }); @@ -154,6 +167,31 @@ const PlanPage: React.FC = () => { return () => unsubscribe(); }, [scrollToBottom]); + + //WebsocketMessageType.FINAL_RESULT_MESSAGE + useEffect(() => { + const unsubscribe = webSocketService.on(WebsocketMessageType.FINAL_RESULT_MESSAGE, (finalMessage: any) => { + console.log('📋 Final Result Message', finalMessage); + const agentMessageData = { + agent: 'ProxyAgent', + agent_type: AgentMessageType.AI_AGENT, + timestamp: Date.now(), + steps: [], // intentionally always empty + next_steps: [], // intentionally always empty + raw_content: finalMessage.content || '', + raw_data: finalMessage || '', + } as AgentMessageData; + console.log('✅ Parsed final result message:', agentMessageData); + setAgentMessages(prev => [...prev, agentMessageData]); + setSubmittingChatDisableInput(true); + scrollToBottom(); + + }); + + return () => unsubscribe(); + }, [scrollToBottom]); + + //WebsocketMessageType.AGENT_MESSAGE useEffect(() => { const unsubscribe = webSocketService.on(WebsocketMessageType.AGENT_MESSAGE, (agentMessage: any) => { @@ -325,24 +363,39 @@ const PlanPage: React.FC = () => { return; } setInput(""); - setRAIError(null); // Clear any previous RAI errors + if (!planData?.plan) return; - setSubmitting(true); + setSubmittingChatDisableInput(true); let id = showToast("Submitting clarification", "progress"); try { // Use legacy method for non-v3 backends - await PlanDataService.submitClarification( - planData.plan.id, - planData.plan.session_id, - chatInput - ); - + const response = await PlanDataService.submitClarification({ + request_id: clarificationMessage?.request_id || "", + answer: chatInput, + plan_id: planData?.plan.id, + m_plan_id: planApprovalRequest?.id || "" + }); + console.log("Clarification submitted successfully:", response); setInput(""); dismissToast(id); showToast("Clarification submitted successfully", "success"); - await loadPlanData(false); + setClarificationMessage(null); + const agentMessageData = { + agent: 'You', + agent_type: AgentMessageType.HUMAN_AGENT, + timestamp: Date.now(), + steps: [], // intentionally always empty + next_steps: [], // intentionally always empty + raw_content: chatInput || '', + raw_data: chatInput || '', + } as AgentMessageData; + + setAgentMessages(prev => [...prev, agentMessageData]); + setSubmittingChatDisableInput(true); + scrollToBottom(); + } catch (error: any) { dismissToast(id); @@ -373,7 +426,7 @@ const PlanPage: React.FC = () => { ], user_action: 'Please modify your input and try again.' }; - setRAIError(raiErrorData); + } else { // Handle other types of errors showToast( @@ -385,7 +438,7 @@ const PlanPage: React.FC = () => { ); } } finally { - setSubmitting(false); + setSubmittingChatDisableInput(false); } }, [planData?.plan, showToast, dismissToast, loadPlanData] diff --git a/src/frontend/src/services/PlanDataService.tsx b/src/frontend/src/services/PlanDataService.tsx index 373101290..d70e2acf9 100644 --- a/src/frontend/src/services/PlanDataService.tsx +++ b/src/frontend/src/services/PlanDataService.tsx @@ -6,7 +6,9 @@ import { PlanMessage, MPlanData, StepStatus, - WebsocketMessageType + WebsocketMessageType, + ParsedUserClarification, + AgentMessageType } from "@/models"; import { apiService } from "@/api"; @@ -46,7 +48,7 @@ export class PlanDataService { messages: PlanMessage[] ): ProcessedPlanData { // Extract unique agents from steps - console.log("Processing plan data for plan ID:", plan); + const uniqueAgents = new Set(); if (plan.steps && plan.steps.length > 0) { plan.steps.forEach((step) => { @@ -139,13 +141,19 @@ export class PlanDataService { * @param clarification Clarification text * @returns Promise with API response */ - static async submitClarification( - planId: string, - sessionId: string, - clarification: string - ) { + static async submitClarification({ + request_id, + answer, + plan_id, + m_plan_id + }: { + request_id: string; + answer: string; + plan_id: string; + m_plan_id: string; + }) { try { - return apiService.submitClarification(planId, sessionId, clarification); + return apiService.submitClarification(request_id, answer, plan_id, m_plan_id); } catch (error) { console.log("Failed to submit clarification:", error); throw error; @@ -359,6 +367,7 @@ export class PlanDataService { */ static parseAgentMessage(rawData: any): { agent: string; + agent_type: AgentMessageType; timestamp: number | null; steps: Array<{ title: string; @@ -460,6 +469,7 @@ export class PlanDataService { return { agent, + agent_type: AgentMessageType.AI_AGENT, timestamp, steps, next_steps: nextSteps, @@ -521,5 +531,147 @@ export class PlanDataService { return null; } } + // ...inside export class PlanDataService { (place near other parsers, e.g. after parseAgentMessageStreaming) + + /** + * Parse a user clarification request message (possibly deeply nested). + * Accepts objects like: + * { + * type: 'user_clarification_request', + * data: { type: 'user_clarification_request', data: { type: 'user_clarification_request', data: "UserClarificationRequest(...)" } } + * } + * Returns ParsedUserClarification or null if not parsable. + */ + // ...existing code... + /** + * Parse a user clarification request message (possibly deeply nested). + * Enhanced to support: + * - question in single OR double quotes + * - request_id in single OR double quotes + * - escaped newline / quote sequences + */ + static parseUserClarificationRequest(rawData: any): ParsedUserClarification | null { + try { + const extractString = (val: any, depth = 0): string | null => { + if (depth > 15) return null; + if (typeof val === 'string') { + return val.startsWith('UserClarificationRequest(') ? val : null; + } + if (val && typeof val === 'object') { + // Prefer .data traversal + if (val.data !== undefined) { + const inner = extractString(val.data, depth + 1); + if (inner) return inner; + } + for (const k of Object.keys(val)) { + if (k === 'data') continue; + const inner = extractString(val[k], depth + 1); + if (inner) return inner; + } + } + return null; + }; + + const source = extractString(rawData); + if (!source) return null; + + // question=( "...") OR ('...') + const questionRegex = /question=(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)')/; + const qMatch = source.match(questionRegex); + if (!qMatch) return null; + + let question = (qMatch[1] ?? qMatch[2] ?? '') + .replace(/\\n/g, '\n') + .replace(/\\'/g, "'") + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\') + .trim(); + + // request_id='uuid' or "uuid" + const requestIdRegex = /request_id=(?:"([a-fA-F0-9-]+)"|'([a-fA-F0-9-]+)')/; + const rMatch = source.match(requestIdRegex); + if (!rMatch) return null; + const request_id = rMatch[1] ?? rMatch[2]; + + return { + type: WebsocketMessageType.USER_CLARIFICATION_REQUEST, + question, + request_id + }; + } catch (e) { + console.error('parseUserClarificationRequest failed:', e); + return null; + } + } + // ...inside export class PlanDataService (place near other parsers) ... + + /** + * Parse a final result message (possibly nested). + * Accepts structures like: + * { + * type: 'final_result_message', + * data: { type: 'final_result_message', data: { content: '...', status: 'completed', timestamp: 12345.6 } } + * } + * Returns null if not parsable. + */ + static parseFinalResultMessage(rawData: any): { + type: WebsocketMessageType; + content: string; + status: string; + timestamp: number | null; + raw_data: any; + } | null { + try { + const extractPayload = (val: any, depth = 0): any => { + if (depth > 10) return null; + if (!val || typeof val !== 'object') return null; + // If it has content & status, assume it's the payload + if (('content' in val) && ('status' in val)) return val; + if ('data' in val) { + const inner = extractPayload(val.data, depth + 1); + if (inner) return inner; + } + // Scan other keys as fallback + for (const k of Object.keys(val)) { + if (k === 'data') continue; + const inner = extractPayload(val[k], depth + 1); + if (inner) return inner; + } + return null; + }; + + const payload = extractPayload(rawData); + if (!payload) return null; + + let content = typeof payload.content === 'string' ? payload.content : ''; + content = content + .replace(/\\n/g, '\n') + .replace(/\\'/g, "'") + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\') + .trim(); + + const statusRaw = (payload.status || 'completed').toString().trim(); + const status = statusRaw.toLowerCase(); + + let timestamp: number | null = null; + if (payload.timestamp != null) { + const num = Number(payload.timestamp); + if (!Number.isNaN(num)) timestamp = num; + } + + return { + type: WebsocketMessageType.FINAL_RESULT_MESSAGE, + content, + status, + timestamp, + raw_data: rawData + }; + } catch (e) { + console.error('parseFinalResultMessage failed:', e); + return null; + } + } + } \ No newline at end of file diff --git a/src/frontend/src/services/WebSocketService.tsx b/src/frontend/src/services/WebSocketService.tsx index 73e017f89..1cc3d062c 100644 --- a/src/frontend/src/services/WebSocketService.tsx +++ b/src/frontend/src/services/WebSocketService.tsx @@ -225,10 +225,9 @@ class WebSocketService { case WebsocketMessageType.USER_CLARIFICATION_REQUEST: { if (message.data) { - //\const transformed = PlanDataService.parseUserClarificationRequest(message); - console.log('WebSocket USER_CLARIFICATION_REQUEST message received:', message); - - this.emit(WebsocketMessageType.USER_CLARIFICATION_REQUEST, message); + const transformed = PlanDataService.parseUserClarificationRequest(message); + console.log('WebSocket USER_CLARIFICATION_REQUEST message received:', transformed); + this.emit(WebsocketMessageType.USER_CLARIFICATION_REQUEST, transformed); } break; } diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index 9cad6162c..3af6a7acd 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -13,23 +13,13 @@ export default defineConfig({ }, }, + + // Server configuration server: { port: 3001, open: true, - host: true, - proxy: { - '/api': { - target: 'http://localhost:8000', // Backend API endpoints - changeOrigin: true, - secure: false, - }, - '/config': { - target: 'http://localhost:3000', // Frontend server for config - changeOrigin: true, - secure: false, - } - } + host: true }, // Build configuration