Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 197 additions & 6 deletions src/backend/app_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import logging
import os
import re
import uuid
from typing import Dict, List, Optional

Expand All @@ -27,6 +28,7 @@
from models.messages_kernel import (
AgentMessage,
AgentType,
GeneratePlanRequest,
HumanClarification,
HumanFeedback,
InputTask,
Expand Down Expand Up @@ -188,14 +190,22 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
track_event_if_configured(
"RAI failed",
{
"status": "Plan not created",
"status": "Plan not created - RAI validation failed",
"description": input_task.description,
"session_id": input_task.session_id,
},
)

return {
"status": "Plan not created",
"status": "RAI_VALIDATION_FAILED",
"message": "Content Safety Check Failed",
"detail": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.",
"suggestions": [
"Remove any potentially harmful, inappropriate, or unsafe content",
"Use more professional and constructive language",
"Focus on legitimate business or educational objectives",
"Ensure your request complies with content policies",
],
}
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
user_id = authenticated_user["user_principal_id"]
Expand Down Expand Up @@ -345,7 +355,7 @@ async def create_plan_endpoint(input_task: InputTask, request: Request):
description: Error message
"""
# Perform RAI check on the description
if not await rai_success(input_task.description):
if not await rai_success(input_task.description, False):
track_event_if_configured(
"RAI failed",
{
Expand All @@ -356,7 +366,18 @@ async def create_plan_endpoint(input_task: InputTask, request: Request):
)
raise HTTPException(
status_code=400,
detail="Task description failed safety validation. Please revise your request.",
detail={
"error_type": "RAI_VALIDATION_FAILED",
"message": "Content Safety Check Failed",
"description": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.",
"suggestions": [
"Remove any potentially harmful, inappropriate, or unsafe content",
"Use more professional and constructive language",
"Focus on legitimate business or educational objectives",
"Ensure your request complies with content policies",
],
"user_action": "Please revise your request and try again",
},
)

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


@app.post("/api/generate_plan")
async def generate_plan_endpoint(
generate_plan_request: GeneratePlanRequest, request: Request
):
"""
Generate plan steps for an existing plan using the planner agent.

---
tags:
- Plans
parameters:
- name: user_principal_id
in: header
type: string
required: true
description: User ID extracted from the authentication header
- name: body
in: body
required: true
schema:
type: object
properties:
plan_id:
type: string
description: The ID of the existing plan to generate steps for
responses:
200:
description: Plan generation completed successfully
schema:
type: object
properties:
status:
type: string
description: Success message
plan_id:
type: string
description: The ID of the plan that was generated
steps_created:
type: integer
description: Number of steps created
400:
description: Invalid request or processing error
schema:
type: object
properties:
detail:
type: string
description: Error message
404:
description: Plan not found
schema:
type: object
properties:
detail:
type: string
description: Error message
"""
# Get authenticated user
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
user_id = authenticated_user["user_principal_id"]

if not user_id:
track_event_if_configured(
"UserIdNotFound", {"status_code": 400, "detail": "no user"}
)
raise HTTPException(status_code=400, detail="no user")

try:
# Initialize memory store
kernel, memory_store = await initialize_runtime_and_context("", user_id)

# Get the existing plan
plan = await memory_store.get_plan_by_plan_id(
plan_id=generate_plan_request.plan_id
)
if not plan:
track_event_if_configured(
"GeneratePlanNotFound",
{
"status_code": 404,
"detail": "Plan not found",
"plan_id": generate_plan_request.plan_id,
},
)
raise HTTPException(status_code=404, detail="Plan not found")

# Create the agents for this session
client = None
try:
client = config.get_ai_project_client()
except Exception as client_exc:
logging.error(f"Error creating AIProjectClient: {client_exc}")

agents = await AgentFactory.create_all_agents(
session_id=plan.session_id,
user_id=user_id,
memory_store=memory_store,
client=client,
)

# Get the group chat manager to process the plan
group_chat_manager = agents[AgentType.GROUP_CHAT_MANAGER.value]

# Create an InputTask from the plan's initial goal
input_task = InputTask(
session_id=plan.session_id, description=plan.initial_goal
)

# Use the group chat manager to generate the plan steps
await group_chat_manager.handle_input_task(input_task)

# Get the updated plan with steps
updated_plan = await memory_store.get_plan_by_plan_id(
plan_id=generate_plan_request.plan_id
)
steps = await memory_store.get_steps_by_plan(
plan_id=generate_plan_request.plan_id
)

# Log successful plan generation
track_event_if_configured(
"PlanGenerated",
{
"status": f"Plan generation completed for plan ID: {generate_plan_request.plan_id}",
"plan_id": generate_plan_request.plan_id,
"session_id": plan.session_id,
"steps_created": len(steps),
},
)

if client:
try:
client.close()
except Exception as e:
logging.error(f"Error closing AIProjectClient: {e}")

return {
"status": "Plan generation completed successfully",
"plan_id": generate_plan_request.plan_id,
"steps_created": len(steps),
}

except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
track_event_if_configured(
"GeneratePlanError",
{
"plan_id": generate_plan_request.plan_id,
"error": str(e),
},
)
raise HTTPException(status_code=400, detail=f"Error generating plan: {e}")


@app.post("/api/human_feedback")
async def human_feedback_endpoint(human_feedback: HumanFeedback, request: Request):
"""
Expand Down Expand Up @@ -588,12 +765,26 @@ async def human_clarification_endpoint(
track_event_if_configured(
"RAI failed",
{
"status": "Clarification is not received",
"status": "Clarification rejected - RAI validation failed",
"description": human_clarification.human_clarification,
"session_id": human_clarification.session_id,
},
)
raise HTTPException(status_code=400, detail="Invalida Clarification")
raise HTTPException(
status_code=400,
detail={
"error_type": "RAI_VALIDATION_FAILED",
"message": "Clarification Safety Check Failed",
"description": "Your clarification contains content that doesn't meet our safety guidelines. Please provide a more appropriate clarification.",
"suggestions": [
"Use clear and professional language",
"Avoid potentially harmful or inappropriate content",
"Focus on providing constructive feedback or clarification",
"Ensure your message complies with content policies",
],
"user_action": "Please revise your clarification and try again",
},
)

authenticated_user = get_authenticated_user_details(request_headers=request.headers)
user_id = authenticated_user["user_principal_id"]
Expand Down
6 changes: 6 additions & 0 deletions src/backend/models/messages_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ class UserLanguage(KernelBaseModel):
language: str


class GeneratePlanRequest(KernelBaseModel):
"""Message representing a request to generate a plan from an existing plan ID."""

plan_id: str


class ApprovalRequest(KernelBaseModel):
"""Message sent to HumanAgent to request approval for a step."""

Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { HomePage, PlanPage } from './pages';
import { HomePage, PlanPage, PlanCreatePage } from './pages';

function App() {
return (
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/plan/:planId/create" element={<PlanCreatePage />} />
<Route path="/plan/:planId" element={<PlanPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
Expand Down
52 changes: 47 additions & 5 deletions src/frontend/src/components/content/HomeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "./../../styles/HomeInput.css";
import { HomeInputProps, quickTasks, QuickTask } from "../../models/homeInput";
import { TaskService } from "../../services/TaskService";
import { NewTaskService } from "../../services/NewTaskService";
import { RAIErrorCard, RAIErrorData } from "../errors";

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

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

const resetTextarea = () => {
setInput("");
setRAIError(null); // Clear any RAI errors
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.focus();
Expand All @@ -54,6 +57,7 @@ const HomeInput: React.FC<HomeInputProps> = ({
const handleSubmit = async () => {
if (input.trim()) {
setSubmitting(true);
setRAIError(null); // Clear any previous RAI errors
let id = showToast("Creating a plan", "progress");

try {
Expand All @@ -67,17 +71,40 @@ const HomeInput: React.FC<HomeInputProps> = ({
if (response.plan_id && response.plan_id !== null) {
showToast("Plan created!", "success");
dismissToast(id);
navigate(`/plan/${response.plan_id}`);
navigate(`/plan/${response.plan_id}/create`);
} else {
showToast("Failed to create plan", "error");
dismissToast(id);
}
} catch (error: any) {
dismissToast(id);
// Show more specific error message if available
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
showToast(errorMessage, "error");
showToast(JSON.parse(error?.message)?.detail, "error");

// Check if this is an RAI validation error
let errorDetail = null;
try {
// Try to parse the error detail if it's a string
if (typeof error?.response?.data?.detail === 'string') {
errorDetail = JSON.parse(error.response.data.detail);
} else {
errorDetail = error?.response?.data?.detail;
}
} catch (parseError) {
// If parsing fails, use the original error
errorDetail = error?.response?.data?.detail;
}

// Handle RAI validation errors with better UX
if (errorDetail?.error_type === 'RAI_VALIDATION_FAILED') {
setRAIError(errorDetail);
} else {
// Handle other errors with toast messages
const errorMessage = errorDetail?.description ||
errorDetail?.message ||
error?.response?.data?.message ||
error?.message ||
"Something went wrong";
showToast(errorMessage, "error");
}

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

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

{/* Show RAI error if present */}
{raiError && (
<RAIErrorCard
error={raiError}
onRetry={() => {
setRAIError(null);
if (textareaRef.current) {
textareaRef.current.focus();
}
}}
onDismiss={() => setRAIError(null)}
/>
)}

<ChatInput
ref={textareaRef} // forwarding
value={input}
Expand Down
Loading
Loading