Skip to content

Commit 25ba001

Browse files
2 parents 260313f + be4aaa7 commit 25ba001

File tree

1 file changed

+169
-65
lines changed

1 file changed

+169
-65
lines changed

src/frontend/src/services/PlanDataService.tsx

Lines changed: 169 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ export class PlanDataService {
362362
* Parse an agent message object or repr string:
363363
* Input forms supported:
364364
* - { type: 'agent_message', data: "AgentMessage(agent_name='X', timestamp=..., content='...')"}
365+
* - { type: 'agent_message', data: { agent_name: 'X', timestamp: 12345, content: '...' } }
365366
* - "AgentMessage(agent_name='X', timestamp=..., content='...')"
366367
* Returns a structured object with steps parsed from markdown-ish content.
367368
*/
@@ -380,11 +381,62 @@ export class PlanDataService {
380381
raw_data: any;
381382
} | null {
382383
try {
383-
// Unwrap wrapper
384-
if (rawData && typeof rawData === 'object' && rawData.type === WebsocketMessageType.AGENT_MESSAGE && typeof rawData.data === 'string') {
385-
return this.parseAgentMessage(rawData.data);
384+
// Handle JSON string input - parse it first
385+
if (typeof rawData === 'string' && rawData.startsWith('{')) {
386+
try {
387+
rawData = JSON.parse(rawData);
388+
} catch (e) {
389+
console.error('Failed to parse JSON string:', e);
390+
// Fall through to handle as regular string
391+
}
392+
}
393+
394+
// Unwrap wrapper - handle object format
395+
if (rawData && typeof rawData === 'object' && rawData.type === WebsocketMessageType.AGENT_MESSAGE) {
396+
if (typeof rawData.data === 'object' && rawData.data.agent_name) {
397+
// New format: { type: 'agent_message', data: { agent_name: '...', timestamp: 123, content: '...' } }
398+
const data = rawData.data;
399+
const content = data.content || '';
400+
const timestamp = typeof data.timestamp === 'number' ? data.timestamp : null;
401+
402+
// Parse the content for steps and next_steps (reuse existing logic)
403+
const { steps, next_steps } = this.parseContentForStepsAndNextSteps(content);
404+
405+
return {
406+
agent: data.agent_name || 'UnknownAgent',
407+
agent_type: AgentMessageType.AI_AGENT,
408+
timestamp,
409+
steps,
410+
next_steps,
411+
content,
412+
raw_data: rawData
413+
};
414+
} else if (typeof rawData.data === 'string') {
415+
// Old format: { type: 'agent_message', data: "AgentMessage(...)" }
416+
return this.parseAgentMessage(rawData.data);
417+
}
386418
}
387419

420+
// Handle direct object format
421+
if (rawData && typeof rawData === 'object' && rawData.agent_name) {
422+
const content = rawData.content || '';
423+
const timestamp = typeof rawData.timestamp === 'number' ? rawData.timestamp : null;
424+
425+
// Parse the content for steps and next_steps
426+
const { steps, next_steps } = this.parseContentForStepsAndNextSteps(content);
427+
428+
return {
429+
agent: rawData.agent_name || 'UnknownAgent',
430+
agent_type: AgentMessageType.AI_AGENT,
431+
timestamp,
432+
steps,
433+
next_steps,
434+
content,
435+
raw_data: rawData
436+
};
437+
}
438+
439+
// Handle old string format: "AgentMessage(...)"
388440
if (typeof rawData !== 'string') return null;
389441
if (!rawData.startsWith('AgentMessage(')) return null;
390442

@@ -409,70 +461,15 @@ export class PlanDataService {
409461
.replace(/\\"/g, '"')
410462
.replace(/\\\\/g, '\\');
411463

412-
// Parse sections of the form "##### Title Completed"
413-
// Each block ends at --- line or next "##### " or end.
414-
const lines = content.split('\n');
415-
const steps: Array<{ title: string; fields: Record<string, string>; summary?: string; raw_block: string; }> = [];
416-
let i = 0;
417-
while (i < lines.length) {
418-
const headingMatch = lines[i].match(/^#####\s+(.+?)\s+Completed\s*$/i);
419-
if (headingMatch) {
420-
const title = headingMatch[1].trim();
421-
const blockLines: string[] = [];
422-
i++;
423-
while (i < lines.length && !/^---\s*$/.test(lines[i]) && !/^#####\s+/.test(lines[i])) {
424-
blockLines.push(lines[i]);
425-
i++;
426-
}
427-
// Skip separator line if present
428-
if (i < lines.length && /^---\s*$/.test(lines[i])) i++;
429-
430-
const fields: Record<string, string> = {};
431-
let summary: string | undefined;
432-
for (const bl of blockLines) {
433-
const fieldMatch = bl.match(/^\*\*(.+?)\*\*:\s*(.*)$/);
434-
if (fieldMatch) {
435-
const fieldName = fieldMatch[1].trim().replace(/:$/, '');
436-
const value = fieldMatch[2].trim().replace(/\\s+$/, '');
437-
if (fieldName) fields[fieldName] = value;
438-
} else {
439-
const summaryMatch = bl.match(/^AGENT SUMMARY:\s*(.+)$/i);
440-
if (summaryMatch) {
441-
summary = summaryMatch[1].trim();
442-
}
443-
}
444-
}
445-
446-
steps.push({
447-
title,
448-
fields,
449-
summary,
450-
raw_block: blockLines.join('\n').trim()
451-
});
452-
} else {
453-
i++;
454-
}
455-
}
456-
457-
// Next Steps section
458-
const nextSteps: string[] = [];
459-
const nextIdx = lines.findIndex(l => /^Next Steps:/.test(l.trim()));
460-
if (nextIdx !== -1) {
461-
for (let j = nextIdx + 1; j < lines.length; j++) {
462-
const l = lines[j].trim();
463-
if (!l) continue;
464-
if (/^[-*]\s+/.test(l)) {
465-
nextSteps.push(l.replace(/^[-*]\s+/, '').trim());
466-
}
467-
}
468-
}
464+
// Parse the content for steps and next_steps
465+
const { steps, next_steps } = this.parseContentForStepsAndNextSteps(content);
469466

470467
return {
471468
agent,
472469
agent_type: AgentMessageType.AI_AGENT,
473470
timestamp,
474471
steps,
475-
next_steps: nextSteps,
472+
next_steps,
476473
content,
477474
raw_data: rawData
478475
};
@@ -481,12 +478,86 @@ export class PlanDataService {
481478
return null;
482479
}
483480
}
484-
// ...inside export class PlanDataService { (place near other parsers)
481+
482+
/**
483+
* Helper method to parse content for steps and next_steps
484+
* Extracted to avoid code duplication
485+
*/
486+
private static parseContentForStepsAndNextSteps(content: string): {
487+
steps: Array<{
488+
title: string;
489+
fields: Record<string, string>;
490+
summary?: string;
491+
raw_block: string;
492+
}>;
493+
next_steps: string[];
494+
} {
495+
// Parse sections of the form "##### Title Completed"
496+
// Each block ends at --- line or next "##### " or end.
497+
const lines = content.split('\n');
498+
const steps: Array<{ title: string; fields: Record<string, string>; summary?: string; raw_block: string; }> = [];
499+
let i = 0;
500+
while (i < lines.length) {
501+
const headingMatch = lines[i].match(/^#####\s+(.+?)\s+Completed\s*$/i);
502+
if (headingMatch) {
503+
const title = headingMatch[1].trim();
504+
const blockLines: string[] = [];
505+
i++;
506+
while (i < lines.length && !/^---\s*$/.test(lines[i]) && !/^#####\s+/.test(lines[i])) {
507+
blockLines.push(lines[i]);
508+
i++;
509+
}
510+
// Skip separator line if present
511+
if (i < lines.length && /^---\s*$/.test(lines[i])) i++;
512+
513+
const fields: Record<string, string> = {};
514+
let summary: string | undefined;
515+
for (const bl of blockLines) {
516+
const fieldMatch = bl.match(/^\*\*(.+?)\*\*:\s*(.*)$/);
517+
if (fieldMatch) {
518+
const fieldName = fieldMatch[1].trim().replace(/:$/, '');
519+
const value = fieldMatch[2].trim().replace(/\\s+$/, '');
520+
if (fieldName) fields[fieldName] = value;
521+
} else {
522+
const summaryMatch = bl.match(/^AGENT SUMMARY:\s*(.+)$/i);
523+
if (summaryMatch) {
524+
summary = summaryMatch[1].trim();
525+
}
526+
}
527+
}
528+
529+
steps.push({
530+
title,
531+
fields,
532+
summary,
533+
raw_block: blockLines.join('\n').trim()
534+
});
535+
} else {
536+
i++;
537+
}
538+
}
539+
540+
// Next Steps section
541+
const next_steps: string[] = [];
542+
const nextIdx = lines.findIndex(l => /^Next Steps:/.test(l.trim()));
543+
if (nextIdx !== -1) {
544+
for (let j = nextIdx + 1; j < lines.length; j++) {
545+
const l = lines[j].trim();
546+
if (!l) continue;
547+
if (/^[-*]\s+/.test(l)) {
548+
next_steps.push(l.replace(/^[-*]\s+/, '').trim());
549+
}
550+
}
551+
}
552+
553+
return { steps, next_steps };
554+
}
485555

486556
/**
487557
* Parse streaming agent message fragments.
488558
* Supports:
489559
* - { type: 'agent_message_streaming', data: "AgentMessageStreaming(agent_name='X', content='partial', is_final=False)" }
560+
* - { type: 'agent_message_streaming', data: { agent_name: 'X', content: 'partial', is_final: true } }
490561
* - "AgentMessageStreaming(agent_name='X', content='partial', is_final=False)"
491562
*/
492563
static parseAgentMessageStreaming(rawData: any): {
@@ -496,11 +567,44 @@ export class PlanDataService {
496567
raw_data: any;
497568
} | null {
498569
try {
499-
// Unwrap wrapper
500-
if (rawData && typeof rawData === 'object' && rawData.type === 'agent_message_streaming' && typeof rawData.data === 'string') {
501-
return this.parseAgentMessageStreaming(rawData.data);
570+
// Handle JSON string input - parse it first
571+
if (typeof rawData === 'string' && rawData.startsWith('{')) {
572+
try {
573+
rawData = JSON.parse(rawData);
574+
} catch (e) {
575+
console.error('Failed to parse JSON string:', e);
576+
// Fall through to handle as regular string
577+
}
578+
}
579+
580+
// Unwrap wrapper - handle object format
581+
if (rawData && typeof rawData === 'object' && rawData.type === 'agent_message_streaming') {
582+
if (typeof rawData.data === 'object' && rawData.data.agent_name) {
583+
// New format: { type: 'agent_message_streaming', data: { agent_name: '...', content: '...', is_final: true } }
584+
const data = rawData.data;
585+
return {
586+
agent: data.agent_name || 'UnknownAgent',
587+
content: data.content || '',
588+
is_final: Boolean(data.is_final),
589+
raw_data: rawData
590+
};
591+
} else if (typeof rawData.data === 'string') {
592+
// Old format: { type: 'agent_message_streaming', data: "AgentMessageStreaming(...)" }
593+
return this.parseAgentMessageStreaming(rawData.data);
594+
}
595+
}
596+
597+
// Handle direct object format
598+
if (rawData && typeof rawData === 'object' && rawData.agent_name) {
599+
return {
600+
agent: rawData.agent_name || 'UnknownAgent',
601+
content: rawData.content || '',
602+
is_final: Boolean(rawData.is_final),
603+
raw_data: rawData
604+
};
502605
}
503606

607+
// Handle old string format: "AgentMessageStreaming(...)"
504608
if (typeof rawData !== 'string') return null;
505609
if (!rawData.startsWith('AgentMessageStreaming(')) return null;
506610

0 commit comments

Comments
 (0)