Skip to content

Commit c386b21

Browse files
committed
Add RAI validation and user clarification parsing
Introduces RAI content safety validation for user clarifications in the backend API. Adds plan_id and m_plan_id fields to UserClarificationResponse. Implements frontend parsing for deeply nested user clarification requests and updates WebSocketService to emit parsed clarification messages. Fixes a state variable naming issue in PlanPage.
1 parent 1a07518 commit c386b21

File tree

6 files changed

+114
-10
lines changed

6 files changed

+114
-10
lines changed

src/backend/v3/api/router.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,36 @@ async def user_clarification(
424424
)
425425
# Set the approval in the orchestration config
426426
if user_id and human_feedback.request_id:
427+
### validate rai
428+
if human_feedback.answer != None or human_feedback.answer !="":
429+
if not await rai_success(human_feedback.answer, False):
430+
track_event_if_configured(
431+
"RAI failed",
432+
{
433+
"status": "Plan Clarification ",
434+
"description": human_feedback.answer,
435+
"request_id": human_feedback.request_id,
436+
},
437+
)
438+
raise HTTPException(
439+
status_code=400,
440+
detail={
441+
"error_type": "RAI_VALIDATION_FAILED",
442+
"message": "Content Safety Check Failed",
443+
"description": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.",
444+
"suggestions": [
445+
"Remove any potentially harmful, inappropriate, or unsafe content",
446+
"Use more professional and constructive language",
447+
"Focus on legitimate business or educational objectives",
448+
"Ensure your request complies with content policies",
449+
],
450+
"user_action": "Please revise your request and try again",
451+
},
452+
)
453+
454+
455+
456+
427457
if (
428458
orchestration_config
429459
and human_feedback.request_id in orchestration_config.clarifications

src/backend/v3/models/messages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class UserClarificationResponse:
101101
"""Response for user clarification from the frontend."""
102102
request_id: str
103103
answer: str = ""
104+
plan_id: str = ""
105+
m_plan_id: str = ""
104106

105107
@dataclass(slots=True)
106108
class FinalResultMessage:

src/frontend/src/models/messages.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,10 @@ export interface ParsedPlanApprovalRequest {
168168
plan_id: string;
169169
parsedData: MPlanData;
170170
rawData: string;
171+
}
172+
173+
export interface ParsedUserClarification {
174+
type: WebsocketMessageType.USER_CLARIFICATION_REQUEST;
175+
question: string;
176+
request_id: string;
171177
}

src/frontend/src/pages/PlanPage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const PlanPage: React.FC = () => {
4242
const [planData, setPlanData] = useState<ProcessedPlanData | any>(null);
4343
const [allPlans, setAllPlans] = useState<ProcessedPlanData[]>([]);
4444
const [loading, setLoading] = useState<boolean>(true);
45-
const [submittingChatDisableInput, setSubmitting] = useState<boolean>(false);
45+
const [submittingChatDisableInput, setSubmittingChatDisableInput] = useState<boolean>(false);
4646
const [error, setError] = useState<Error | null>(null);
4747

4848
const [planApprovalRequest, setPlanApprovalRequest] = useState<MPlanData | null>(null);
@@ -325,9 +325,9 @@ const PlanPage: React.FC = () => {
325325
return;
326326
}
327327
setInput("");
328-
setRAIError(null); // Clear any previous RAI errors
328+
329329
if (!planData?.plan) return;
330-
setSubmitting(true);
330+
setSubmittingChatDisableInput(true);
331331
let id = showToast("Submitting clarification", "progress");
332332

333333
try {
@@ -373,7 +373,7 @@ const PlanPage: React.FC = () => {
373373
],
374374
user_action: 'Please modify your input and try again.'
375375
};
376-
setRAIError(raiErrorData);
376+
377377
} else {
378378
// Handle other types of errors
379379
showToast(
@@ -385,7 +385,7 @@ const PlanPage: React.FC = () => {
385385
);
386386
}
387387
} finally {
388-
setSubmitting(false);
388+
setSubmittingChatDisableInput(false);
389389
}
390390
},
391391
[planData?.plan, showToast, dismissToast, loadPlanData]

src/frontend/src/services/PlanDataService.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
PlanMessage,
77
MPlanData,
88
StepStatus,
9-
WebsocketMessageType
9+
WebsocketMessageType,
10+
ParsedUserClarification
1011
} from "@/models";
1112
import { apiService } from "@/api";
1213

@@ -521,5 +522,71 @@ export class PlanDataService {
521522
return null;
522523
}
523524
}
525+
// ...inside export class PlanDataService { (place near other parsers, e.g. after parseAgentMessageStreaming)
526+
527+
/**
528+
* Parse a user clarification request message (possibly deeply nested).
529+
* Accepts objects like:
530+
* {
531+
* type: 'user_clarification_request',
532+
* data: { type: 'user_clarification_request', data: { type: 'user_clarification_request', data: "UserClarificationRequest(...)" } }
533+
* }
534+
* Returns ParsedUserClarification or null if not parsable.
535+
*/
536+
static parseUserClarificationRequest(rawData: any): ParsedUserClarification | null {
537+
try {
538+
// Depth-first search to locate the innermost string starting with UserClarificationRequest(
539+
const extractString = (val: any, depth = 0): string | null => {
540+
if (depth > 10) return null; // safety
541+
if (typeof val === 'string') {
542+
if (val.startsWith('UserClarificationRequest(')) return val;
543+
return null;
544+
}
545+
if (val && typeof val === 'object') {
546+
// Common pattern: { type: 'user_clarification_request', data: ... }
547+
if (val.data !== undefined) {
548+
const inner = extractString(val.data, depth + 1);
549+
if (inner) return inner;
550+
}
551+
// Also scan other enumerable props just in case
552+
for (const k of Object.keys(val)) {
553+
if (k === 'data') continue;
554+
const inner = extractString(val[k], depth + 1);
555+
if (inner) return inner;
556+
}
557+
}
558+
return null;
559+
};
560+
561+
const source = extractString(rawData);
562+
if (!source) return null;
563+
564+
// Extract question and request_id
565+
// question='...'
566+
const questionMatch = source.match(/question='((?:\\'|[^'])*)'/);
567+
const requestIdMatch = source.match(/request_id='([a-fA-F0-9-]+)'/);
568+
569+
if (!questionMatch || !requestIdMatch) return null;
570+
571+
let question = questionMatch[1]
572+
.replace(/\\n/g, '\n')
573+
.replace(/\\'/g, "'")
574+
.replace(/\\"/g, '"')
575+
.replace(/\\\\/g, '\\')
576+
.trim();
577+
578+
const request_id = requestIdMatch[1];
579+
580+
return {
581+
type: WebsocketMessageType.USER_CLARIFICATION_REQUEST,
582+
question,
583+
request_id
584+
};
585+
} catch (e) {
586+
console.error('parseUserClarificationRequest failed:', e);
587+
return null;
588+
}
589+
}
590+
524591

525592
}

src/frontend/src/services/WebSocketService.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,9 @@ class WebSocketService {
225225

226226
case WebsocketMessageType.USER_CLARIFICATION_REQUEST: {
227227
if (message.data) {
228-
//\const transformed = PlanDataService.parseUserClarificationRequest(message);
229-
console.log('WebSocket USER_CLARIFICATION_REQUEST message received:', message);
230-
231-
this.emit(WebsocketMessageType.USER_CLARIFICATION_REQUEST, message);
228+
const transformed = PlanDataService.parseUserClarificationRequest(message);
229+
console.log('WebSocket USER_CLARIFICATION_REQUEST message received:', transformed);
230+
this.emit(WebsocketMessageType.USER_CLARIFICATION_REQUEST, transformed);
232231
}
233232
break;
234233
}

0 commit comments

Comments
 (0)