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.
- examples/console_discussion/console_app.py +266 -0
- examples/{run_remote_mcp_example copy.py → mcp_examples/run_remote_mcp_example_v2.py} +65 -1
- lollms_client/__init__.py +1 -1
- lollms_client/lollms_core.py +408 -274
- lollms_client/lollms_discussion.py +599 -294
- lollms_client/lollms_llm_binding.py +3 -0
- lollms_client/lollms_types.py +1 -1
- {lollms_client-0.22.0.dist-info → lollms_client-0.24.0.dist-info}/METADATA +2 -1
- {lollms_client-0.22.0.dist-info → lollms_client-0.24.0.dist-info}/RECORD +16 -16
- {lollms_client-0.22.0.dist-info → lollms_client-0.24.0.dist-info}/top_level.txt +0 -1
- personalities/parrot.py +0 -10
- /examples/{external_mcp.py → mcp_examples/external_mcp.py} +0 -0
- /examples/{local_mcp.py → mcp_examples/local_mcp.py} +0 -0
- /examples/{openai_mcp.py → mcp_examples/openai_mcp.py} +0 -0
- /examples/{run_standard_mcp_example.py → mcp_examples/run_standard_mcp_example.py} +0 -0
- {lollms_client-0.22.0.dist-info → lollms_client-0.24.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.22.0.dist-info → lollms_client-0.24.0.dist-info}/licenses/LICENSE +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
|
"""
|
|
@@ -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],
|
|
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
|
-
|
|
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
|
-
|
|
1445
|
+
reasoning_system_prompt: str = "You are a logical and adaptive AI assistant.",
|
|
1292
1446
|
images: Optional[List[str]] = None,
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1497
|
+
sources_this_turn: List[Dict[str, Any]] = []
|
|
1498
|
+
tool_calls_this_turn: List[Dict[str, Any]] = []
|
|
1316
1499
|
original_user_prompt = prompt
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
tool_result = {"error": f"
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
|
1562
|
-
-
|
|
1563
|
-
- 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.
|
|
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,
|