Skip to content

Commit d5c2847

Browse files
committed
fix (tasks): new history management for task-scoped chats
1 parent 065e73a commit d5c2847

5 files changed

Lines changed: 80 additions & 30 deletions

File tree

src/client/components/tasks/TaskDetailsContent.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ const TaskChatSection = ({ task, onSendChatMessage }) => {
144144
<ChatBubble
145145
key={index}
146146
role={msg.role}
147+
turn_steps={msg.turn_steps || []}
147148
content={msg.content}
148149
message={msg}
149150
/>

src/server/main/chat/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ def parse_assistant_response(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
126126
role = msg.get("role", "")
127127
content = msg.get("content", "")
128128

129+
# ✅ Thoughts from <think> tags
130+
if role == "assistant" and content and "<think>" in content:
131+
think_matches = re.findall(r"<think>([\s\S]*?)</think>", content, re.DOTALL)
132+
for match in think_matches:
133+
turn_steps.append({
134+
"type": "thought",
135+
"content": match.strip()
136+
})
137+
129138
# ✅ Thoughts from <think> tags
130139
if role == "assistant" and content and "<think>" in content:
131140
think_matches = re.findall(r"<think>([\s\S]*?)</think>", content, re.DOTALL)

src/server/workers/planner/db.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,8 @@ async def update_task_field(self, task_id: str, fields: dict):
9898
)
9999
logger.info(f"Updated fields for task {task_id}: {list(fields.keys())}")
100100

101-
async def update_task_with_plan(self, task_id: str, plan_data: dict, is_change_request: bool = False):
101+
async def update_task_with_plan(self, task_id: str, plan_data: dict, is_change_request: bool = False, chat_history: Optional[List[Dict]] = None):
102102
"""Updates a task with a generated plan and sets it to pending approval."""
103-
SENSITIVE_PLAN_FIELDS = ["name", "description", "plan"]
104-
encrypt_doc(plan_data, SENSITIVE_PLAN_FIELDS)
105-
106103
plan_steps = plan_data.get("plan", [])
107104

108105
update_doc = {
@@ -111,12 +108,19 @@ async def update_task_with_plan(self, task_id: str, plan_data: dict, is_change_r
111108
"updated_at": datetime.datetime.now(datetime.timezone.utc)
112109
}
113110

111+
if chat_history is not None:
112+
update_doc["chat_history"] = chat_history
113+
114114
# Only set the main description for the very first run.
115115
if not is_change_request:
116116
name = plan_data.get("name", "Proactively generated plan")
117117
update_doc["name"] = name
118118
update_doc["description"] = plan_data.get("description", "")
119119

120+
# Encrypt all sensitive fields at once before updating
121+
SENSITIVE_TASK_FIELDS = ["name", "description", "plan", "chat_history"]
122+
encrypt_doc(update_doc, SENSITIVE_TASK_FIELDS)
123+
120124
result = await self.tasks_collection.update_one(
121125
{"task_id": task_id},
122126
{"$set": update_doc}

src/server/workers/planner/prompts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@
5858
]
5959
}}
6060
61-
Final Instructions:
61+
Final Instructions & Output Format:
6262
- Create a concise `name` for the task.
6363
- Create a concise `description` summarizing the overall goal.
6464
- Break down the goal into logical steps, choosing the most appropriate tool for each.
6565
- If an action item is not actionable with the given tools (e.g., "Think about the marketing report"), do not create a plan for it.
6666
- Do not include any text outside of the JSON object. Your response must begin with `{{` and end with `}}`.
6767
- ALWAYS RETURN THE JSON OBJECT.
68+
- CRITICAL: Your final response MUST be the JSON object described above, wrapped in `<answer>` tags. For example: `<answer>{{"name": "...", ...}}</answer>`.
6869
"""

src/server/workers/tasks.py

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from main.analytics import capture_event
1515
from json_extractor import JsonExtractor
1616
from workers.utils.api_client import notify_user, push_task_list_update
17+
from main.chat.utils import parse_assistant_response
1718
from main.plans import PLAN_LIMITS
1819
from main.config import INTEGRATIONS_CONFIG
1920
from main.tasks.prompts import TASK_CREATION_PROMPT
@@ -464,6 +465,7 @@ def generate_plan_from_context(task_id: str, user_id: str):
464465

465466
async def async_generate_plan(task_id: str, user_id: str):
466467
"""Async logic for plan generation."""
468+
# MODIFICATION: This function now also saves the full agent turn to chat_history
467469
db_manager = PlannerMongoManager()
468470
try:
469471
task = await db_manager.get_task(task_id)
@@ -489,29 +491,16 @@ async def async_generate_plan(task_id: str, user_id: str):
489491
# If the document is missing it for some reason, log a warning and proceed.
490492
if not task.get("user_id"):
491493
logger.warning(f"Task {task_id} document is missing user_id. Proceeding with passed user_id '{user_id}'.")
492-
# Attempt to heal the document
493494
await db_manager.update_task_field(task_id, {"user_id": user_id})
494495

495496
original_context = task.get("original_context", {})
496497

497-
# For re-planning, add previous results and chat history to the context
498-
if task.get("chat_history"):
499-
original_context["chat_history"] = task.get("chat_history")
500-
original_context["previous_plan"] = task.get("plan")
501-
original_context["previous_result"] = task.get("result")
502-
503498
user_profile = await db_manager.user_profiles_collection.find_one(
504499
{"user_id": user_id},
505500
{"userData.personalInfo": 1} # Projection to get only necessary data
506501
)
507502
if not user_profile:
508503
logger.error(f"User profile not found for user_id '{user_id}' associated with task {task_id}. Cannot generate plan.")
509-
await db_manager.update_task_status(task_id, "error", {"error": f"User profile not found for user_id '{user_id}'."})
510-
return
511-
512-
if not user_profile:
513-
logger.error(f"User profile not found for user_id '{user_id}' associated with task {task_id}. Cannot generate plan.")
514-
await db_manager.update_task_status(task_id, "error", {"error": f"User profile not found for user_id '{user_id}'."})
515504
return
516505

517506
personal_info = user_profile.get("userData", {}).get("personalInfo", {})
@@ -527,36 +516,82 @@ async def async_generate_plan(task_id: str, user_id: str):
527516
user_timezone = ZoneInfo(user_timezone_str)
528517
except ZoneInfoNotFoundError:
529518
logger.warning(f"Invalid timezone '{user_timezone_str}' for user {user_id}. Defaulting to UTC.")
519+
user_timezone_str = "UTC"
530520
user_timezone = ZoneInfo("UTC")
531521

532522
current_user_time = datetime.datetime.now(user_timezone).strftime('%Y-%m-%d %H:%M:%S %Z')
533523

534-
action_items = task.get("action_items", [])
535-
if not action_items:
536-
# This is likely a manually created task. Use its description as the action item.
537-
logger.info(f"Task {task_id}: No 'action_items' field found. Using main description as the action.")
538-
action_items = [task.get("description", "")]
539-
540524
available_tools = get_all_mcp_descriptions()
541-
542525
agent_config = get_planner_agent(available_tools, current_user_time, user_name, user_location)
543526

544-
user_prompt_content = "Please create a plan for the following action items:\n- " + "\n- ".join(action_items)
545-
messages = [{'role': 'user', 'content': user_prompt_content}]
527+
messages = []
528+
if is_change_request:
529+
logger.info(f"Task {task_id} has chat history. Constructing messages from history for re-planning.")
530+
531+
previous_plan_str = json.dumps(task.get("plan", []), indent=2)
532+
# Use default=str to handle non-serializable types like datetime
533+
previous_result_str = json.dumps(task.get("result", "No previous result."), indent=2, default=str)
534+
535+
context_message = (
536+
"You are re-planning a task based on user feedback. Here is the context of the previous run:\n\n"
537+
f"**Previous Plan:**\n```json\n{previous_plan_str}\n```\n\n"
538+
f"**Previous Result:**\n```json\n{previous_result_str}\n```\n\n"
539+
"Now, review the following conversation and generate a new plan based on the user's latest request."
540+
)
541+
messages.append({"role": "system", "content": context_message})
542+
543+
for msg in task["chat_history"]:
544+
# This part is tricky. The planner doesn't need the full turn_steps of previous turns.
545+
# It just needs the user/assistant content.
546+
role = msg.get("role")
547+
if role not in ["user", "assistant"]:
548+
continue
549+
messages.append({
550+
"role": role,
551+
"content": msg.get("content")
552+
})
553+
else:
554+
action_items = task.get("action_items", []) or [task.get("description", "")]
555+
user_prompt_content = "Please create a plan for the following action items:\n- " + "\n- ".join(action_items)
556+
messages = [{'role': 'user', 'content': user_prompt_content}]
546557

547558
final_response_str = ""
559+
final_history = None
548560
for chunk in run_main_agent(system_message=agent_config["system_message"], function_list=agent_config["function_list"], messages=messages):
561+
final_history = chunk # Keep track of the latest state
549562
if isinstance(chunk, list) and chunk and chunk[-1].get("role") == "assistant":
550563
final_response_str = chunk[-1].get("content", "")
551564

552565
if not final_response_str:
553566
raise Exception("Planner agent returned no response.")
554567

555-
plan_data = JsonExtractor.extract_valid_json(clean_llm_output(final_response_str))
568+
if not final_history:
569+
raise Exception("Planner agent returned no history.")
570+
571+
# --- NEW: Parse the full agent turn and save it to chat_history ---
572+
assistant_turn_start_index = next((i for i, msg in reversed(list(enumerate(final_history))) if msg.get("role") in ["user", "system"]), -1) + 1
573+
assistant_messages = final_history[assistant_turn_start_index:]
574+
parsed_turn = parse_assistant_response(assistant_messages)
575+
plan_json_str = parsed_turn.get("final_content", "")
576+
turn_steps = parsed_turn.get("turn_steps", [])
577+
578+
plan_data = JsonExtractor.extract_valid_json(plan_json_str)
556579
if not plan_data or "plan" not in plan_data:
557-
raise Exception(f"Planner agent returned invalid JSON: {final_response_str}")
580+
raise Exception(f"Planner agent returned invalid JSON inside <answer> tag: {plan_json_str}")
581+
582+
new_assistant_message = {
583+
"message_id": str(uuid.uuid4()),
584+
"role": "assistant",
585+
"content": "I have generated a new plan based on your request. Please review it for approval.",
586+
"turn_steps": turn_steps,
587+
"timestamp": datetime.datetime.now(datetime.timezone.utc)
588+
}
589+
590+
chat_history = task.get("chat_history", [])
591+
if not isinstance(chat_history, list): chat_history = []
592+
chat_history.append(new_assistant_message)
558593

559-
await db_manager.update_task_with_plan(task_id, plan_data, is_change_request)
594+
await db_manager.update_task_with_plan(task_id, plan_data, is_change_request, chat_history)
560595
capture_event(user_id, "proactive_task_generated", {
561596
"task_id": task_id,
562597
"source": task.get("original_context", {}).get("source", "unknown"),

0 commit comments

Comments
 (0)