Skip to content

Commit fe6fa57

Browse files
committed
Refactor plan approval request parsing logic
Simplifies and restructures PlanDataService.parsePlanApprovalRequest to handle multiple input formats more robustly and consistently. Removes redundant code, improves normalization, and enhances step extraction. Adds a debug log for PLAN_APPROVAL_REQUEST in WebSocketService and adjusts agent message handling to always transform message data.
1 parent 7621380 commit fe6fa57

File tree

2 files changed

+171
-177
lines changed

2 files changed

+171
-177
lines changed

src/frontend/src/services/PlanDataService.tsx

Lines changed: 169 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -154,208 +154,201 @@ export class PlanDataService {
154154

155155
static parsePlanApprovalRequest(rawData: any): MPlanData | null {
156156
try {
157-
console.log('🔍 Parsing plan approval request:', rawData, 'Type:', typeof rawData);
158-
159-
// Already parsed object passthrough
160-
if (rawData && typeof rawData === 'object' && rawData.type === WebsocketMessageType.PLAN_APPROVAL_REQUEST) {
161-
return rawData.parsedData || null;
162-
}
163-
164-
// Wrapper form: { type: 'plan_approval_request', data: 'PlanApprovalRequest(plan=MPlan(...), ...)' }
165-
if (
166-
rawData &&
167-
typeof rawData === 'object' &&
168-
rawData.type === 'plan_approval_request' &&
169-
typeof rawData.data === 'string'
170-
) {
171-
// Recurse using the contained string
172-
return this.parsePlanApprovalRequest(rawData.data);
173-
}
174-
175-
// Structured v3 style: { plan: { id, steps, user_request, ... }, context?: {...} }
176-
if (rawData && typeof rawData === 'object' && rawData.plan && typeof rawData.plan === 'object') {
177-
const mplan = rawData.plan;
178-
179-
// Extract user_request text
180-
let userRequestText = 'Plan approval required';
181-
if (mplan.user_request) {
182-
if (typeof mplan.user_request === 'string') {
183-
userRequestText = mplan.user_request;
184-
} else if (Array.isArray(mplan.user_request.items)) {
185-
const textContent = mplan.user_request.items.find((item: any) => item.text);
186-
if (textContent?.text) {
187-
userRequestText = textContent.text.replace(/\u200b/g, '').trim();
188-
}
189-
} else if (mplan.user_request.content) {
190-
userRequestText = mplan.user_request.content;
191-
}
192-
}
193-
194-
const steps = (mplan.steps || [])
195-
.map((step: any, index: number) => {
157+
if (!rawData) return null;
158+
159+
// Normalize to the PlanApprovalRequest(...) string that contains MPlan(...)
160+
let source: string | null = null;
161+
162+
if (typeof rawData === 'object') {
163+
if (typeof rawData.data === 'string' && /PlanApprovalRequest\(plan=MPlan\(/.test(rawData.data)) {
164+
source = rawData.data;
165+
} else if (rawData.plan && typeof rawData.plan === 'object') {
166+
// Already structured style
167+
const mplan = rawData.plan;
168+
const userRequestText =
169+
typeof mplan.user_request === 'string'
170+
? mplan.user_request
171+
: (Array.isArray(mplan.user_request?.items)
172+
? (mplan.user_request.items.find((i: any) => i.text)?.text || '')
173+
: (mplan.user_request?.content || '')
174+
).replace?.(/\u200b/g, '').trim() || 'Plan approval required';
175+
176+
const steps = (mplan.steps || []).map((step: any, i: number) => {
196177
const action = step.action || '';
197178
const cleanAction = action
198179
.replace(/\*\*/g, '')
199180
.replace(/^Certainly!\s*/i, '')
200181
.replace(/^Given the team composition and the available facts,?\s*/i, '')
201-
.replace(/^here is a (?:concise )?plan to address the original request[^.]*\.\s*/i, '')
182+
.replace(/^here is a (?:concise )?plan[^.]*\.\s*/i, '')
202183
.replace(/^(?:here is|this is) a (?:concise )?(?:plan|approach|strategy)[^.]*[.:]\s*/i, '')
203184
.replace(/^\*\*([^*]+)\*\*:?\s*/g, '$1: ')
204185
.replace(/^[-]\s*/, '')
205186
.replace(/\s+/g, ' ')
206187
.trim();
207-
208188
return {
209-
id: index + 1,
189+
id: i + 1,
210190
action,
211191
cleanAction,
212192
agent: step.agent || step._agent || 'System'
213193
};
214-
})
215-
.filter((s: any) =>
216-
s.cleanAction.length > 3 &&
217-
!/^(?:involvement|certainly|given|here is)/i.test(s.cleanAction)
218-
);
219-
220-
return {
221-
id: mplan.id || mplan.plan_id || 'unknown',
222-
status: (mplan.overall_status || rawData.status || 'PENDING_APPROVAL'),
223-
user_request: userRequestText,
224-
team: Array.isArray(mplan.team) ? mplan.team : [],
225-
facts: mplan.facts || '',
226-
steps,
227-
context: {
228-
task: userRequestText,
229-
participant_descriptions: rawData.context?.participant_descriptions || {}
230-
},
231-
user_id: mplan.user_id,
232-
team_id: mplan.team_id,
233-
plan_id: mplan.plan_id,
234-
overall_status: mplan.overall_status,
235-
raw_data: rawData
236-
};
194+
}).filter((s: any) => s.cleanAction.length > 3 && !/^(?:involvement|certainly|given|here is)/i.test(s.cleanAction));
195+
196+
197+
const result: MPlanData = {
198+
id: mplan.id || mplan.plan_id || 'unknown',
199+
status: (mplan.overall_status || rawData.status || 'PENDING_APPROVAL').toString().toUpperCase(),
200+
user_request: userRequestText,
201+
team: Array.isArray(mplan.team) ? mplan.team : [],
202+
facts: mplan.facts || '',
203+
steps,
204+
context: {
205+
task: userRequestText,
206+
participant_descriptions: rawData.context?.participant_descriptions || {}
207+
},
208+
user_id: mplan.user_id,
209+
team_id: mplan.team_id,
210+
plan_id: mplan.plan_id,
211+
overall_status: mplan.overall_status,
212+
raw_data: rawData
213+
};
214+
return result;
215+
}
216+
} else if (typeof rawData === 'string') {
217+
if (/PlanApprovalRequest\(plan=MPlan\(/.test(rawData)) {
218+
source = rawData;
219+
} else if (/^MPlan\(/.test(rawData)) {
220+
source = `PlanApprovalRequest(plan=${rawData})`;
221+
}
237222
}
238223

239-
// String representation parsing (PlanApprovalRequest(...MPlan(...)) or raw repr)
240-
if (typeof rawData === 'string') {
241-
const source = rawData;
242-
243-
// Extract MPlan(...) block (optional)
244-
// Not strictly needed but could be used for scoping later.
245-
// const mplanBlock = source.match(/MPlan\(([\s\S]*?)\)\)/);
246-
247-
// User request (first text='...')
248-
let user_request =
249-
source.match(/text=['"]([^'"]+?)['"]/)
250-
?.[1]
251-
?.replace(/\\u200b/g, '')
252-
.trim() || 'Plan approval required';
253-
254-
const id = source.match(/MPlan\(id=['"]([^'"]+)['"]/)?.[1] ||
255-
source.match(/id=['"]([^'"]+)['"]/)?.[1] ||
256-
'unknown';
257-
258-
let status =
259-
source.match(/overall_status=<PlanStatus\.([a-zA-Z_]+):/)?.[1] ||
260-
source.match(/overall_status=['"]([^'"]+)['"]/)?.[1] ||
261-
'PENDING_APPROVAL';
262-
if (status) {
263-
status = status.toUpperCase();
264-
}
224+
if (!source) return null;
265225

266-
const teamRaw =
267-
source.match(/team=\[([^\]]*)\]/)?.[1] || '';
268-
const team = teamRaw
269-
.split(',')
270-
.map(s => s.trim().replace(/['"]/g, ''))
271-
.filter(Boolean);
272-
273-
const facts =
274-
source
275-
.match(/facts="([^"]*(?:\\.[^"]*)*)"/)?.[1]
276-
?.replace(/\\n/g, '\n')
277-
.replace(/\\"/g, '"') || '';
278-
279-
// Steps: accept single or double quotes: action='...' or action="..."
280-
const stepRegex = /MStep\(([^)]*?)\)/g;
281-
const steps: any[] = [];
282-
const uniqueActions = new Set<string>();
283-
let match: RegExpExecArray | null;
284-
let stepIndex = 1;
285-
286-
while ((match = stepRegex.exec(source)) !== null) {
287-
const chunk = match[1];
288-
const agent =
289-
chunk.match(/agent=['"]([^'"]+)['"]/)?.[1] || 'System';
290-
const actionRaw =
291-
chunk.match(/action=['"]([^'"]+)['"]/)?.[1] || '';
292-
293-
if (!actionRaw) continue;
294-
295-
let cleanAction = actionRaw
296-
.replace(/\*\*/g, '')
297-
.replace(/^Certainly!\s*/i, '')
298-
.replace(/^Given the team composition and the available facts,?\s*/i, '')
299-
.replace(/^here is a (?:concise )?plan to[^.]*\.\s*/i, '')
300-
.replace(/^\*\*([^*]+)\*\*:?\s*/g, '$1: ')
301-
.replace(/^[-]\s*/, '')
302-
.replace(/\s+/g, ' ')
303-
.trim();
304-
305-
if (
306-
cleanAction.length > 3 &&
307-
!uniqueActions.has(cleanAction.toLowerCase()) &&
308-
!/^(?:here is|this is|given|certainly|involvement)$/i.test(cleanAction)
309-
) {
310-
uniqueActions.add(cleanAction.toLowerCase());
311-
steps.push({
312-
id: stepIndex++,
313-
action: actionRaw,
314-
cleanAction,
315-
agent
316-
});
317-
}
318-
}
226+
// Extract inner MPlan body
227+
const mplanMatch =
228+
source.match(/plan=MPlan\(([\s\S]*?)\),\s*status=/) ||
229+
source.match(/plan=MPlan\(([\s\S]*?)\)\s*\)/);
230+
const body = mplanMatch ? mplanMatch[1] : null;
231+
if (!body) return null;
319232

320-
// participant_descriptions (best-effort)
321-
let participant_descriptions: Record<string, string> = {};
322-
const pdMatch =
323-
source.match(/participant_descriptions['"]?\s*:\s*({[^}]*})/) ||
324-
source.match(/'participant_descriptions':\s*({[^}]*})/);
325-
326-
if (pdMatch?.[1]) {
327-
const transformed = pdMatch[1]
328-
.replace(/'/g, '"')
329-
.replace(/([a-zA-Z0-9_]+)\s*:/g, '"$1":');
330-
try {
331-
participant_descriptions = JSON.parse(transformed);
332-
} catch {
333-
participant_descriptions = {};
334-
}
233+
const pick = (re: RegExp, upper = false): string | undefined => {
234+
const m = body.match(re);
235+
return m ? (upper ? m[1].toUpperCase() : m[1]) : undefined;
236+
};
237+
238+
const id = pick(/id='([^']+)'/) || pick(/id="([^"]+)"/) || 'unknown';
239+
const user_id = pick(/user_id='([^']*)'/) || '';
240+
const team_id = pick(/team_id='([^']*)'/) || '';
241+
const plan_id = pick(/plan_id='([^']*)'/) || '';
242+
let overall_status =
243+
pick(/overall_status=<PlanStatus\.([a-zA-Z_]+):/, true) ||
244+
pick(/overall_status='([^']+)'/, true) ||
245+
'PENDING_APPROVAL';
246+
247+
const outerStatus =
248+
source.match(/status='([^']+)'/)?.[1] ||
249+
source.match(/status="([^"]+)"/)?.[1];
250+
const status = (outerStatus || overall_status || 'PENDING_APPROVAL').toUpperCase();
251+
252+
let user_request =
253+
source.match(/text='([^']+)'/)?.[1] ||
254+
source.match(/text="([^"]+)"/)?.[1] ||
255+
'Plan approval required';
256+
user_request = user_request.replace(/\\u200b/g, '').trim();
257+
258+
const teamRaw = body.match(/team=\[([^\]]*)\]/)?.[1] || '';
259+
const team = teamRaw
260+
.split(',')
261+
.map(s => s.trim().replace(/['"]/g, ''))
262+
.filter(Boolean);
263+
264+
const facts =
265+
body
266+
.match(/facts="([^"]*(?:\\.[^"]*)*)"/)?.[1]
267+
?.replace(/\\n/g, '\n')
268+
.replace(/\\"/g, '"') || '';
269+
270+
const steps: MPlanData['steps'] = [];
271+
const stepRegex = /MStep\(([^)]*?)\)/g;
272+
let stepMatch: RegExpExecArray | null;
273+
let idx = 1;
274+
const seen = new Set<string>();
275+
while ((stepMatch = stepRegex.exec(body)) !== null) {
276+
const chunk = stepMatch[1];
277+
const agent =
278+
chunk.match(/agent='([^']+)'/)?.[1] ||
279+
chunk.match(/agent="([^"]+)"/)?.[1] ||
280+
'System';
281+
const actionRaw =
282+
chunk.match(/action='([^']+)'/)?.[1] ||
283+
chunk.match(/action="([^"]+)"/)?.[1] ||
284+
'';
285+
if (!actionRaw) continue;
286+
287+
const cleanAction = actionRaw
288+
.replace(/\*\*/g, '')
289+
.replace(/^Certainly!\s*/i, '')
290+
.replace(/^Given the team composition and the available facts,?\s*/i, '')
291+
.replace(/^here is a (?:concise )?plan to[^.]*\.\s*/i, '')
292+
.replace(/^\*\*([^*]+)\*\*:?\s*/g, '$1: ')
293+
.replace(/^[-]\s*/, '')
294+
.replace(/\s+/g, ' ')
295+
.trim();
296+
297+
const key = cleanAction.toLowerCase();
298+
if (
299+
cleanAction.length > 3 &&
300+
!seen.has(key) &&
301+
!/^(?:here is|this is|given|certainly|involvement)$/i.test(cleanAction)
302+
) {
303+
seen.add(key);
304+
steps.push({
305+
id: idx++,
306+
action: actionRaw,
307+
cleanAction,
308+
agent
309+
});
335310
}
311+
}
336312

337-
return {
338-
id,
339-
status,
340-
user_request,
341-
team,
342-
facts,
343-
steps,
344-
context: {
345-
task: user_request,
346-
participant_descriptions
347-
},
348-
raw_data: rawData
349-
};
313+
let participant_descriptions: Record<string, string> = {};
314+
const pdMatch =
315+
source.match(/participant_descriptions['"]?\s*:\s*({[^}]*})/) ||
316+
source.match(/'participant_descriptions':\s*({[^}]*})/);
317+
if (pdMatch?.[1]) {
318+
const jsonish = pdMatch[1]
319+
.replace(/'/g, '"')
320+
.replace(/([a-zA-Z0-9_]+)\s*:/g, '"$1":');
321+
try {
322+
participant_descriptions = JSON.parse(jsonish);
323+
} catch {
324+
participant_descriptions = {};
325+
}
350326
}
351327

352-
return null;
353-
} catch (error) {
354-
console.error('Error parsing plan approval request:', error);
328+
const result: MPlanData = {
329+
id,
330+
status,
331+
user_request,
332+
team,
333+
facts,
334+
steps,
335+
context: {
336+
task: user_request,
337+
participant_descriptions
338+
},
339+
user_id,
340+
team_id,
341+
plan_id,
342+
overall_status,
343+
raw_data: rawData
344+
};
345+
346+
return result;
347+
} catch (e) {
348+
console.error('parsePlanApprovalRequest failed:', e);
355349
return null;
356350
}
357351
}
358-
// ...existing code...
359352

360353
/**
361354
* Parse an agent message object or repr string:

src/frontend/src/services/WebSocketService.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class WebSocketService {
171171

172172
switch (message.type) {
173173
case WebsocketMessageType.PLAN_APPROVAL_REQUEST: {
174+
console.log("enter plan approval request");
174175
const parsedData = PlanDataService.parsePlanApprovalRequest(message.data);
175176
if (parsedData) {
176177
const structuredMessage: ParsedPlanApprovalRequest = {
@@ -187,7 +188,7 @@ class WebSocketService {
187188
}
188189

189190
case WebsocketMessageType.AGENT_MESSAGE: {
190-
if (message.data && !message.data.plan_id && firstPlanId) {
191+
if (message.data) {
191192
const transformed: StreamMessage = {
192193
...message,
193194
data: {

0 commit comments

Comments
 (0)