This commit is contained in:
puzhen 2025-10-19 17:10:53 +01:00
parent 033bd19c93
commit bea6b7634c
3 changed files with 88 additions and 59 deletions

View file

@ -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]]

View file

@ -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

View file

@ -450,34 +450,37 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
*/
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<ChatStore>) => createStore<ChatStore>()(
}))
},
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) => ({