lollms-client 0.22.0__py3-none-any.whl → 0.24.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lollms-client might be problematic. Click here for more details.

@@ -20,7 +20,7 @@ import requests
20
20
  from typing import List, Optional, Callable, Union, Dict, Any
21
21
  import numpy as np
22
22
  from pathlib import Path
23
- import os
23
+ import uuid
24
24
 
25
25
  class LollmsClient():
26
26
  """
@@ -526,7 +526,8 @@ class LollmsClient():
526
526
  seed: Optional[int] = None,
527
527
  n_threads: Optional[int] = None,
528
528
  ctx_size: Optional[int] = None,
529
- streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None
529
+ streaming_callback: Optional[Callable[[str, MSG_TYPE, Dict], bool]] = None,
530
+ **kwargs
530
531
  ) -> Union[str, dict]:
531
532
  """
532
533
  High-level method to perform a chat generation using a LollmsDiscussion object.
@@ -558,7 +559,7 @@ class LollmsClient():
558
559
  discussion=discussion,
559
560
  branch_tip_id=branch_tip_id,
560
561
  n_predict=n_predict if n_predict is not None else self.default_n_predict,
561
- stream=stream if stream is not None else self.default_stream,
562
+ stream=stream if stream is not None else True if streaming_callback is not None else self.default_stream,
562
563
  temperature=temperature if temperature is not None else self.default_temperature,
563
564
  top_k=top_k if top_k is not None else self.default_top_k,
564
565
  top_p=top_p if top_p is not None else self.default_top_p,
@@ -835,7 +836,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
835
836
  formatted_tools_list = "\n".join([f"- Tool: {t.get('name')}\n Description: {t.get('description')}\n Schema: {json.dumps(t.get('input_schema'))}" for t in tools])
836
837
 
837
838
  if streaming_callback:
838
- streaming_callback("Building/Revising plan...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "plan_extraction"}, turn_history)
839
+ streaming_callback("Building/Revising plan...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "plan_extraction"}, turn_history = turn_history)
839
840
 
840
841
  obj_prompt = (
841
842
  "You are an Intelligent Workflow Planner. Your mission is to create the most efficient plan possible by analyzing the user's request within the context of the full conversation.\n\n"
@@ -863,8 +864,8 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
863
864
  current_plan = self.remove_thinking_blocks(initial_plan_gen).strip()
864
865
 
865
866
  if streaming_callback:
866
- streaming_callback("Building initial plan...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "plan_extraction"}, turn_history)
867
- streaming_callback(f"Current plan:\n{current_plan}", MSG_TYPE.MSG_TYPE_STEP, {"id": "plan"}, turn_history)
867
+ streaming_callback("Building initial plan...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "plan_extraction"}, turn_history = turn_history)
868
+ streaming_callback(f"Current plan:\n{current_plan}", MSG_TYPE.MSG_TYPE_STEP, {"id": "plan"}, turn_history = turn_history)
868
869
  turn_history.append({"type": "initial_plan", "content": current_plan})
869
870
 
870
871
  tool_calls_made_this_turn = []
@@ -872,7 +873,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
872
873
 
873
874
  while llm_iterations < max_llm_iterations:
874
875
  llm_iterations += 1
875
- if streaming_callback: streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"planning_step_{llm_iterations}"}, turn_history)
876
+ if streaming_callback: streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"planning_step_{llm_iterations}"}, turn_history = turn_history)
876
877
 
877
878
  formatted_agent_history = "No actions taken yet in this turn."
878
879
  if agent_work_history:
@@ -896,7 +897,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
896
897
  except (json.JSONDecodeError, AttributeError, KeyError) as e:
897
898
  error_message = f"JSON parsing failed (Attempt {i+1}/{max_json_retries+1}). Error: {e}"
898
899
  ASCIIColors.warning(error_message)
899
- if streaming_callback: streaming_callback(error_message, MSG_TYPE.MSG_TYPE_WARNING, None, turn_history)
900
+ if streaming_callback: streaming_callback(error_message, MSG_TYPE.MSG_TYPE_WARNING, None, turn_history = turn_history)
900
901
  turn_history.append({"type": "error", "content": f"Invalid JSON response: {raw_llm_decision_json}"})
901
902
  if i >= max_json_retries:
902
903
  ASCIIColors.error("Max JSON retries reached. Aborting agent loop.")
@@ -918,7 +919,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
918
919
  current_plan = llm_decision.get("updated_plan", current_plan)
919
920
  action = llm_decision.get("action")
920
921
  action_details = llm_decision.get("action_details", {})
921
- if streaming_callback: streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history)
922
+ if streaming_callback: streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history = turn_history)
922
923
 
923
924
  if action == "call_tool":
924
925
  if len(tool_calls_made_this_turn) >= max_tool_calls:
@@ -930,18 +931,18 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
930
931
  ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
931
932
  break
932
933
 
933
- if streaming_callback: streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
934
+ if streaming_callback: streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}"}, turn_history = turn_history)
934
935
  tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
935
936
  if streaming_callback:
936
- streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
937
- streaming_callback(json.dumps(tool_result, indent=2), MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, tool_result, turn_history)
937
+ streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history = turn_history)
938
+ streaming_callback(json.dumps(tool_result, indent=2), MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, tool_result, turn_history = turn_history)
938
939
 
939
- if streaming_callback: streaming_callback("Synthesizing new knowledge...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"synthesis_step_{llm_iterations}"}, turn_history)
940
+ if streaming_callback: streaming_callback("Synthesizing new knowledge...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"synthesis_step_{llm_iterations}"}, turn_history = turn_history)
940
941
  new_scratchpad = self._synthesize_knowledge(previous_scratchpad=knowledge_scratchpad, tool_name=tool_name, tool_params=tool_params, tool_result=tool_result)
941
942
  knowledge_scratchpad = new_scratchpad
942
943
  if streaming_callback:
943
- streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"synthesis_step_{llm_iterations}"}, turn_history)
944
- streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id": "scratchpad_update"}, turn_history)
944
+ streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"synthesis_step_{llm_iterations}"}, turn_history = turn_history)
945
+ streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id": "scratchpad_update"}, turn_history = turn_history)
945
946
 
946
947
  work_entry = { "thought": llm_decision.get("thought", "N/A"), "tool_name": tool_name, "tool_params": tool_params, "tool_result": tool_result, "synthesized_knowledge": knowledge_scratchpad }
947
948
  agent_work_history.append(work_entry)
@@ -960,12 +961,12 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
960
961
  break
961
962
 
962
963
  if streaming_callback:
963
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations}) complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
964
+ streaming_callback(f"LLM reasoning step (iteration {llm_iterations}) complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history = turn_history)
964
965
 
965
966
  if streaming_callback:
966
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations}) complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
967
+ streaming_callback(f"LLM reasoning step (iteration {llm_iterations}) complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history = turn_history)
967
968
  if streaming_callback:
968
- streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history)
969
+ streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history = turn_history)
969
970
 
970
971
  final_answer_prompt = (
971
972
  "You are an AI assistant tasked with providing a final, comprehensive answer to the user based on the research performed.\n\n"
@@ -982,7 +983,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
982
983
  final_answer_text = self.generate_text(prompt=final_answer_prompt, system_prompt=system_prompt, images=images, stream=streaming_callback is not None, streaming_callback=streaming_callback, temperature=final_answer_temperature if final_answer_temperature is not None else self.default_temperature, **(llm_generation_kwargs or {}))
983
984
 
984
985
  if streaming_callback:
985
- streaming_callback("Final answer generation complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "final_answer_synthesis"}, turn_history)
986
+ streaming_callback("Final answer generation complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "final_answer_synthesis"}, turn_history = turn_history)
986
987
 
987
988
  final_answer = self.remove_thinking_blocks(final_answer_text)
988
989
  turn_history.append({"type":"final_answer_generated", "content": final_answer})
@@ -1283,303 +1284,436 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1283
1284
  "error": None
1284
1285
  }
1285
1286
 
1287
+ # --- Start of modified/added methods ---
1288
+ def _synthesize_knowledge(
1289
+ self,
1290
+ previous_scratchpad: str,
1291
+ tool_name: str,
1292
+ tool_params: dict,
1293
+ tool_result: dict
1294
+ ) -> str:
1295
+ """
1296
+ A dedicated LLM call to interpret a tool's output and update the knowledge scratchpad.
1297
+ """
1298
+ # Sanitize tool_result for LLM to avoid sending large binary/base64 data
1299
+ sanitized_result = tool_result.copy()
1300
+ if 'image_path' in sanitized_result:
1301
+ sanitized_result['summary'] = f"An image was successfully generated and saved to '{sanitized_result['image_path']}'."
1302
+ # Remove keys that might contain large data if they exist
1303
+ sanitized_result.pop('image_base64', None)
1304
+ elif 'file_path' in sanitized_result and 'content' in sanitized_result:
1305
+ sanitized_result['summary'] = f"Content was successfully written to '{sanitized_result['file_path']}'."
1306
+ sanitized_result.pop('content', None)
1307
+
1308
+
1309
+ synthesis_prompt = (
1310
+ "You are a data analyst assistant. Your sole job is to interpret the output of a tool and integrate it into the existing research summary (knowledge scratchpad).\n\n"
1311
+ "--- PREVIOUS KNOWLEDGE SCRATCHPAD ---\n"
1312
+ f"{previous_scratchpad}\n\n"
1313
+ "--- ACTION JUST TAKEN ---\n"
1314
+ f"Tool Called: `{tool_name}`\n"
1315
+ f"Parameters: {json.dumps(tool_params)}\n\n"
1316
+ "--- RAW TOOL OUTPUT ---\n"
1317
+ f"```json\n{json.dumps(sanitized_result, indent=2)}\n```\n\n"
1318
+ "--- YOUR TASK ---\n"
1319
+ "Read the 'RAW TOOL OUTPUT' and explain what it means in plain language. Then, integrate this new information with the 'PREVIOUS KNOWLEDGE SCRATCHPAD' to create a new, complete, and self-contained summary.\n"
1320
+ "Your output should be ONLY the text of the new scratchpad, with no extra commentary or formatting.\n\n"
1321
+ "--- NEW KNOWLEDGE SCRATCHPAD ---\n"
1322
+ )
1323
+ new_scratchpad_text = self.generate_text(prompt=synthesis_prompt, n_predict=1024, temperature=0.0)
1324
+ return self.remove_thinking_blocks(new_scratchpad_text).strip()
1325
+ def generate_structured_content(
1326
+ self,
1327
+ prompt: str,
1328
+ template: Union[dict, list],
1329
+ system_prompt: Optional[str] = None,
1330
+ images: Optional[List[str]] = None,
1331
+ max_retries: int = 3,
1332
+ **kwargs
1333
+ ) -> Union[dict, list, None]:
1334
+ """
1335
+ Generates structured content (JSON) from a prompt, ensuring it matches a given template.
1336
+
1337
+ This method repeatedly calls the LLM until a valid JSON object that can be parsed
1338
+ and somewhat matches the template is returned, or until max_retries is reached.
1339
+
1340
+ Args:
1341
+ prompt (str): The main prompt to guide the LLM.
1342
+ template (Union[dict, list]): A Python dict or list representing the desired JSON structure.
1343
+ system_prompt (Optional[str], optional): An optional system prompt. Defaults to None.
1344
+ images (Optional[List[str]], optional): A list of image paths for multimodal prompts. Defaults to None.
1345
+ max_retries (int, optional): The maximum number of times to retry generation if parsing fails. Defaults to 3.
1346
+ **kwargs: Additional keyword arguments to pass to the underlying generate_text method.
1347
+
1348
+ Returns:
1349
+ Union[dict, list, None]: The parsed JSON object (as a Python dict or list), or None if it fails after all retries.
1350
+ """
1351
+ template_str = json.dumps(template, indent=4)
1352
+
1353
+ if not system_prompt:
1354
+ system_prompt = "You are a highly intelligent AI assistant that excels at generating structured data in JSON format."
1355
+
1356
+ final_system_prompt = (
1357
+ f"{system_prompt}\n\n"
1358
+ "You MUST generate a response that is a single, valid JSON object matching the structure of the template provided by the user. "
1359
+ "Your entire response should be enclosed in a single ```json markdown code block. "
1360
+ "Do not include any other text, explanations, or apologies outside of the JSON code block.\n"
1361
+ f"Here is the JSON template you must follow:\n{template_str}"
1362
+ )
1363
+
1364
+ current_prompt = prompt
1365
+ for attempt in range(max_retries):
1366
+ raw_llm_output = self.generate_text(
1367
+ prompt=current_prompt,
1368
+ system_prompt=final_system_prompt,
1369
+ images=images,
1370
+ **kwargs
1371
+ )
1372
+
1373
+ if not raw_llm_output:
1374
+ ASCIIColors.warning(f"Structured content generation failed (Attempt {attempt + 1}/{max_retries}): LLM returned an empty response.")
1375
+ current_prompt = f"You previously returned an empty response. Please try again and adhere strictly to the JSON format. \nOriginal prompt was: {prompt}"
1376
+ continue
1377
+
1378
+ try:
1379
+ # Use robust_json_parser which handles cleanup of markdown tags, comments, etc.
1380
+ parsed_json = robust_json_parser(raw_llm_output)
1381
+ # Optional: Add validation against the template's structure here if needed
1382
+ return parsed_json
1383
+ except (ValueError, json.JSONDecodeError) as e:
1384
+ ASCIIColors.warning(f"Structured content parsing failed (Attempt {attempt + 1}/{max_retries}). Error: {e}")
1385
+ trace_exception(e)
1386
+ # Prepare for retry with more explicit instructions
1387
+ current_prompt = (
1388
+ "Your previous response could not be parsed as valid JSON. Please review the error and the required template and try again. "
1389
+ "Ensure your entire output is a single, clean JSON object inside a ```json code block.\n\n"
1390
+ f"--- PARSING ERROR ---\n{str(e)}\n\n"
1391
+ f"--- YOUR PREVIOUS INVALID RESPONSE ---\n{raw_llm_output}\n\n"
1392
+ f"--- REQUIRED JSON TEMPLATE ---\n{template_str}\n\n"
1393
+ f"--- ORIGINAL PROMPT ---\n{prompt}"
1394
+ )
1395
+
1396
+ ASCIIColors.error("Failed to generate valid structured content after multiple retries.")
1397
+ return None
1398
+
1399
+ def _synthesize_knowledge(
1400
+ self,
1401
+ previous_scratchpad: str,
1402
+ tool_name: str,
1403
+ tool_params: dict,
1404
+ tool_result: dict
1405
+ ) -> str:
1406
+ """
1407
+ A dedicated LLM call to interpret a tool's output and update the knowledge scratchpad.
1408
+ """
1409
+ # Sanitize tool_result for LLM to avoid sending large binary/base64 data
1410
+ sanitized_result = tool_result.copy()
1411
+ if 'image_path' in sanitized_result:
1412
+ sanitized_result['summary'] = f"An image was successfully generated and saved to '{sanitized_result['image_path']}'."
1413
+ # Remove keys that might contain large data if they exist
1414
+ sanitized_result.pop('image_base64', None)
1415
+ elif 'file_path' in sanitized_result and 'content' in sanitized_result:
1416
+ sanitized_result['summary'] = f"Content was successfully written to '{sanitized_result['file_path']}'."
1417
+ sanitized_result.pop('content', None)
1418
+
1419
+
1420
+ synthesis_prompt = (
1421
+ "You are a data analyst assistant. Your sole job is to interpret the output of a tool and integrate it into the existing research summary (knowledge scratchpad).\n\n"
1422
+ "--- PREVIOUS KNOWLEDGE SCRATCHPAD ---\n"
1423
+ f"{previous_scratchpad}\n\n"
1424
+ "--- ACTION JUST TAKEN ---\n"
1425
+ f"Tool Called: `{tool_name}`\n"
1426
+ f"Parameters: {json.dumps(tool_params)}\n\n"
1427
+ "--- RAW TOOL OUTPUT ---\n"
1428
+ f"```json\n{json.dumps(sanitized_result, indent=2)}\n```\n\n"
1429
+ "--- YOUR TASK ---\n"
1430
+ "Read the 'RAW TOOL OUTPUT' and explain what it means in plain language. Then, integrate this new information with the 'PREVIOUS KNOWLEDGE SCRATCHPAD' to create a new, complete, and self-contained summary.\n"
1431
+ "Your output should be ONLY the text of the new scratchpad, with no extra commentary or formatting.\n\n"
1432
+ "--- NEW KNOWLEDGE SCRATCHPAD ---\n"
1433
+ )
1434
+ new_scratchpad_text = self.generate_text(prompt=synthesis_prompt, n_predict=1024, temperature=0.0)
1435
+ return self.remove_thinking_blocks(new_scratchpad_text).strip()
1436
+
1437
+ # In lollms_client/lollms_discussion.py -> LollmsClient class
1438
+
1286
1439
  def generate_with_mcp_rag(
1287
1440
  self,
1288
1441
  prompt: str,
1289
- rag_query_function: Callable[[str, Optional[str], int, float], List[Dict[str, Any]]],
1442
+ use_mcps: Union[None, bool, List[str]] = None,
1443
+ use_data_store: Union[None, Dict[str, Callable]] = None,
1290
1444
  system_prompt: str = None,
1291
- objective_extraction_system_prompt="Extract objectives",
1445
+ reasoning_system_prompt: str = "You are a logical and adaptive AI assistant.",
1292
1446
  images: Optional[List[str]] = None,
1293
- tools: Optional[List[Dict[str, Any]]] = None,
1294
- max_tool_calls: int = 10,
1295
- max_llm_iterations: int = 15,
1296
- tool_call_decision_temperature: float = 0.0,
1447
+ max_reasoning_steps: int = 10,
1448
+ decision_temperature: float = 0.0,
1297
1449
  final_answer_temperature: float = None,
1298
- streaming_callback: Optional[Callable[[str, MSG_TYPE, Optional[Dict], Optional[List]], bool]] = None,
1299
- build_plan: bool = True,
1300
- rag_vectorizer_name: Optional[str] = None,
1450
+ streaming_callback: Optional[Callable[[str, 'MSG_TYPE', Optional[Dict], Optional[List]], bool]] = None,
1301
1451
  rag_top_k: int = 5,
1302
1452
  rag_min_similarity_percent: float = 70.0,
1453
+ output_summarization_threshold: int = 500, # In tokens
1303
1454
  **llm_generation_kwargs
1304
1455
  ) -> Dict[str, Any]:
1305
- """
1306
- Generates a response using a stateful agent that can choose between calling standard
1307
- MCP tools and querying a RAG database, all within a unified reasoning loop.
1456
+ """Generates a response using a dynamic agent with stateful, ID-based step tracking.
1457
+
1458
+ This method orchestrates a sophisticated agentic process where an AI
1459
+ repeatedly observes its state, thinks about the next best action, and
1460
+ acts. This "observe-think-act" loop allows the agent to adapt to new
1461
+ information, recover from failures, and build a comprehensive
1462
+ understanding of the problem before responding.
1463
+
1464
+ A key feature is its stateful step notification system, designed for rich
1465
+ UI integration. When a step starts, it sends a `step_start` message with
1466
+ a unique ID and description. When it finishes, it sends a `step_end`
1467
+ message with the same ID, allowing a user interface to track the
1468
+ progress of specific, long-running tasks like tool calls.
1469
+
1470
+ Args:
1471
+ prompt: The user's initial prompt or question.
1472
+ use_mcps: Controls MCP tool usage.
1473
+ use_data_store: Controls RAG usage.
1474
+ system_prompt: The main system prompt for the final answer generation.
1475
+ reasoning_system_prompt: The system prompt for the iterative
1476
+ decision-making process.
1477
+ images: A list of base64-encoded images provided by the user.
1478
+ max_reasoning_steps: The maximum number of reasoning cycles.
1479
+ decision_temperature: The temperature for the LLM's decision-making.
1480
+ final_answer_temperature: The temperature for the final answer synthesis.
1481
+ streaming_callback: A function for real-time output of tokens and steps.
1482
+ rag_top_k: The number of top documents to retrieve during RAG.
1483
+ rag_min_similarity_percent: Minimum similarity for RAG results.
1484
+ output_summarization_threshold: The token count that triggers automatic
1485
+ summarization of a tool's text output.
1486
+ **llm_generation_kwargs: Additional keyword arguments for LLM calls.
1487
+
1488
+ Returns:
1489
+ A dictionary containing the agent's full run, including the final
1490
+ answer, the complete internal scratchpad, a log of tool calls,
1491
+ any retrieved RAG sources, and other metadata.
1308
1492
  """
1309
1493
  if not self.binding:
1310
- return {"final_answer": "", "tool_calls": [], "error": "LLM binding not initialized."}
1311
- if not self.mcp:
1312
- return {"final_answer": "", "tool_calls": [], "error": "MCP binding not initialized."}
1494
+ return {"final_answer": "", "tool_calls": [], "sources": [], "error": "LLM binding not initialized."}
1313
1495
 
1314
1496
  # --- Initialize Agent State ---
1315
- turn_history: List[Dict[str, Any]] = []
1497
+ sources_this_turn: List[Dict[str, Any]] = []
1498
+ tool_calls_this_turn: List[Dict[str, Any]] = []
1316
1499
  original_user_prompt = prompt
1317
- knowledge_scratchpad = "No information gathered yet."
1318
- current_objectives = ""
1319
- agent_work_history = []
1320
- tool_calls_made_this_turn = []
1321
- llm_iterations = 0
1322
-
1323
- # --- 1. Discover MCP Tools and Inject the RAG Tool ---
1324
- if tools is None:
1325
- try:
1326
- mcp_tools = self.mcp.discover_tools(force_refresh=True)
1327
- if not mcp_tools: ASCIIColors.warning("No MCP tools discovered.")
1328
- except Exception as e_disc:
1329
- return {"final_answer": "", "tool_calls": [], "error": f"Failed to discover MCP tools: {e_disc}"}
1330
- else:
1331
- mcp_tools = tools
1332
-
1333
- # Define the RAG tool and add it to the list
1334
- rag_tool_definition = {
1335
- "name": "research::query_database",
1336
- "description": (
1337
- "Queries a vector database to find relevant text chunks based on a natural language query. "
1338
- "Use this to gather information, answer questions, or find context for a task before using other tools."
1339
- ),
1340
- "input_schema": {
1341
- "type": "object",
1342
- "properties": {
1343
- "query": {
1344
- "type": "string",
1345
- "description": "The natural language query to search for. Be specific to get the best results."
1346
- }
1347
- },
1348
- "required": ["query"]
1349
- }
1350
- }
1351
- available_tools = [rag_tool_definition] + mcp_tools
1352
-
1353
- # --- 2. Optional Initial Objectives Extraction ---
1354
- formatted_tools_list = "\n".join([
1355
- f"- Full Tool Name: {t.get('name')}\n Description: {t.get('description')}\n Input Schema: {json.dumps(t.get('input_schema'))}"
1356
- for t in available_tools
1357
- ])
1358
- if build_plan:
1359
- if streaming_callback:
1360
- streaming_callback("Extracting initial objectives...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "objectives_extraction"}, turn_history)
1500
+
1501
+ initial_state_parts = [
1502
+ "### Initial State",
1503
+ "- My goal is to address the user's request.",
1504
+ "- I have not taken any actions yet."
1505
+ ]
1506
+ if images:
1507
+ initial_state_parts.append(f"- The user has provided {len(images)} image(s) for context.")
1508
+ current_scratchpad = "\n".join(initial_state_parts)
1509
+
1510
+ # --- Define Inner Helper Function for Stateful Step Logging ---
1511
+ def log_step(
1512
+ description: str,
1513
+ step_type: str,
1514
+ metadata: Optional[Dict] = None,
1515
+ is_start: bool = True
1516
+ ) -> Optional[str]:
1517
+ """
1518
+ Logs a step start or end, generating a unique ID for correlation.
1519
+ This is an inner function that has access to the `streaming_callback`.
1361
1520
 
1362
- # The enhanced prompt is placed inside the original parenthesis format.
1363
- # The f-strings for tool lists and user prompts are preserved.
1521
+ Returns the ID for start events so it can be used for the end event.
1522
+ """
1523
+ if not streaming_callback:
1524
+ return None
1364
1525
 
1365
- obj_prompt = (
1366
- "You are a hyper-efficient and logical project planner. Your sole purpose is to analyze the user's request and create a concise, numbered list of actionable steps to fulfill it.\n\n"
1367
- "Your plan must be the most direct and minimal path to the user's goal.\n\n"
1368
- "**Your Core Directives:**\n\n"
1369
- "1. **Analyze the Request:** Break down the user's prompt into the essential, core tasks required.\n"
1370
- "2. **Evaluate Tools with Extreme Scrutiny:** For each task, determine if a tool is **absolutely necessary**. Do not suggest a tool unless the task is impossible without it.\n"
1371
- "3. **Prioritize Simplicity:** If the request can be answered directly without any tools (e.g., it's a simple question or requires a creative response), your entire plan should be a single step: \"1. Formulate a direct answer to the user's request.\"\n\n"
1372
- "**CRITICAL RULES:**\n"
1373
- "* **DO NOT** add any steps, objectives, or tool uses that were not explicitly required by the user.\n"
1374
- "* **DO NOT** attempt to use a tool just because it is available. Most requests will not require any tools.\n"
1375
- "* **DO NOT** add \"nice-to-have\" or \"extra\" tasks. Stick strictly to the request.\n\n"
1376
- "Your final output must be a short, numbered list of steps. Do not call any tools in this planning phase.\n\n"
1377
- "---\n"
1378
- "**Available Tools:**\n"
1379
- f"{formatted_tools_list}\n\n"
1380
- "**User Request:**\n"
1381
- f'"{original_user_prompt}"'
1382
- )
1383
- initial_objectives_gen = self.generate_text(prompt=obj_prompt, system_prompt=objective_extraction_system_prompt, temperature=0.0, stream=False)
1384
- current_objectives = self.remove_thinking_blocks(initial_objectives_gen).strip()
1526
+ event_id = str(uuid.uuid4()) if is_start else None
1385
1527
 
1386
- if streaming_callback:
1387
- streaming_callback(f"Initial Objectives:\n{current_objectives}", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "objectives_extraction"}, turn_history)
1388
- else:
1389
- current_objectives = f"Fulfill the user's request: '{original_user_prompt}'"
1390
-
1391
- turn_history.append({"type": "initial_objectives", "content": current_objectives})
1392
-
1528
+ params = {"type": step_type, "description": description, **(metadata or {})}
1529
+
1530
+ if is_start:
1531
+ params["id"] = event_id
1532
+ streaming_callback(description, MSG_TYPE.MSG_TYPE_STEP_START, params)
1533
+ return event_id
1534
+ else:
1535
+ if 'id' in params:
1536
+ streaming_callback(description, MSG_TYPE.MSG_TYPE_STEP_END, params)
1537
+ else: # Fallback for simple, non-duration steps
1538
+ streaming_callback(description, MSG_TYPE.MSG_TYPE_STEP, params)
1539
+ return None
1540
+
1541
+ # --- 1. Discover Available Tools ---
1542
+ available_tools = []
1543
+ if use_mcps and self.mcp:
1544
+ available_tools.extend(self.mcp.discover_tools(force_refresh=True))
1545
+ if use_data_store:
1546
+ for store_name in use_data_store:
1547
+ available_tools.append({
1548
+ "name": f"research::{store_name}",
1549
+ "description": f"Queries the '{store_name}' knowledge base for relevant information.",
1550
+ "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
1551
+ })
1552
+
1553
+ formatted_tools_list = "\n".join([f"- {t['name']}: {t['description']}" for t in available_tools])
1554
+ formatted_tools_list += "\n- request_clarification: Use if the user's request is ambiguous."
1555
+ formatted_tools_list += "\n- final_answer: Use when you are ready to respond to the user."
1393
1556
 
1557
+ # --- 2. Dynamic Reasoning Loop ---
1558
+ for i in range(max_reasoning_steps):
1559
+ reasoning_step_id = log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", is_start=True)
1394
1560
 
1395
- # --- 3. Main Agent Loop ---
1396
- while llm_iterations < max_llm_iterations:
1397
- llm_iterations += 1
1398
- if streaming_callback:
1399
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1561
+ user_context = f'Original User Request: "{original_user_prompt}"'
1562
+ if images:
1563
+ user_context += f'\n(Note: {len(images)} image(s) were provided with this request.)'
1400
1564
 
1401
- # Format agent history for the prompt
1402
- formatted_agent_history = "No actions taken yet."
1403
- if agent_work_history:
1404
- history_parts = []
1405
- for i, entry in enumerate(agent_work_history):
1406
- history_parts.append(
1407
- f"### Step {i+1}:\n"
1408
- f"**Thought:** {entry['thought']}\n"
1409
- f"**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n"
1410
- f"**Observation:**\n```json\n{json.dumps(entry['tool_result'], indent=2)}\n```"
1411
- )
1412
- formatted_agent_history = "\n\n".join(history_parts)
1413
-
1414
- # Construct the "Thinking & Planning" prompt
1415
- decision_prompt_template = f"""You are a strategic AI assistant. Your goal is to achieve a set of objectives by intelligently using research and system tools.
1565
+ reasoning_prompt_template = f"""You are a logical AI assistant. Your task is to achieve the user's goal by thinking step-by-step and using the available tools.
1416
1566
 
1417
1567
  --- AVAILABLE TOOLS ---
1418
1568
  {formatted_tools_list}
1419
-
1420
- --- CURRENT STATE ---
1421
- Original User Request: {original_user_prompt}
1422
- Current Research Objectives:
1423
- {current_objectives}
1424
-
1425
- Knowledge Scratchpad (our current understanding):
1426
- {knowledge_scratchpad}
1427
-
1428
- --- AGENT WORK HISTORY (previous steps in this turn) ---
1429
- {formatted_agent_history}
1430
-
1431
- --- INSTRUCTIONS ---
1432
- 1. **Analyze:** Review the entire work history, objectives, and scratchpad.
1433
- 2. **Update State:** Based on the latest observations, update the scratchpad and refine the objectives. The scratchpad should be a comprehensive summary of ALL knowledge gathered.
1434
- 3. **Decide Next Action:** Choose ONE of the following: `call_tool`, `final_answer`, or `clarify`. Always prefer to gather information with `research::query_database` before attempting to use other tools if you lack context.
1435
-
1436
- --- OUTPUT FORMAT ---
1437
- Respond with a single JSON object inside a JSON markdown tag. Use this exact schema:
1438
- ```json
1439
- {{
1440
- "thought": "Your reasoning for the chosen action, analyzing how the work history informs your next step. Explain why you are choosing a specific tool (or to answer).",
1441
- "updated_scratchpad": "The new, complete, and comprehensive summary of all knowledge gathered. Integrate new findings with old ones. if no new knowledge is gathered, this should be an empty string.",
1442
- "updated_objectives": "The full, potentially revised, list of objectives. If no change, repeat the current list.",
1443
- "action": "The chosen action: 'call_tool', 'final_answer', or 'clarify'.",
1444
- "tool_name": "(string, if action is 'call_tool') The full 'alias::tool_name' of the tool to use.",
1445
- "tool_params": {{"query": "...", "param2": "..."}},
1446
- "clarification_request": "(string, if action is 'clarify') Your question to the user."
1447
- }}
1448
- ```
1569
+ --- CONTEXT ---
1570
+ {user_context}
1571
+ --- YOUR INTERNAL SCRATCHPAD (Work History & Analysis) ---
1572
+ {current_scratchpad}
1573
+ --- END OF SCRATCHPAD ---
1574
+
1575
+ **INSTRUCTIONS:**
1576
+ 1. **OBSERVE:** Review the `Observation` from your most recent step in the scratchpad.
1577
+ 2. **THINK:**
1578
+ - Does the latest observation completely fulfill the user's original request?
1579
+ - If YES, your next action MUST be to use the `final_answer` tool.
1580
+ - If NO, what is the single next logical step needed?
1581
+ - If you are stuck or the request is ambiguous, use `request_clarification`.
1582
+ 3. **ACT:** Formulate your decision as a JSON object.
1449
1583
  """
1450
- raw_llm_decision_json = self.generate_text(
1451
- prompt=decision_prompt_template, n_predict=2048, temperature=tool_call_decision_temperature
1584
+ action_template = {
1585
+ "thought": "My detailed analysis of the last observation and my reasoning for the next action.",
1586
+ "action": {
1587
+ "tool_name": "The single tool to use (e.g., 'time_machine::get_current_time', 'final_answer').",
1588
+ "tool_params": {"param1": "value1"},
1589
+ "clarification_question": "(string, ONLY if tool_name is 'request_clarification')"
1590
+ }
1591
+ }
1592
+
1593
+ structured_action_response = self.generate_code(
1594
+ prompt=reasoning_prompt_template,
1595
+ template=json.dumps(action_template, indent=2),
1596
+ system_prompt=reasoning_system_prompt,
1597
+ temperature=decision_temperature,
1598
+ images=images if i == 0 else None
1452
1599
  )
1453
1600
 
1454
- # --- 4. Parse LLM's plan and update state ---
1455
1601
  try:
1456
- llm_decision = robust_json_parser(raw_llm_decision_json)
1457
- turn_history.append({"type": "llm_plan", "content": llm_decision})
1458
-
1459
- current_objectives = llm_decision.get("updated_objectives", current_objectives)
1460
- new_scratchpad = llm_decision.get("updated_scratchpad")
1461
-
1462
- if new_scratchpad and new_scratchpad != knowledge_scratchpad:
1463
- knowledge_scratchpad = new_scratchpad
1464
- if streaming_callback:
1465
- streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP, {"id": "scratchpad_update"}, turn_history)
1466
- streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id":"scratch_pad_update"}, turn_history)
1467
-
1468
- except (json.JSONDecodeError, AttributeError, KeyError) as e:
1469
- ASCIIColors.error(f"Failed to parse LLM decision JSON: {raw_llm_decision_json}. Error: {e}")
1470
- turn_history.append({"type": "error", "content": f"Failed to parse LLM plan: {raw_llm_decision_json}"})
1602
+ action_data = json.loads(structured_action_response)
1603
+ thought = action_data.get("thought", "No thought was generated.")
1604
+ action = action_data.get("action", {})
1605
+ tool_name = action.get("tool_name")
1606
+ tool_params = action.get("tool_params", {})
1607
+ except (json.JSONDecodeError, TypeError) as e:
1608
+ current_scratchpad += f"\n\n### Step {i+1} Failure\n- **Error:** Failed to generate a valid JSON action: {e}"
1609
+ if reasoning_step_id:
1610
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id, "error": str(e)}, is_start=False)
1471
1611
  break
1472
1612
 
1473
- if streaming_callback:
1474
- streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history)
1475
-
1476
- # --- 5. Execute the chosen action ---
1477
- action = llm_decision.get("action")
1478
- tool_result = None
1479
-
1480
- if action == "call_tool":
1481
- if len(tool_calls_made_this_turn) >= max_tool_calls:
1482
- ASCIIColors.warning("Max tool calls reached. Forcing final answer.")
1483
- break
1613
+ current_scratchpad += f"\n\n### Step {i+1}: Thought\n{thought}"
1614
+ if streaming_callback:
1615
+ streaming_callback(thought, MSG_TYPE.MSG_TYPE_INFO, {"type": "thought"})
1484
1616
 
1485
- tool_name = llm_decision.get("tool_name")
1486
- tool_params = llm_decision.get("tool_params", {})
1617
+ if not tool_name:
1618
+ current_scratchpad += f"\n\n### Step {i+1} Failure\n- **Error:** Did not specify a tool name."
1619
+ if reasoning_step_id:
1620
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1621
+ break
1487
1622
 
1488
- if not tool_name or not isinstance(tool_params, dict):
1489
- ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
1490
- break
1623
+ if tool_name == "request_clarification":
1624
+ clarification_question = action.get("clarification_question", "Could you please provide more details?")
1625
+ current_scratchpad += f"\n\n### Step {i+1}: Action\n- **Action:** Decided to request clarification.\n- **Question:** {clarification_question}"
1626
+ if reasoning_step_id:
1627
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1628
+ return {"final_answer": clarification_question, "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1629
+
1630
+ if tool_name == "final_answer":
1631
+ current_scratchpad += f"\n\n### Step {i+1}: Action\n- **Action:** Decided to formulate the final answer."
1632
+ if reasoning_step_id:
1633
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1634
+ break
1491
1635
 
1492
- if streaming_callback:
1493
- streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
1494
-
1495
- try:
1496
- # ** DYNAMIC TOOL/RAG DISPATCH **
1497
- if tool_name == "research::query_database":
1498
- query = tool_params.get("query")
1499
- if not query:
1500
- tool_result = {"error": "RAG tool called without a 'query' parameter."}
1501
- else:
1502
- retrieved_chunks = rag_query_function(query, rag_vectorizer_name, rag_top_k, rag_min_similarity_percent)
1503
- if not retrieved_chunks:
1504
- tool_result = {"summary": "No relevant documents found for the query.", "chunks": []}
1505
- else:
1506
- tool_result = {
1507
- "summary": f"Found {len(retrieved_chunks)} relevant document chunks.",
1508
- "chunks": retrieved_chunks
1509
- }
1636
+ tool_call_id = log_step(f"Executing tool: {tool_name}", "tool_call", metadata={"name": tool_name, "parameters": tool_params}, is_start=True)
1637
+ tool_result = None
1638
+ try:
1639
+ if tool_name.startswith("research::") and use_data_store:
1640
+ store_name = tool_name.split("::")[1]
1641
+ rag_callable = use_data_store.get(store_name, {}).get("callable")
1642
+ query = tool_params.get("query", "")
1643
+ retrieved_chunks = rag_callable(query, rag_top_k=rag_top_k, rag_min_similarity_percent=rag_min_similarity_percent)
1644
+ if retrieved_chunks:
1645
+ sources_this_turn.extend(retrieved_chunks)
1646
+ tool_result = {"status": "success", "summary": f"Found {len(retrieved_chunks)} relevant chunks.", "chunks": retrieved_chunks}
1510
1647
  else:
1511
- # Standard MCP tool execution
1512
- tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1513
-
1514
- except Exception as e_exec:
1515
- trace_exception(e_exec)
1516
- tool_result = {"error": f"An exception occurred while executing tool '{tool_name}': {e_exec}"}
1517
-
1518
- # Record the work cycle in the agent's history
1519
- work_entry = {
1520
- "thought": llm_decision.get("thought", "N/A"),
1521
- "tool_name": tool_name,
1522
- "tool_params": tool_params,
1523
- "tool_result": tool_result
1524
- }
1525
- agent_work_history.append(work_entry)
1526
- tool_calls_made_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1527
-
1528
- if streaming_callback:
1529
- streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
1530
- streaming_callback(json.dumps(tool_result, indent=2), MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, tool_result, turn_history)
1531
-
1532
- elif action == "clarify":
1533
- clarification_request = llm_decision.get("clarification_request", "I need more information. Could you please clarify?")
1534
- return {"final_answer": clarification_request, "tool_calls": tool_calls_made_this_turn, "error": None, "clarification": True}
1535
-
1536
- elif action == "final_answer":
1537
- ASCIIColors.info("LLM decided to formulate a final answer.")
1538
- break
1648
+ tool_result = {"status": "success", "summary": "No relevant documents found."}
1649
+ elif use_mcps and self.mcp:
1650
+ mcp_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1651
+ tool_result = {"status": "success", "output": mcp_result} if not (isinstance(mcp_result, dict) and "error" in mcp_result) else {"status": "failure", **mcp_result}
1652
+ else:
1653
+ tool_result = {"status": "failure", "error": f"Tool '{tool_name}' not found."}
1654
+ except Exception as e:
1655
+ trace_exception(e)
1656
+ tool_result = {"status": "failure", "error": f"Exception executing tool: {str(e)}"}
1539
1657
 
1658
+ if tool_call_id:
1659
+ log_step(f"Executing tool: {tool_name}", "tool_call", metadata={"id": tool_call_id, "result": tool_result}, is_start=False)
1660
+
1661
+ observation_text = ""
1662
+ if isinstance(tool_result, dict):
1663
+ sanitized_result = tool_result.copy()
1664
+ summarized_fields = {}
1665
+ for key, value in tool_result.items():
1666
+ if isinstance(value, str) and key.endswith("_base64") and len(value) > 256:
1667
+ sanitized_result[key] = f"[Image was generated. Size: {len(value)} bytes]"
1668
+ continue
1669
+ if isinstance(value, str) and len(self.tokenize(value)) > output_summarization_threshold:
1670
+ if streaming_callback: streaming_callback(f"Summarizing long output from field '{key}'...", MSG_TYPE.MSG_TYPE_STEP, {"type": "summarization"})
1671
+ summary = self.sequential_summarize(text=value, chunk_processing_prompt=f"Summarize key info from this chunk of '{key}'.", callback=streaming_callback)
1672
+ summarized_fields[key] = summary
1673
+ sanitized_result[key] = f"[Content summarized, see summary below. Original length: {len(value)} chars]"
1674
+ observation_text = f"```json\n{json.dumps(sanitized_result, indent=2)}\n```"
1675
+ if summarized_fields:
1676
+ observation_text += "\n\n**Summaries of Long Outputs:**"
1677
+ for key, summary in summarized_fields.items():
1678
+ observation_text += f"\n- **Summary of '{key}':**\n{summary}"
1540
1679
  else:
1541
- ASCIIColors.warning(f"LLM returned unknown or missing action: '{action}'. Forcing final answer.")
1542
- break
1543
- if streaming_callback:
1544
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1545
-
1546
- if streaming_callback:
1547
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1548
- # --- 6. Generate Final Answer ---
1549
- if streaming_callback:
1550
- streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history)
1551
-
1552
- final_answer_prompt = f"""You are an AI assistant providing a final, comprehensive answer based on research and tool use.
1680
+ observation_text = f"Tool returned non-dictionary output: {str(tool_result)}"
1553
1681
 
1554
- --- CONTEXT ---
1555
- Original User Request: "{original_user_prompt}"
1556
-
1557
- --- SUMMARY OF FINDINGS (Knowledge Scratchpad) ---
1558
- {knowledge_scratchpad}
1682
+ tool_calls_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1683
+ current_scratchpad += f"\n\n### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n- **Result:**\n{observation_text}"
1684
+ log_step("{"+'"scratchpad":"'+current_scratchpad+'"}', "scratchpad", is_start=False)
1685
+
1686
+ if reasoning_step_id:
1687
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1559
1688
 
1689
+ # --- Final Answer Synthesis ---
1690
+ synthesis_id = log_step("Synthesizing final answer...", "final_answer_synthesis", is_start=True)
1691
+
1692
+ final_answer_prompt = f"""You are an AI assistant. Provide a final, comprehensive answer based on your work.
1693
+ --- Original User Request ---
1694
+ "{original_user_prompt}"
1695
+ --- Your Internal Scratchpad (Actions Taken & Findings) ---
1696
+ {current_scratchpad}
1560
1697
  --- INSTRUCTIONS ---
1561
- - Synthesize a clear, complete answer for the user based ONLY on the information in the 'Summary of Findings'.
1562
- - Address the user directly and answer their original request.
1563
- - Do not make up information. If the findings are insufficient, state what you found and what remains unanswered.
1698
+ - Synthesize a clear and friendly answer for the user based ONLY on your scratchpad.
1699
+ - If images were provided by the user, incorporate your analysis of them into the answer.
1700
+ - Do not talk about your internal process unless it's necessary to explain why you couldn't find an answer.
1564
1701
  """
1565
- final_answer_text = self.generate_text(
1566
- prompt=final_answer_prompt,
1567
- system_prompt=system_prompt,
1568
- images=images,
1569
- stream=streaming_callback is not None,
1570
- streaming_callback=streaming_callback,
1571
- temperature=final_answer_temperature if final_answer_temperature is not None else self.default_temperature,
1572
- **(llm_generation_kwargs or {})
1573
- )
1574
-
1575
- if streaming_callback:
1576
- streaming_callback("Final answer generation complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "final_answer_synthesis"}, turn_history)
1577
-
1702
+ final_answer_text = self.generate_text(prompt=final_answer_prompt, system_prompt=system_prompt, images=images, stream=streaming_callback is not None, streaming_callback=streaming_callback, temperature=final_answer_temperature, **llm_generation_kwargs)
1578
1703
  final_answer = self.remove_thinking_blocks(final_answer_text)
1579
- turn_history.append({"type":"final_answer_generated", "content": final_answer})
1580
-
1581
- return {"final_answer": final_answer, "tool_calls": tool_calls_made_this_turn, "error": None}
1582
1704
 
1705
+ if synthesis_id:
1706
+ log_step("Synthesizing final answer...", "final_answer_synthesis", metadata={"id": synthesis_id}, is_start=False)
1707
+
1708
+ return {
1709
+ "final_answer": final_answer,
1710
+ "final_scratchpad": current_scratchpad,
1711
+ "tool_calls": tool_calls_this_turn,
1712
+ "sources": sources_this_turn,
1713
+ "clarification_required": False,
1714
+ "error": None
1715
+ }
1716
+
1583
1717
  def generate_code(
1584
1718
  self,
1585
1719
  prompt,