lollms-client 0.23.0__py3-none-any.whl → 0.24.1__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
  """
@@ -836,7 +836,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
836
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])
837
837
 
838
838
  if streaming_callback:
839
- 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)
840
840
 
841
841
  obj_prompt = (
842
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"
@@ -864,8 +864,8 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
864
864
  current_plan = self.remove_thinking_blocks(initial_plan_gen).strip()
865
865
 
866
866
  if streaming_callback:
867
- streaming_callback("Building initial plan...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "plan_extraction"}, turn_history)
868
- 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)
869
869
  turn_history.append({"type": "initial_plan", "content": current_plan})
870
870
 
871
871
  tool_calls_made_this_turn = []
@@ -873,7 +873,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
873
873
 
874
874
  while llm_iterations < max_llm_iterations:
875
875
  llm_iterations += 1
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)
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)
877
877
 
878
878
  formatted_agent_history = "No actions taken yet in this turn."
879
879
  if agent_work_history:
@@ -897,7 +897,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
897
897
  except (json.JSONDecodeError, AttributeError, KeyError) as e:
898
898
  error_message = f"JSON parsing failed (Attempt {i+1}/{max_json_retries+1}). Error: {e}"
899
899
  ASCIIColors.warning(error_message)
900
- 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)
901
901
  turn_history.append({"type": "error", "content": f"Invalid JSON response: {raw_llm_decision_json}"})
902
902
  if i >= max_json_retries:
903
903
  ASCIIColors.error("Max JSON retries reached. Aborting agent loop.")
@@ -919,7 +919,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
919
919
  current_plan = llm_decision.get("updated_plan", current_plan)
920
920
  action = llm_decision.get("action")
921
921
  action_details = llm_decision.get("action_details", {})
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)
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)
923
923
 
924
924
  if action == "call_tool":
925
925
  if len(tool_calls_made_this_turn) >= max_tool_calls:
@@ -931,18 +931,18 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
931
931
  ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
932
932
  break
933
933
 
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)
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)
935
935
  tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
936
936
  if streaming_callback:
937
- streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
938
- 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)
939
939
 
940
- 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)
941
941
  new_scratchpad = self._synthesize_knowledge(previous_scratchpad=knowledge_scratchpad, tool_name=tool_name, tool_params=tool_params, tool_result=tool_result)
942
942
  knowledge_scratchpad = new_scratchpad
943
943
  if streaming_callback:
944
- streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"synthesis_step_{llm_iterations}"}, turn_history)
945
- 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)
946
946
 
947
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 }
948
948
  agent_work_history.append(work_entry)
@@ -961,12 +961,12 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
961
961
  break
962
962
 
963
963
  if streaming_callback:
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)
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)
965
965
 
966
966
  if streaming_callback:
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)
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)
968
968
  if streaming_callback:
969
- 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)
970
970
 
971
971
  final_answer_prompt = (
972
972
  "You are an AI assistant tasked with providing a final, comprehensive answer to the user based on the research performed.\n\n"
@@ -983,7 +983,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
983
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 {}))
984
984
 
985
985
  if streaming_callback:
986
- 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)
987
987
 
988
988
  final_answer = self.remove_thinking_blocks(final_answer_text)
989
989
  turn_history.append({"type":"final_answer_generated", "content": final_answer})
@@ -1434,317 +1434,290 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1434
1434
  new_scratchpad_text = self.generate_text(prompt=synthesis_prompt, n_predict=1024, temperature=0.0)
1435
1435
  return self.remove_thinking_blocks(new_scratchpad_text).strip()
1436
1436
 
1437
+ # In lollms_client/lollms_discussion.py -> LollmsClient class
1438
+
1437
1439
  def generate_with_mcp_rag(
1438
1440
  self,
1439
1441
  prompt: str,
1440
1442
  use_mcps: Union[None, bool, List[str]] = None,
1441
1443
  use_data_store: Union[None, Dict[str, Callable]] = None,
1442
1444
  system_prompt: str = None,
1443
- objective_extraction_system_prompt="Extract objectives",
1445
+ reasoning_system_prompt: str = "You are a logical and adaptive AI assistant.",
1444
1446
  images: Optional[List[str]] = None,
1445
- max_tool_calls: int = 10,
1446
- max_llm_iterations: int = 15,
1447
- tool_call_decision_temperature: float = 0.0,
1447
+ max_reasoning_steps: int = 10,
1448
+ decision_temperature: float = 0.0,
1448
1449
  final_answer_temperature: float = None,
1449
- streaming_callback: Optional[Callable[[str, MSG_TYPE, Optional[Dict], Optional[List]], bool]] = None,
1450
- build_plan: bool = True,
1450
+ streaming_callback: Optional[Callable[[str, 'MSG_TYPE', Optional[Dict], Optional[List]], bool]] = None,
1451
1451
  rag_top_k: int = 5,
1452
1452
  rag_min_similarity_percent: float = 70.0,
1453
+ output_summarization_threshold: int = 500, # In tokens
1453
1454
  **llm_generation_kwargs
1454
1455
  ) -> Dict[str, Any]:
1455
- """
1456
- Generates a response using a stateful agent that can choose between calling standard
1457
- MCP tools and querying one or more RAG databases, 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.
1458
1492
  """
1459
1493
  if not self.binding:
1460
- return {"final_answer": "", "tool_calls": [], "error": "LLM binding not initialized."}
1494
+ return {"final_answer": "", "tool_calls": [], "sources": [], "error": "LLM binding not initialized."}
1461
1495
 
1462
1496
  # --- Initialize Agent State ---
1463
- turn_history: List[Dict[str, Any]] = []
1497
+ sources_this_turn: List[Dict[str, Any]] = []
1498
+ tool_calls_this_turn: List[Dict[str, Any]] = []
1464
1499
  original_user_prompt = prompt
1465
- knowledge_scratchpad = "No information gathered yet."
1466
- current_objectives = ""
1467
- agent_work_history = []
1468
- tool_calls_made_this_turn = []
1469
- llm_iterations = 0
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`.
1520
+
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
1470
1525
 
1471
- # --- 1. Discover Available Tools (MCP and RAG) ---
1526
+ event_id = str(uuid.uuid4()) if is_start else None
1527
+
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 ---
1472
1542
  available_tools = []
1473
-
1474
- # Discover MCP tools if requested
1475
1543
  if use_mcps and self.mcp:
1476
- discovered_mcp_tools = self.mcp.discover_tools(force_refresh=True)
1477
- if isinstance(use_mcps, list):
1478
- # Filter for specific MCP tools
1479
- available_tools.extend([t for t in discovered_mcp_tools if t['name'] in use_mcps])
1480
- else: # use_mcps is True
1481
- available_tools.extend(discovered_mcp_tools)
1482
-
1483
- # Define and add RAG tools if requested
1544
+ available_tools.extend(self.mcp.discover_tools(force_refresh=True))
1484
1545
  if use_data_store:
1485
- for store_name, _ in use_data_store.items():
1486
- rag_tool_definition = {
1546
+ for store_name in use_data_store:
1547
+ available_tools.append({
1487
1548
  "name": f"research::{store_name}",
1488
- "description": (
1489
- f"Queries the '{store_name}' information database to find relevant text chunks based on a natural language query. "
1490
- "Use this to gather information, answer questions, or find context for a task before using other tools."
1491
- ),
1492
- "input_schema": {
1493
- "type": "object",
1494
- "properties": {
1495
- "query": {
1496
- "type": "string",
1497
- "description": "The natural language query to search for. Be specific to get the best results."
1498
- }
1499
- },
1500
- "required": ["query"]
1501
- }
1502
- }
1503
- available_tools.append(rag_tool_definition)
1549
+ "description": f"Queries the '{store_name}' knowledge base for relevant information.",
1550
+ "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
1551
+ })
1504
1552
 
1505
- if not available_tools:
1506
- # If no tools are available, just do a simple text generation
1507
- final_answer_text = self.generate_text(prompt=prompt, system_prompt=system_prompt, stream=streaming_callback is not None, streaming_callback=streaming_callback)
1508
- return {"final_answer": self.remove_thinking_blocks(final_answer_text), "tool_calls": [], "error": None}
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."
1509
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)
1510
1560
 
1511
- formatted_tools_list = "\n".join([
1512
- f"- Full Tool Name: {t.get('name')}\n Description: {t.get('description')}\n Input Schema: {json.dumps(t.get('input_schema'))}"
1513
- for t in available_tools
1514
- ])
1515
-
1516
- # --- 2. Optional Initial Objectives Extraction ---
1517
- if build_plan:
1518
- if streaming_callback:
1519
- streaming_callback("Extracting initial objectives...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "objectives_extraction"}, 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.)'
1520
1564
 
1521
- obj_prompt = (
1522
- "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"
1523
- "Your plan must be the most direct and minimal path to the user's goal.\n\n"
1524
- "**Your Core Directives:**\n\n"
1525
- "1. **Analyze the Request:** Break down the user's prompt into the essential, core tasks required.\n"
1526
- "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"
1527
- "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"
1528
- "**CRITICAL RULES:**\n"
1529
- "* **DO NOT** add any steps, objectives, or tool uses that were not explicitly required by the user.\n"
1530
- "* **DO NOT** attempt to use a tool just because it is available. Most requests will not require any tools.\n"
1531
- "* **DO NOT** add \"nice-to-have\" or \"extra\" tasks. Stick strictly to the request.\n\n"
1532
- "Your final output must be a short, numbered list of steps. Do not call any tools in this planning phase.\n\n"
1533
- "---\n"
1534
- "**Available Tools:**\n"
1535
- f"{formatted_tools_list}\n\n"
1536
- "**User Request:**\n"
1537
- f'"{original_user_prompt}"'
1538
- )
1539
- initial_objectives_gen = self.generate_text(prompt=obj_prompt, system_prompt=objective_extraction_system_prompt, temperature=0.0, stream=False)
1540
- current_objectives = self.remove_thinking_blocks(initial_objectives_gen).strip()
1541
-
1542
- if streaming_callback:
1543
- streaming_callback(f"Initial Objectives:\n{current_objectives}", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "objectives_extraction"}, turn_history)
1544
- else:
1545
- current_objectives = f"Fulfill the user's request: '{original_user_prompt}'"
1546
-
1547
- turn_history.append({"type": "initial_objectives", "content": current_objectives})
1548
-
1549
- # --- 3. Main Agent Loop ---
1550
- while llm_iterations < max_llm_iterations:
1551
- llm_iterations += 1
1552
- if streaming_callback:
1553
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1554
-
1555
- # Format agent history for the prompt
1556
- formatted_agent_history = "No actions taken yet."
1557
- if agent_work_history:
1558
- history_parts = []
1559
- for i, entry in enumerate(agent_work_history):
1560
- # Sanitize the result for history display, similar to knowledge synthesis
1561
- sanitized_hist_result = entry['tool_result'].copy()
1562
- if 'image_base64' in sanitized_hist_result: sanitized_hist_result.pop('image_base64')
1563
- if 'content' in sanitized_hist_result and len(sanitized_hist_result['content']) > 200: sanitized_hist_result['content'] = sanitized_hist_result['content'][:200] + '... (truncated)'
1564
-
1565
- history_parts.append(
1566
- f"### Step {i+1}:\n"
1567
- f"**Thought:** {entry['thought']}\n"
1568
- f"**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n"
1569
- f"**Observation:**\n```json\n{json.dumps(sanitized_hist_result, indent=2)}\n```"
1570
- )
1571
- formatted_agent_history = "\n\n".join(history_parts)
1572
-
1573
- 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.
1574
1566
 
1575
1567
  --- AVAILABLE TOOLS ---
1576
1568
  {formatted_tools_list}
1577
-
1578
- --- CURRENT STATE ---
1579
- Original User Request: {original_user_prompt}
1580
- Current Research Objectives:
1581
- {current_objectives}
1582
-
1583
- Knowledge Scratchpad (our current understanding):
1584
- {knowledge_scratchpad}
1585
-
1586
- --- AGENT WORK HISTORY (previous steps in this turn) ---
1587
- {formatted_agent_history}
1588
-
1589
- --- INSTRUCTIONS ---
1590
- 1. **Analyze:** Review the entire work history, objectives, and scratchpad.
1591
- 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.
1592
- 3. **Decide Next Action:** Choose ONE of the following: `call_tool`, `final_answer`, or `clarify`. Always prefer to gather information with a `research::` tool before attempting to use other tools if you lack context.
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.
1593
1583
  """
1594
- decision_template = {
1595
- "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).",
1596
- "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.",
1597
- "updated_objectives": "The full, potentially revised, list of objectives. If no change, repeat the current list.",
1598
- "action": "The chosen action: 'call_tool', 'final_answer', or 'clarify'.",
1599
- "action_details": {
1600
- "tool_name": "(string, if action is 'call_tool') The full 'alias::tool_name' of the tool to use.",
1601
- "tool_params": {"query": "...", "param2": "..."},
1602
- "clarification_request": "(string, if action is 'clarify') Your question to the user."
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')"
1603
1590
  }
1604
1591
  }
1605
- llm_decision = self.generate_structured_content(
1606
- prompt=decision_prompt_template,
1607
- template=decision_template,
1608
- temperature=tool_call_decision_temperature
1609
- )
1610
-
1611
- if not llm_decision:
1612
- ASCIIColors.error("LLM failed to generate a valid decision JSON. Aborting loop.")
1613
- break
1614
-
1615
- # --- 4. Parse LLM's plan and update state ---
1616
- turn_history.append({"type": "llm_plan", "content": llm_decision})
1617
-
1618
- current_objectives = llm_decision.get("updated_objectives", current_objectives)
1619
- new_scratchpad = llm_decision.get("updated_scratchpad")
1620
1592
 
1621
- if new_scratchpad and new_scratchpad.strip() and new_scratchpad != knowledge_scratchpad:
1622
- knowledge_scratchpad = new_scratchpad
1623
- if streaming_callback:
1624
- streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP, {"id": "scratchpad_update"}, turn_history)
1625
- streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id":"scratch_pad_update"}, turn_history)
1626
-
1627
- if streaming_callback:
1628
- streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history)
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
1599
+ )
1629
1600
 
1630
- # --- 5. Execute the chosen action ---
1631
- action = llm_decision.get("action")
1632
- action_details = llm_decision.get("action_details", {})
1633
- tool_result = None
1601
+ try:
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
+ log_step(f"\n\n### Step {i+1} Failure\n- **Error:** Failed to generate a valid JSON action: {e}", "scratchpad", is_start=False)
1610
+ if reasoning_step_id:
1611
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id, "error": str(e)}, is_start=False)
1612
+ break
1634
1613
 
1635
- if action == "call_tool":
1636
- if len(tool_calls_made_this_turn) >= max_tool_calls:
1637
- ASCIIColors.warning("Max tool calls reached. Forcing final answer.")
1638
- break
1614
+ current_scratchpad += f"\n\n### Step {i+1}: Thought\n{thought}"
1615
+ log_step(f"\n\n### Step {i+1}: Thought\n{thought}", "scratchpad", is_start=False)
1616
+ if streaming_callback:
1617
+ streaming_callback(thought, MSG_TYPE.MSG_TYPE_INFO, {"type": "thought"})
1639
1618
 
1640
- tool_name = action_details.get("tool_name")
1641
- tool_params = action_details.get("tool_params", {})
1619
+ if not tool_name:
1620
+ current_scratchpad += f"\n\n### Step {i+1} Failure\n- **Error:** Did not specify a tool name."
1621
+ log_step(f"\n\n### Step {i+1} Failure\n- **Error:** Did not specify a tool name.", "scratchpad", is_start=False)
1622
+ if reasoning_step_id:
1623
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1624
+ break
1642
1625
 
1643
- if not tool_name or not isinstance(tool_params, dict):
1644
- ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
1645
- break
1626
+ if tool_name == "request_clarification":
1627
+ clarification_question = action.get("clarification_question", "Could you please provide more details?")
1628
+ current_scratchpad += f"\n\n### Step {i+1}: Action\n- **Action:** Decided to request clarification.\n- **Question:** {clarification_question}"
1629
+ log_step(f"\n\n### Step {i+1}: Action\n- **Action:** Decided to request clarification.\n- **Question:** {clarification_question}", "scratchpad", is_start=False)
1630
+ if reasoning_step_id:
1631
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1632
+ return {"final_answer": clarification_question, "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1633
+
1634
+ if tool_name == "final_answer":
1635
+ current_scratchpad += f"\n\n### Step {i+1}: Action\n- **Action:** Decided to formulate the final answer."
1636
+ log_step(f"\n\n### Step {i+1}: Action\n- **Action:** Decided to formulate the final answer.", "scratchpad", is_start=False)
1637
+ if reasoning_step_id:
1638
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1639
+ break
1646
1640
 
1647
- if streaming_callback:
1648
- streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}", "tool_name": tool_name}, turn_history)
1649
-
1650
- try:
1651
- # ** DYNAMIC TOOL/RAG DISPATCH **
1652
- if tool_name.startswith("research::") and use_data_store:
1653
- store_name = tool_name.split("::")[1]
1654
- rag_query_function_local = use_data_store.get(store_name)
1655
- if not rag_query_function_local:
1656
- tool_result = {"error": f"RAG data store '{store_name}' not found or provided."}
1657
- else:
1658
- query = tool_params.get("query")
1659
- if not query:
1660
- tool_result = {"error": "RAG tool called without a 'query' parameter."}
1661
- else:
1662
- retrieved_chunks = rag_query_function_local.get("callable", lambda: {'Search error'})(query, rag_top_k, rag_min_similarity_percent)
1663
- if not retrieved_chunks:
1664
- tool_result = {"summary": "No relevant documents found for the query.", "chunks": []}
1665
- else:
1666
- tool_result = {
1667
- "summary": f"Found {len(retrieved_chunks)} relevant document chunks.",
1668
- "chunks": retrieved_chunks
1669
- }
1670
- elif use_mcps and self.mcp:
1671
- # Standard MCP tool execution
1672
- tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1641
+ tool_call_id = log_step(f"Executing tool: {tool_name}", "tool_call", metadata={"name": tool_name, "parameters": tool_params}, is_start=True)
1642
+ tool_result = None
1643
+ try:
1644
+ if tool_name.startswith("research::") and use_data_store:
1645
+ store_name = tool_name.split("::")[1]
1646
+ rag_callable = use_data_store.get(store_name, {}).get("callable")
1647
+ query = tool_params.get("query", "")
1648
+ retrieved_chunks = rag_callable(query, rag_top_k=rag_top_k, rag_min_similarity_percent=rag_min_similarity_percent)
1649
+ if retrieved_chunks:
1650
+ sources_this_turn.extend(retrieved_chunks)
1651
+ tool_result = {"status": "success", "summary": f"Found {len(retrieved_chunks)} relevant chunks.", "chunks": retrieved_chunks}
1673
1652
  else:
1674
- tool_result = {"error": f"Tool '{tool_name}' cannot be executed. RAG store not found or MCP binding not configured."}
1675
-
1676
- except Exception as e_exec:
1677
- trace_exception(e_exec)
1678
- tool_result = {"error": f"An exception occurred while executing tool '{tool_name}': {e_exec}"}
1679
-
1680
- if streaming_callback:
1681
- streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}", "result": tool_result}, turn_history)
1682
-
1683
- knowledge_scratchpad = self._synthesize_knowledge(knowledge_scratchpad, tool_name, tool_params, tool_result)
1684
- if streaming_callback:
1685
- streaming_callback(f"Knowledge scratchpad updated after {tool_name} call.", MSG_TYPE.MSG_TYPE_INFO, {"id": "scratchpad_update"}, turn_history)
1686
-
1687
- work_entry = {
1688
- "thought": llm_decision.get("thought", "N/A"),
1689
- "tool_name": tool_name,
1690
- "tool_params": tool_params,
1691
- "tool_result": tool_result
1692
- }
1693
- agent_work_history.append(work_entry)
1694
- tool_calls_made_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1695
-
1696
- elif action == "clarify":
1697
- clarification_request = action_details.get("clarification_request", "I need more information. Could you please clarify?")
1698
- return {"final_answer": clarification_request, "tool_calls": tool_calls_made_this_turn, "error": None, "clarification": True}
1699
-
1700
- elif action == "final_answer":
1701
- ASCIIColors.info("LLM decided to formulate a final answer.")
1702
- break
1653
+ tool_result = {"status": "success", "summary": "No relevant documents found."}
1654
+ elif use_mcps and self.mcp:
1655
+ mcp_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1656
+ tool_result = {"status": "success", "output": mcp_result} if not (isinstance(mcp_result, dict) and "error" in mcp_result) else {"status": "failure", **mcp_result}
1657
+ else:
1658
+ tool_result = {"status": "failure", "error": f"Tool '{tool_name}' not found."}
1659
+ except Exception as e:
1660
+ trace_exception(e)
1661
+ tool_result = {"status": "failure", "error": f"Exception executing tool: {str(e)}"}
1703
1662
 
1663
+ if tool_call_id:
1664
+ log_step(f"Executing tool: {tool_name}", "tool_call", metadata={"id": tool_call_id, "result": tool_result}, is_start=False)
1665
+
1666
+ observation_text = ""
1667
+ if isinstance(tool_result, dict):
1668
+ sanitized_result = tool_result.copy()
1669
+ summarized_fields = {}
1670
+ for key, value in tool_result.items():
1671
+ if isinstance(value, str) and key.endswith("_base64") and len(value) > 256:
1672
+ sanitized_result[key] = f"[Image was generated. Size: {len(value)} bytes]"
1673
+ continue
1674
+ if isinstance(value, str) and len(self.tokenize(value)) > output_summarization_threshold:
1675
+ if streaming_callback: streaming_callback(f"Summarizing long output from field '{key}'...", MSG_TYPE.MSG_TYPE_STEP, {"type": "summarization"})
1676
+ summary = self.sequential_summarize(text=value, chunk_processing_prompt=f"Summarize key info from this chunk of '{key}'.", callback=streaming_callback)
1677
+ summarized_fields[key] = summary
1678
+ sanitized_result[key] = f"[Content summarized, see summary below. Original length: {len(value)} chars]"
1679
+ observation_text = f"```json\n{json.dumps(sanitized_result, indent=2)}\n```"
1680
+ if summarized_fields:
1681
+ observation_text += "\n\n**Summaries of Long Outputs:**"
1682
+ for key, summary in summarized_fields.items():
1683
+ observation_text += f"\n- **Summary of '{key}':**\n{summary}"
1704
1684
  else:
1705
- ASCIIColors.warning(f"LLM returned unknown or missing action: '{action}'. Forcing final answer.")
1706
- break
1707
- if streaming_callback:
1708
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1709
-
1710
- if streaming_callback:
1711
- streaming_callback(f"LLM reasoning loop finished.", MSG_TYPE.MSG_TYPE_STEP, {"id": "reasoning_loop_end"}, turn_history)
1712
-
1713
- # --- 6. Generate Final Answer ---
1714
- if streaming_callback:
1715
- streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history)
1685
+ observation_text = f"Tool returned non-dictionary output: {str(tool_result)}"
1716
1686
 
1717
- final_answer_prompt = f"""You are an AI assistant providing a final, comprehensive answer based on research and tool use.
1718
-
1719
- --- CONTEXT ---
1720
- Original User Request: "{original_user_prompt}"
1721
-
1722
- --- SUMMARY OF FINDINGS (Knowledge Scratchpad) ---
1723
- {knowledge_scratchpad}
1687
+ tool_calls_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1688
+ current_scratchpad += f"\n\n### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n- **Result:**\n{observation_text}"
1689
+ log_step(f"### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n", "scratchpad", is_start=False)
1690
+
1691
+ if reasoning_step_id:
1692
+ log_step(f"Reasoning Step {i+1}/{max_reasoning_steps}", "reasoning_step", metadata={"id": reasoning_step_id}, is_start=False)
1724
1693
 
1694
+ # --- Final Answer Synthesis ---
1695
+ synthesis_id = log_step("Synthesizing final answer...", "final_answer_synthesis", is_start=True)
1696
+
1697
+ final_answer_prompt = f"""You are an AI assistant. Provide a final, comprehensive answer based on your work.
1698
+ --- Original User Request ---
1699
+ "{original_user_prompt}"
1700
+ --- Your Internal Scratchpad (Actions Taken & Findings) ---
1701
+ {current_scratchpad}
1725
1702
  --- INSTRUCTIONS ---
1726
- - Synthesize a clear, complete answer for the user based ONLY on the information in the 'Summary of Findings'.
1727
- - Address the user directly and answer their original request.
1728
- - Do not make up information. If the findings are insufficient, state what you found and what remains unanswered.
1703
+ - Synthesize a clear and friendly answer for the user based ONLY on your scratchpad.
1704
+ - If images were provided by the user, incorporate your analysis of them into the answer.
1705
+ - Do not talk about your internal process unless it's necessary to explain why you couldn't find an answer.
1729
1706
  """
1730
- final_answer_text = self.generate_text(
1731
- prompt=final_answer_prompt,
1732
- system_prompt=system_prompt,
1733
- images=images,
1734
- stream=streaming_callback is not None,
1735
- streaming_callback=streaming_callback,
1736
- temperature=final_answer_temperature if final_answer_temperature is not None else self.default_temperature,
1737
- **(llm_generation_kwargs or {})
1738
- )
1707
+ 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)
1708
+ final_answer = self.remove_thinking_blocks(final_answer_text)
1739
1709
 
1740
- if streaming_callback:
1741
- streaming_callback("Final answer generation complete.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": "final_answer_synthesis"}, turn_history)
1710
+ if synthesis_id:
1711
+ log_step("Synthesizing final answer...", "final_answer_synthesis", metadata={"id": synthesis_id}, is_start=False)
1742
1712
 
1743
- final_answer = self.remove_thinking_blocks(final_answer_text)
1744
- turn_history.append({"type":"final_answer_generated", "content": final_answer})
1745
-
1746
- return {"final_answer": final_answer, "tool_calls": tool_calls_made_this_turn, "error": None}
1747
-
1713
+ return {
1714
+ "final_answer": final_answer,
1715
+ "final_scratchpad": current_scratchpad,
1716
+ "tool_calls": tool_calls_this_turn,
1717
+ "sources": sources_this_turn,
1718
+ "clarification_required": False,
1719
+ "error": None
1720
+ }
1748
1721
 
1749
1722
  def generate_code(
1750
1723
  self,