Skip to content

Commit 8e00916

Browse files
authored
Merge pull request #367 from microsoft/create-plan-page
feat - Create plan page
2 parents b5cc787 + 2895f63 commit 8e00916

File tree

14 files changed

+982
-15
lines changed

14 files changed

+982
-15
lines changed

src/backend/app_kernel.py

Lines changed: 197 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
import os
6+
import re
67
import uuid
78
from typing import Dict, List, Optional
89

@@ -27,6 +28,7 @@
2728
from models.messages_kernel import (
2829
AgentMessage,
2930
AgentType,
31+
GeneratePlanRequest,
3032
HumanClarification,
3133
HumanFeedback,
3234
InputTask,
@@ -188,14 +190,22 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
188190
track_event_if_configured(
189191
"RAI failed",
190192
{
191-
"status": "Plan not created",
193+
"status": "Plan not created - RAI validation failed",
192194
"description": input_task.description,
193195
"session_id": input_task.session_id,
194196
},
195197
)
196198

197199
return {
198-
"status": "Plan not created",
200+
"status": "RAI_VALIDATION_FAILED",
201+
"message": "Content Safety Check Failed",
202+
"detail": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.",
203+
"suggestions": [
204+
"Remove any potentially harmful, inappropriate, or unsafe content",
205+
"Use more professional and constructive language",
206+
"Focus on legitimate business or educational objectives",
207+
"Ensure your request complies with content policies",
208+
],
199209
}
200210
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
201211
user_id = authenticated_user["user_principal_id"]
@@ -345,7 +355,7 @@ async def create_plan_endpoint(input_task: InputTask, request: Request):
345355
description: Error message
346356
"""
347357
# Perform RAI check on the description
348-
if not await rai_success(input_task.description):
358+
if not await rai_success(input_task.description, False):
349359
track_event_if_configured(
350360
"RAI failed",
351361
{
@@ -356,7 +366,18 @@ async def create_plan_endpoint(input_task: InputTask, request: Request):
356366
)
357367
raise HTTPException(
358368
status_code=400,
359-
detail="Task description failed safety validation. Please revise your request.",
369+
detail={
370+
"error_type": "RAI_VALIDATION_FAILED",
371+
"message": "Content Safety Check Failed",
372+
"description": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.",
373+
"suggestions": [
374+
"Remove any potentially harmful, inappropriate, or unsafe content",
375+
"Use more professional and constructive language",
376+
"Focus on legitimate business or educational objectives",
377+
"Ensure your request complies with content policies",
378+
],
379+
"user_action": "Please revise your request and try again",
380+
},
360381
)
361382

362383
# Get authenticated user
@@ -420,6 +441,162 @@ async def create_plan_endpoint(input_task: InputTask, request: Request):
420441
raise HTTPException(status_code=400, detail=f"Error creating plan: {e}")
421442

422443

444+
@app.post("/api/generate_plan")
445+
async def generate_plan_endpoint(
446+
generate_plan_request: GeneratePlanRequest, request: Request
447+
):
448+
"""
449+
Generate plan steps for an existing plan using the planner agent.
450+
451+
---
452+
tags:
453+
- Plans
454+
parameters:
455+
- name: user_principal_id
456+
in: header
457+
type: string
458+
required: true
459+
description: User ID extracted from the authentication header
460+
- name: body
461+
in: body
462+
required: true
463+
schema:
464+
type: object
465+
properties:
466+
plan_id:
467+
type: string
468+
description: The ID of the existing plan to generate steps for
469+
responses:
470+
200:
471+
description: Plan generation completed successfully
472+
schema:
473+
type: object
474+
properties:
475+
status:
476+
type: string
477+
description: Success message
478+
plan_id:
479+
type: string
480+
description: The ID of the plan that was generated
481+
steps_created:
482+
type: integer
483+
description: Number of steps created
484+
400:
485+
description: Invalid request or processing error
486+
schema:
487+
type: object
488+
properties:
489+
detail:
490+
type: string
491+
description: Error message
492+
404:
493+
description: Plan not found
494+
schema:
495+
type: object
496+
properties:
497+
detail:
498+
type: string
499+
description: Error message
500+
"""
501+
# Get authenticated user
502+
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
503+
user_id = authenticated_user["user_principal_id"]
504+
505+
if not user_id:
506+
track_event_if_configured(
507+
"UserIdNotFound", {"status_code": 400, "detail": "no user"}
508+
)
509+
raise HTTPException(status_code=400, detail="no user")
510+
511+
try:
512+
# Initialize memory store
513+
kernel, memory_store = await initialize_runtime_and_context("", user_id)
514+
515+
# Get the existing plan
516+
plan = await memory_store.get_plan_by_plan_id(
517+
plan_id=generate_plan_request.plan_id
518+
)
519+
if not plan:
520+
track_event_if_configured(
521+
"GeneratePlanNotFound",
522+
{
523+
"status_code": 404,
524+
"detail": "Plan not found",
525+
"plan_id": generate_plan_request.plan_id,
526+
},
527+
)
528+
raise HTTPException(status_code=404, detail="Plan not found")
529+
530+
# Create the agents for this session
531+
client = None
532+
try:
533+
client = config.get_ai_project_client()
534+
except Exception as client_exc:
535+
logging.error(f"Error creating AIProjectClient: {client_exc}")
536+
537+
agents = await AgentFactory.create_all_agents(
538+
session_id=plan.session_id,
539+
user_id=user_id,
540+
memory_store=memory_store,
541+
client=client,
542+
)
543+
544+
# Get the group chat manager to process the plan
545+
group_chat_manager = agents[AgentType.GROUP_CHAT_MANAGER.value]
546+
547+
# Create an InputTask from the plan's initial goal
548+
input_task = InputTask(
549+
session_id=plan.session_id, description=plan.initial_goal
550+
)
551+
552+
# Use the group chat manager to generate the plan steps
553+
await group_chat_manager.handle_input_task(input_task)
554+
555+
# Get the updated plan with steps
556+
updated_plan = await memory_store.get_plan_by_plan_id(
557+
plan_id=generate_plan_request.plan_id
558+
)
559+
steps = await memory_store.get_steps_by_plan(
560+
plan_id=generate_plan_request.plan_id
561+
)
562+
563+
# Log successful plan generation
564+
track_event_if_configured(
565+
"PlanGenerated",
566+
{
567+
"status": f"Plan generation completed for plan ID: {generate_plan_request.plan_id}",
568+
"plan_id": generate_plan_request.plan_id,
569+
"session_id": plan.session_id,
570+
"steps_created": len(steps),
571+
},
572+
)
573+
574+
if client:
575+
try:
576+
client.close()
577+
except Exception as e:
578+
logging.error(f"Error closing AIProjectClient: {e}")
579+
580+
return {
581+
"status": "Plan generation completed successfully",
582+
"plan_id": generate_plan_request.plan_id,
583+
"steps_created": len(steps),
584+
}
585+
586+
except HTTPException:
587+
# Re-raise HTTP exceptions
588+
raise
589+
except Exception as e:
590+
track_event_if_configured(
591+
"GeneratePlanError",
592+
{
593+
"plan_id": generate_plan_request.plan_id,
594+
"error": str(e),
595+
},
596+
)
597+
raise HTTPException(status_code=400, detail=f"Error generating plan: {e}")
598+
599+
423600
@app.post("/api/human_feedback")
424601
async def human_feedback_endpoint(human_feedback: HumanFeedback, request: Request):
425602
"""
@@ -588,12 +765,26 @@ async def human_clarification_endpoint(
588765
track_event_if_configured(
589766
"RAI failed",
590767
{
591-
"status": "Clarification is not received",
768+
"status": "Clarification rejected - RAI validation failed",
592769
"description": human_clarification.human_clarification,
593770
"session_id": human_clarification.session_id,
594771
},
595772
)
596-
raise HTTPException(status_code=400, detail="Invalida Clarification")
773+
raise HTTPException(
774+
status_code=400,
775+
detail={
776+
"error_type": "RAI_VALIDATION_FAILED",
777+
"message": "Clarification Safety Check Failed",
778+
"description": "Your clarification contains content that doesn't meet our safety guidelines. Please provide a more appropriate clarification.",
779+
"suggestions": [
780+
"Use clear and professional language",
781+
"Avoid potentially harmful or inappropriate content",
782+
"Focus on providing constructive feedback or clarification",
783+
"Ensure your message complies with content policies",
784+
],
785+
"user_action": "Please revise your clarification and try again",
786+
},
787+
)
597788

598789
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
599790
user_id = authenticated_user["user_principal_id"]

src/backend/models/messages_kernel.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ class UserLanguage(KernelBaseModel):
310310
language: str
311311

312312

313+
class GeneratePlanRequest(KernelBaseModel):
314+
"""Message representing a request to generate a plan from an existing plan ID."""
315+
316+
plan_id: str
317+
318+
313319
class ApprovalRequest(KernelBaseModel):
314320
"""Message sent to HumanAgent to request approval for a step."""
315321

src/frontend/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React from 'react';
22
import './App.css';
33
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
4-
import { HomePage, PlanPage } from './pages';
4+
import { HomePage, PlanPage, PlanCreatePage } from './pages';
55

66
function App() {
77
return (
88
<Router>
99
<Routes>
1010
<Route path="/" element={<HomePage />} />
11+
<Route path="/plan/:planId/create" element={<PlanCreatePage />} />
1112
<Route path="/plan/:planId" element={<PlanPage />} />
1213
<Route path="*" element={<Navigate to="/" replace />} />
1314
</Routes>

src/frontend/src/components/content/HomeInput.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import "./../../styles/HomeInput.css";
1414
import { HomeInputProps, quickTasks, QuickTask } from "../../models/homeInput";
1515
import { TaskService } from "../../services/TaskService";
1616
import { NewTaskService } from "../../services/NewTaskService";
17+
import { RAIErrorCard, RAIErrorData } from "../errors";
1718

1819
import ChatInput from "@/coral/modules/ChatInput";
1920
import InlineToaster, { useInlineToaster } from "../toast/InlineToaster";
@@ -26,6 +27,7 @@ const HomeInput: React.FC<HomeInputProps> = ({
2627
}) => {
2728
const [submitting, setSubmitting] = useState(false);
2829
const [input, setInput] = useState("");
30+
const [raiError, setRAIError] = useState<RAIErrorData | null>(null);
2931

3032
const textareaRef = useRef<HTMLTextAreaElement>(null);
3133
const navigate = useNavigate();
@@ -40,6 +42,7 @@ const HomeInput: React.FC<HomeInputProps> = ({
4042

4143
const resetTextarea = () => {
4244
setInput("");
45+
setRAIError(null); // Clear any RAI errors
4346
if (textareaRef.current) {
4447
textareaRef.current.style.height = "auto";
4548
textareaRef.current.focus();
@@ -54,6 +57,7 @@ const HomeInput: React.FC<HomeInputProps> = ({
5457
const handleSubmit = async () => {
5558
if (input.trim()) {
5659
setSubmitting(true);
60+
setRAIError(null); // Clear any previous RAI errors
5761
let id = showToast("Creating a plan", "progress");
5862

5963
try {
@@ -67,17 +71,40 @@ const HomeInput: React.FC<HomeInputProps> = ({
6771
if (response.plan_id && response.plan_id !== null) {
6872
showToast("Plan created!", "success");
6973
dismissToast(id);
70-
navigate(`/plan/${response.plan_id}`);
74+
navigate(`/plan/${response.plan_id}/create`);
7175
} else {
7276
showToast("Failed to create plan", "error");
7377
dismissToast(id);
7478
}
7579
} catch (error: any) {
7680
dismissToast(id);
77-
// Show more specific error message if available
78-
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
79-
showToast(errorMessage, "error");
80-
showToast(JSON.parse(error?.message)?.detail, "error");
81+
82+
// Check if this is an RAI validation error
83+
let errorDetail = null;
84+
try {
85+
// Try to parse the error detail if it's a string
86+
if (typeof error?.response?.data?.detail === 'string') {
87+
errorDetail = JSON.parse(error.response.data.detail);
88+
} else {
89+
errorDetail = error?.response?.data?.detail;
90+
}
91+
} catch (parseError) {
92+
// If parsing fails, use the original error
93+
errorDetail = error?.response?.data?.detail;
94+
}
95+
96+
// Handle RAI validation errors with better UX
97+
if (errorDetail?.error_type === 'RAI_VALIDATION_FAILED') {
98+
setRAIError(errorDetail);
99+
} else {
100+
// Handle other errors with toast messages
101+
const errorMessage = errorDetail?.description ||
102+
errorDetail?.message ||
103+
error?.response?.data?.message ||
104+
error?.message ||
105+
"Something went wrong";
106+
showToast(errorMessage, "error");
107+
}
81108

82109
} finally {
83110
setInput("");
@@ -88,6 +115,7 @@ const HomeInput: React.FC<HomeInputProps> = ({
88115

89116
const handleQuickTaskClick = (task: QuickTask) => {
90117
setInput(task.description);
118+
setRAIError(null); // Clear any RAI errors when selecting a quick task
91119
if (textareaRef.current) {
92120
textareaRef.current.focus();
93121
}
@@ -109,6 +137,20 @@ const HomeInput: React.FC<HomeInputProps> = ({
109137
<Title2>How can I help?</Title2>
110138
</div>
111139

140+
{/* Show RAI error if present */}
141+
{raiError && (
142+
<RAIErrorCard
143+
error={raiError}
144+
onRetry={() => {
145+
setRAIError(null);
146+
if (textareaRef.current) {
147+
textareaRef.current.focus();
148+
}
149+
}}
150+
onDismiss={() => setRAIError(null)}
151+
/>
152+
)}
153+
112154
<ChatInput
113155
ref={textareaRef} // forwarding
114156
value={input}

0 commit comments

Comments
 (0)