diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index e7dfbb28b..2e65fb0c4 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -4,7 +4,7 @@ from pathlib import Path import re from typing import Literal from loguru import logger -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, Field, field_validator from camel.types import ModelType, RoleType @@ -20,6 +20,16 @@ class ChatHistory(BaseModel): content: str +class QuestionAnalysisResult(BaseModel): + type: Literal["simple", "complex"] = Field( + description="Whether this is a simple question or complex task" + ) + answer: str | None = Field( + default=None, + description="Direct answer for simple questions. None for complex tasks." + ) + + McpServers = dict[Literal["mcpServers"], dict[str, dict]] diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 0d30301b2..e6a874a69 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -20,7 +20,7 @@ from app.utils.toolkit.human_toolkit import HumanToolkit from app.utils.toolkit.note_taking_toolkit import NoteTakingToolkit from app.utils.workforce import Workforce from loguru import logger -from app.model.chat import Chat, NewAgent, Status, sse_json, TaskContent +from app.model.chat import Chat, NewAgent, QuestionAnalysisResult, Status, sse_json, TaskContent from camel.tasks import Task from app.utils.agent import ( ListenChatAgent, @@ -708,7 +708,7 @@ def add_sub_tasks(camel_task: Task, update_tasks: list[TaskContent]): async def question_confirm(agent: ListenChatAgent, prompt: str, task_lock: TaskLock = None) -> str | Literal[True]: """ - Unified question confirmation that can work with or without context. + Unified question confirmation using structured output. Args: agent: The agent to perform the confirmation @@ -719,46 +719,57 @@ async def question_confirm(agent: ListenChatAgent, prompt: str, task_lock: TaskL Either the answer for simple queries or True for complex tasks """ - # Build context if available context_prompt = "" - if task_lock and task_lock.conversation_history: context_prompt = "=== Previous Conversation ===\n" - for entry in task_lock.conversation_history: role = entry['role'] content = entry['content'] - if role == 'task_result': - # Include full task result context context_prompt += f"[Task Completed]:\n{content}\n" else: context_prompt += f"{role.capitalize()}: {content}\n" - context_prompt += "\n" - if task_lock and task_lock.last_task_result: context_prompt += f"=== Last Task Result ===\n{task_lock.last_task_result}\n\n" - - # Build unified prompt full_prompt = f"""{context_prompt}User Query: {prompt} -Determine if this is: -- A simple question/greeting that can be answered directly → Provide a direct response -- A complex task requiring tools, code execution, or multiple steps → Respond with only "yes" +Analyze if this is a simple question or complex task: -Note: If you can answer using the conversation history or previous results, provide the answer directly. -""" +**Simple question**: Can be answered directly using knowledge or conversation history +- Examples: greetings, fact queries, clarifications about previous results +- Response: Provide a direct, helpful answer +**Complex task**: Requires tools, code execution, file operations, or multi-step planning +- Examples: "create a file", "search for", "implement feature X" +- Response: Indicate this is complex - # Execute agent - resp = agent.step(full_prompt) +Based on the user query, determine the type and provide appropriate response.""" - is_complex = resp.msgs[0].content.lower() == "yes" + try: + resp = agent.step(full_prompt, response_format=QuestionAnalysisResult) - if not is_complex: - return sse_json("wait_confirm", {"content": resp.msgs[0].content, "question": prompt}) - else: + if not resp or not resp.msgs or len(resp.msgs) == 0: + return True + + result = resp.msgs[0].parsed + + if not result: + content = resp.msgs[0].content + if not content: + return True + normalized = content.strip().lower() + if normalized in ["yes", "complex"]: + return True + return sse_json("wait_confirm", {"content": content, "question": prompt}) + + if result.type == "simple" and result.answer: + return sse_json("wait_confirm", {"content": result.answer, "question": prompt}) + else: + return True + + except Exception as e: + logger.error(f"Error in question_confirm: {e}") return True diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 0749d2a6c..4a76ae909 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -450,34 +450,37 @@ const chatStore = (initial?: Partial) => createStore()( */ let currentTaskId = getCurrentTaskId(); const previousChatStore = getCurrentChatStore() - if(agentMessages.step === "confirmed" && - previousChatStore.tasks[currentTaskId].messages.some(m => m.step === "to_sub_tasks")) { - //Create a newChatStore as we are not touching startTask - const initResult = projectStore.appendInitChatStore(projectStore.activeProjectId!); - if(initResult) { - const {taskId: newTaskId, chatStore: newChatStore} = initResult; + if(agentMessages.step === "confirmed" && + previousChatStore.tasks[currentTaskId]?.messages?.some(m => m.step === "to_sub_tasks")) { + if(projectStore.activeProjectId) { + const initResult = projectStore.appendInitChatStore(projectStore.activeProjectId); + if(initResult) { + const {taskId: newTaskId, chatStore: newChatStore} = initResult; - /** - * Get Last user message - * @todo TODO: Internalize the message function when Continuing conversation with improve API - * Just like @function chatStore.startTask(_taskId, undefined, undefined, undefined, tempMessageContent, attachesToSend); - * Instead of manually removing the message if its a new workforce is needed - */ - const lastMessage = previousChatStore.tasks[currentTaskId].messages.at(-1); - previousChatStore.removeMessage(currentTaskId, lastMessage?.id!); + /** + * Get Last user message + * @todo TODO: Internalize the message function when Continuing conversation with improve API + * Just like @function chatStore.startTask(_taskId, undefined, undefined, undefined, tempMessageContent, attachesToSend); + * Instead of manually removing the message if its a new workforce is needed + */ + const lastMessage = previousChatStore.tasks[currentTaskId]?.messages.at(-1); + if(lastMessage?.id) { + previousChatStore.removeMessage(currentTaskId, lastMessage.id); + } - newChatStore?.getState().setIsPending(newTaskId, true); - // Add the user message to show it in UI - newChatStore?.getState().addMessages(newTaskId, { - id: generateUniqueId(), - role: "user", - content: lastMessage?.content ?? "", - //Use previous chatStore's attaches - attaches: JSON.parse(JSON.stringify(previousChatStore.tasks[currentTaskId]?.attaches)) || [], - }); + newChatStore?.getState().setIsPending(newTaskId, true); + if(lastMessage) { + newChatStore?.getState().addMessages(newTaskId, { + id: generateUniqueId(), + role: "user", + content: lastMessage.content ?? "", + attaches: [...(previousChatStore.tasks[currentTaskId]?.attaches || [])], + }); + } - updateLockedReferences(newChatStore, newTaskId); - console.log("[NEW CHATSTORE] In current workforce instance"); + updateLockedReferences(newChatStore, newTaskId); + console.log("[NEW CHATSTORE] In current workforce instance"); + } } } @@ -1513,18 +1516,23 @@ const chatStore = (initial?: Partial) => createStore()( })) }, removeMessage(taskId, messageId) { - set((state) => ({ - ...state, - tasks: { - ...state.tasks, - [taskId]: { - ...state.tasks[taskId], - messages: state.tasks[taskId].messages.filter( - (message) => message.id !== messageId - ), + set((state) => { + if (!state.tasks[taskId]) { + return state; + } + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + messages: state.tasks[taskId].messages.filter( + (message) => message.id !== messageId + ), + }, }, - }, - })) + }; + }) }, setCotList(taskId, cotList) { set((state) => ({