1414from main .analytics import capture_event
1515from json_extractor import JsonExtractor
1616from workers .utils .api_client import notify_user , push_task_list_update
17+ from main .chat .utils import parse_assistant_response
1718from main .plans import PLAN_LIMITS
1819from main .config import INTEGRATIONS_CONFIG
1920from main .tasks .prompts import TASK_CREATION_PROMPT
@@ -464,6 +465,7 @@ def generate_plan_from_context(task_id: str, user_id: str):
464465
465466async 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