diff --git a/src/frontend/src/services/PlanDataService.tsx b/src/frontend/src/services/PlanDataService.tsx index 1449cfe9e..dff5c2ba9 100644 --- a/src/frontend/src/services/PlanDataService.tsx +++ b/src/frontend/src/services/PlanDataService.tsx @@ -362,6 +362,7 @@ export class PlanDataService { * Parse an agent message object or repr string: * Input forms supported: * - { type: 'agent_message', data: "AgentMessage(agent_name='X', timestamp=..., content='...')"} + * - { type: 'agent_message', data: { agent_name: 'X', timestamp: 12345, content: '...' } } * - "AgentMessage(agent_name='X', timestamp=..., content='...')" * Returns a structured object with steps parsed from markdown-ish content. */ @@ -380,11 +381,62 @@ export class PlanDataService { raw_data: any; } | null { try { - // Unwrap wrapper - if (rawData && typeof rawData === 'object' && rawData.type === WebsocketMessageType.AGENT_MESSAGE && typeof rawData.data === 'string') { - return this.parseAgentMessage(rawData.data); + // Handle JSON string input - parse it first + if (typeof rawData === 'string' && rawData.startsWith('{')) { + try { + rawData = JSON.parse(rawData); + } catch (e) { + console.error('Failed to parse JSON string:', e); + // Fall through to handle as regular string + } + } + + // Unwrap wrapper - handle object format + if (rawData && typeof rawData === 'object' && rawData.type === WebsocketMessageType.AGENT_MESSAGE) { + if (typeof rawData.data === 'object' && rawData.data.agent_name) { + // New format: { type: 'agent_message', data: { agent_name: '...', timestamp: 123, content: '...' } } + const data = rawData.data; + const content = data.content || ''; + const timestamp = typeof data.timestamp === 'number' ? data.timestamp : null; + + // Parse the content for steps and next_steps (reuse existing logic) + const { steps, next_steps } = this.parseContentForStepsAndNextSteps(content); + + return { + agent: data.agent_name || 'UnknownAgent', + agent_type: AgentMessageType.AI_AGENT, + timestamp, + steps, + next_steps, + content, + raw_data: rawData + }; + } else if (typeof rawData.data === 'string') { + // Old format: { type: 'agent_message', data: "AgentMessage(...)" } + return this.parseAgentMessage(rawData.data); + } } + // Handle direct object format + if (rawData && typeof rawData === 'object' && rawData.agent_name) { + const content = rawData.content || ''; + const timestamp = typeof rawData.timestamp === 'number' ? rawData.timestamp : null; + + // Parse the content for steps and next_steps + const { steps, next_steps } = this.parseContentForStepsAndNextSteps(content); + + return { + agent: rawData.agent_name || 'UnknownAgent', + agent_type: AgentMessageType.AI_AGENT, + timestamp, + steps, + next_steps, + content, + raw_data: rawData + }; + } + + // Handle old string format: "AgentMessage(...)" if (typeof rawData !== 'string') return null; if (!rawData.startsWith('AgentMessage(')) return null; @@ -409,70 +461,15 @@ export class PlanDataService { .replace(/\\"/g, '"') .replace(/\\\\/g, '\\'); - // Parse sections of the form "##### Title Completed" - // Each block ends at --- line or next "##### " or end. - const lines = content.split('\n'); - const steps: Array<{ title: string; fields: Record; summary?: string; raw_block: string; }> = []; - let i = 0; - while (i < lines.length) { - const headingMatch = lines[i].match(/^#####\s+(.+?)\s+Completed\s*$/i); - if (headingMatch) { - const title = headingMatch[1].trim(); - const blockLines: string[] = []; - i++; - while (i < lines.length && !/^---\s*$/.test(lines[i]) && !/^#####\s+/.test(lines[i])) { - blockLines.push(lines[i]); - i++; - } - // Skip separator line if present - if (i < lines.length && /^---\s*$/.test(lines[i])) i++; - - const fields: Record = {}; - let summary: string | undefined; - for (const bl of blockLines) { - const fieldMatch = bl.match(/^\*\*(.+?)\*\*:\s*(.*)$/); - if (fieldMatch) { - const fieldName = fieldMatch[1].trim().replace(/:$/, ''); - const value = fieldMatch[2].trim().replace(/\\s+$/, ''); - if (fieldName) fields[fieldName] = value; - } else { - const summaryMatch = bl.match(/^AGENT SUMMARY:\s*(.+)$/i); - if (summaryMatch) { - summary = summaryMatch[1].trim(); - } - } - } - - steps.push({ - title, - fields, - summary, - raw_block: blockLines.join('\n').trim() - }); - } else { - i++; - } - } - - // Next Steps section - const nextSteps: string[] = []; - const nextIdx = lines.findIndex(l => /^Next Steps:/.test(l.trim())); - if (nextIdx !== -1) { - for (let j = nextIdx + 1; j < lines.length; j++) { - const l = lines[j].trim(); - if (!l) continue; - if (/^[-*]\s+/.test(l)) { - nextSteps.push(l.replace(/^[-*]\s+/, '').trim()); - } - } - } + // Parse the content for steps and next_steps + const { steps, next_steps } = this.parseContentForStepsAndNextSteps(content); return { agent, agent_type: AgentMessageType.AI_AGENT, timestamp, steps, - next_steps: nextSteps, + next_steps, content, raw_data: rawData }; @@ -481,12 +478,86 @@ export class PlanDataService { return null; } } - // ...inside export class PlanDataService { (place near other parsers) + + /** + * Helper method to parse content for steps and next_steps + * Extracted to avoid code duplication + */ + private static parseContentForStepsAndNextSteps(content: string): { + steps: Array<{ + title: string; + fields: Record; + summary?: string; + raw_block: string; + }>; + next_steps: string[]; + } { + // Parse sections of the form "##### Title Completed" + // Each block ends at --- line or next "##### " or end. + const lines = content.split('\n'); + const steps: Array<{ title: string; fields: Record; summary?: string; raw_block: string; }> = []; + let i = 0; + while (i < lines.length) { + const headingMatch = lines[i].match(/^#####\s+(.+?)\s+Completed\s*$/i); + if (headingMatch) { + const title = headingMatch[1].trim(); + const blockLines: string[] = []; + i++; + while (i < lines.length && !/^---\s*$/.test(lines[i]) && !/^#####\s+/.test(lines[i])) { + blockLines.push(lines[i]); + i++; + } + // Skip separator line if present + if (i < lines.length && /^---\s*$/.test(lines[i])) i++; + + const fields: Record = {}; + let summary: string | undefined; + for (const bl of blockLines) { + const fieldMatch = bl.match(/^\*\*(.+?)\*\*:\s*(.*)$/); + if (fieldMatch) { + const fieldName = fieldMatch[1].trim().replace(/:$/, ''); + const value = fieldMatch[2].trim().replace(/\\s+$/, ''); + if (fieldName) fields[fieldName] = value; + } else { + const summaryMatch = bl.match(/^AGENT SUMMARY:\s*(.+)$/i); + if (summaryMatch) { + summary = summaryMatch[1].trim(); + } + } + } + + steps.push({ + title, + fields, + summary, + raw_block: blockLines.join('\n').trim() + }); + } else { + i++; + } + } + + // Next Steps section + const next_steps: string[] = []; + const nextIdx = lines.findIndex(l => /^Next Steps:/.test(l.trim())); + if (nextIdx !== -1) { + for (let j = nextIdx + 1; j < lines.length; j++) { + const l = lines[j].trim(); + if (!l) continue; + if (/^[-*]\s+/.test(l)) { + next_steps.push(l.replace(/^[-*]\s+/, '').trim()); + } + } + } + + return { steps, next_steps }; + } /** * Parse streaming agent message fragments. * Supports: * - { type: 'agent_message_streaming', data: "AgentMessageStreaming(agent_name='X', content='partial', is_final=False)" } + * - { type: 'agent_message_streaming', data: { agent_name: 'X', content: 'partial', is_final: true } } * - "AgentMessageStreaming(agent_name='X', content='partial', is_final=False)" */ static parseAgentMessageStreaming(rawData: any): { @@ -496,11 +567,44 @@ export class PlanDataService { raw_data: any; } | null { try { - // Unwrap wrapper - if (rawData && typeof rawData === 'object' && rawData.type === 'agent_message_streaming' && typeof rawData.data === 'string') { - return this.parseAgentMessageStreaming(rawData.data); + // Handle JSON string input - parse it first + if (typeof rawData === 'string' && rawData.startsWith('{')) { + try { + rawData = JSON.parse(rawData); + } catch (e) { + console.error('Failed to parse JSON string:', e); + // Fall through to handle as regular string + } + } + + // Unwrap wrapper - handle object format + if (rawData && typeof rawData === 'object' && rawData.type === 'agent_message_streaming') { + if (typeof rawData.data === 'object' && rawData.data.agent_name) { + // New format: { type: 'agent_message_streaming', data: { agent_name: '...', content: '...', is_final: true } } + const data = rawData.data; + return { + agent: data.agent_name || 'UnknownAgent', + content: data.content || '', + is_final: Boolean(data.is_final), + raw_data: rawData + }; + } else if (typeof rawData.data === 'string') { + // Old format: { type: 'agent_message_streaming', data: "AgentMessageStreaming(...)" } + return this.parseAgentMessageStreaming(rawData.data); + } + } + + // Handle direct object format + if (rawData && typeof rawData === 'object' && rawData.agent_name) { + return { + agent: rawData.agent_name || 'UnknownAgent', + content: rawData.content || '', + is_final: Boolean(rawData.is_final), + raw_data: rawData + }; } + // Handle old string format: "AgentMessageStreaming(...)" if (typeof rawData !== 'string') return null; if (!rawData.startsWith('AgentMessageStreaming(')) return null;