lollms-client 0.23.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.
- examples/console_discussion/console_app.py +266 -0
- lollms_client/__init__.py +1 -1
- lollms_client/lollms_core.py +249 -281
- lollms_client/lollms_discussion.py +550 -291
- {lollms_client-0.23.0.dist-info → lollms_client-0.24.0.dist-info}/METADATA +2 -1
- {lollms_client-0.23.0.dist-info → lollms_client-0.24.0.dist-info}/RECORD +9 -8
- {lollms_client-0.23.0.dist-info → lollms_client-0.24.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.23.0.dist-info → lollms_client-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.23.0.dist-info → lollms_client-0.24.0.dist-info}/top_level.txt +0 -0
lollms_client/lollms_core.py
CHANGED
|
@@ -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
|
|
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,285 @@ 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
|
-
|
|
1445
|
+
reasoning_system_prompt: str = "You are a logical and adaptive AI assistant.",
|
|
1444
1446
|
images: Optional[List[str]] = None,
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1497
|
+
sources_this_turn: List[Dict[str, Any]] = []
|
|
1498
|
+
tool_calls_this_turn: List[Dict[str, Any]] = []
|
|
1464
1499
|
original_user_prompt = prompt
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1486
|
-
|
|
1546
|
+
for store_name in use_data_store:
|
|
1547
|
+
available_tools.append({
|
|
1487
1548
|
"name": f"research::{store_name}",
|
|
1488
|
-
"description":
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
1595
|
-
"thought": "
|
|
1596
|
-
"
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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
|
+
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)
|
|
1611
|
+
break
|
|
1634
1612
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
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"})
|
|
1639
1616
|
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
|
1642
1622
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
|
1646
1635
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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)
|
|
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}
|
|
1673
1647
|
else:
|
|
1674
|
-
tool_result = {"
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
|
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)}"}
|
|
1703
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}"
|
|
1704
1679
|
else:
|
|
1705
|
-
|
|
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)
|
|
1680
|
+
observation_text = f"Tool returned non-dictionary output: {str(tool_result)}"
|
|
1716
1681
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
{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)
|
|
1724
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}
|
|
1725
1697
|
--- INSTRUCTIONS ---
|
|
1726
|
-
- Synthesize a clear
|
|
1727
|
-
-
|
|
1728
|
-
- Do not
|
|
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.
|
|
1729
1701
|
"""
|
|
1730
|
-
final_answer_text = self.generate_text(
|
|
1731
|
-
|
|
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
|
-
)
|
|
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)
|
|
1703
|
+
final_answer = self.remove_thinking_blocks(final_answer_text)
|
|
1739
1704
|
|
|
1740
|
-
if
|
|
1741
|
-
|
|
1705
|
+
if synthesis_id:
|
|
1706
|
+
log_step("Synthesizing final answer...", "final_answer_synthesis", metadata={"id": synthesis_id}, is_start=False)
|
|
1742
1707
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
+
}
|
|
1748
1716
|
|
|
1749
1717
|
def generate_code(
|
|
1750
1718
|
self,
|