lollms-client 0.21.0__py3-none-any.whl → 0.23.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.

@@ -7,6 +7,26 @@ from pathlib import Path
7
7
  import json
8
8
  from lollms_client import LollmsClient
9
9
  import subprocess
10
+ from typing import Optional, List, Dict, Any
11
+
12
+
13
+ MOCK_KNOWLEDGE_BASE = {
14
+ "python_basics.md": [
15
+ {"chunk_id": 1, "text": "Python is a high-level, interpreted programming language known for its readability and versatility. It was created by Guido van Rossum and first released in 1991."},
16
+ {"chunk_id": 2, "text": "Key features of Python include dynamic typing, automatic memory management (garbage collection), and a large standard library. It supports multiple programming paradigms, such as procedural, object-oriented, and functional programming."},
17
+ {"chunk_id": 3, "text": "Common applications of Python include web development (e.g., Django, Flask), data science (e.g., Pandas, NumPy, Scikit-learn), machine learning, artificial intelligence, automation, and scripting."},
18
+ ],
19
+ "javascript_info.js": [
20
+ {"chunk_id": 1, "text": "JavaScript is a scripting language primarily used for front-end web development to create interactive effects within web browsers. It is also used in back-end development (Node.js), mobile app development, and game development."},
21
+ {"chunk_id": 2, "text": "JavaScript is dynamically typed, prototype-based, and multi-paradigm. Along with HTML and CSS, it is one of the core technologies of the World Wide Web."},
22
+ {"chunk_id": 3, "text": "Popular JavaScript frameworks and libraries include React, Angular, Vue.js for front-end, and Express.js for Node.js back-end applications."},
23
+ ],
24
+ "ai_concepts.txt": [
25
+ {"chunk_id": 1, "text": "Artificial Intelligence (AI) refers to the simulation of human intelligence in machines that are programmed to think like humans and mimic their actions. The term may also be applied to any machine that exhibits traits associated with a human mind such as learning and problem-solving."},
26
+ {"chunk_id": 2, "text": "Machine Learning (ML) is a subset of AI that provides systems the ability to automatically learn and improve from experience without being explicitly programmed. Deep Learning (DL) is a further subset of ML based on artificial neural networks with representation learning."},
27
+ {"chunk_id": 3, "text": "Retrieval Augmented Generation (RAG) is an AI framework for improving the quality of LLM-generated responses by grounding the model on external sources of knowledge to supplement the LLM’s internal representation of information."},
28
+ ]
29
+ }
10
30
  # --- Dynamically adjust Python path to find lollms_client ---
11
31
  # This assumes the example script is in a directory, and 'lollms_client' is
12
32
  # in a sibling directory or a known relative path. Adjust as needed.
@@ -180,10 +200,54 @@ def main():
180
200
  return True # Continue streaming
181
201
 
182
202
  # --- 4. Use generate_with_mcp ---
203
+
204
+ def mock_rag_query_function(
205
+ query_text: str,
206
+ vectorizer_name: Optional[str] = None, # Ignored in mock
207
+ top_k: int = 3,
208
+ min_similarity_percent: float = 0.0 # Ignored in mock, simple keyword match
209
+ ) -> List[Dict[str, Any]]:
210
+ """
211
+ A mock RAG query function.
212
+ Performs a simple keyword search in the MOCK_KNOWLEDGE_BASE.
213
+ """
214
+ ASCIIColors.magenta(f" [MOCK RAG] Querying with: '{query_text}', top_k={top_k}")
215
+ results = []
216
+ query_lower = query_text.lower()
217
+
218
+ all_chunks = []
219
+ for file_path, chunks_in_file in MOCK_KNOWLEDGE_BASE.items():
220
+ for chunk_data in chunks_in_file:
221
+ all_chunks.append({"file_path": file_path, **chunk_data})
222
+
223
+ # Simple keyword matching and scoring (very basic)
224
+ scored_chunks = []
225
+ for chunk_info in all_chunks:
226
+ score = 0
227
+ for keyword in query_lower.split():
228
+ if keyword in chunk_info["text"].lower() and len(keyword)>2: # Basic relevance
229
+ score += 1
230
+ if "python" in query_lower and "python" in chunk_info["file_path"].lower(): score+=5
231
+ if "javascript" in query_lower and "javascript" in chunk_info["file_path"].lower(): score+=5
232
+ if "ai" in query_lower and "ai" in chunk_info["file_path"].lower(): score+=3
233
+
234
+
235
+ if score > 0 : # Only include if some keywords match
236
+ # Simulate similarity percentage (higher score = higher similarity)
237
+ similarity = min(100.0, score * 20.0 + 40.0) # Arbitrary scaling
238
+ if similarity >= min_similarity_percent:
239
+ scored_chunks.append({
240
+ "file_path": chunk_info["file_path"],
241
+ "chunk_text": chunk_info["text"],
242
+ "similarity_percent": similarity,
243
+ "_score_for_ranking": score # Internal score for sorting
244
+ })
183
245
  ASCIIColors.magenta("\n2. Calling generate_with_mcp to get current time...")
184
246
  time_prompt = "Hey assistant, what time is it right now?"
185
- time_response = client.generate_with_mcp(
247
+ time_response = client.generate_with_mcp_rag(
186
248
  prompt=time_prompt,
249
+ use_mcps=True,
250
+ use_data_store={"coding_store":mock_rag_query_function},
187
251
  streaming_callback=mcp_streaming_callback,
188
252
  interactive_tool_execution=False # Set to True to test interactive mode
189
253
  )
lollms_client/__init__.py CHANGED
@@ -1,13 +1,14 @@
1
1
  # lollms_client/__init__.py
2
2
  from lollms_client.lollms_core import LollmsClient, ELF_COMPLETION_FORMAT
3
3
  from lollms_client.lollms_types import MSG_TYPE # Assuming ELF_GENERATION_FORMAT is not directly used by users from here
4
- from lollms_client.lollms_discussion import LollmsDiscussion, DatabaseManager
4
+ from lollms_client.lollms_discussion import LollmsDiscussion, LollmsDataManager, LollmsMessage
5
+ from lollms_client.lollms_personality import LollmsPersonality
5
6
  from lollms_client.lollms_utilities import PromptReshaper # Keep general utilities
6
7
  # Import new MCP binding classes
7
8
  from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
8
9
 
9
10
 
10
- __version__ = "0.21.0" # Updated version
11
+ __version__ = "0.23.0" # Updated version
11
12
 
12
13
  # Optionally, you could define __all__ if you want to be explicit about exports
13
14
  __all__ = [
@@ -15,7 +16,9 @@ __all__ = [
15
16
  "ELF_COMPLETION_FORMAT",
16
17
  "MSG_TYPE",
17
18
  "LollmsDiscussion",
18
- "DatabaseManager",
19
+ "LollmsMessage",
20
+ "LollmsPersonality",
21
+ "LollmsDataManager",
19
22
  "PromptReshaper",
20
23
  "LollmsMCPBinding", # Export LollmsMCPBinding ABC
21
24
  "LollmsMCPBindingManager", # Export LollmsMCPBindingManager
@@ -76,7 +76,9 @@ class LollmsClient():
76
76
  n_threads: int = 8,
77
77
  streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None,
78
78
  user_name ="user",
79
- ai_name = "assistant"):
79
+ ai_name = "assistant",
80
+ **kwargs
81
+ ):
80
82
  """
81
83
  Initialize the LollmsClient with LLM and optional modality bindings.
82
84
 
@@ -524,7 +526,8 @@ class LollmsClient():
524
526
  seed: Optional[int] = None,
525
527
  n_threads: Optional[int] = None,
526
528
  ctx_size: Optional[int] = None,
527
- streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None
529
+ streaming_callback: Optional[Callable[[str, MSG_TYPE, Dict], bool]] = None,
530
+ **kwargs
528
531
  ) -> Union[str, dict]:
529
532
  """
530
533
  High-level method to perform a chat generation using a LollmsDiscussion object.
@@ -556,7 +559,7 @@ class LollmsClient():
556
559
  discussion=discussion,
557
560
  branch_tip_id=branch_tip_id,
558
561
  n_predict=n_predict if n_predict is not None else self.default_n_predict,
559
- 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,
560
563
  temperature=temperature if temperature is not None else self.default_temperature,
561
564
  top_k=top_k if top_k is not None else self.default_top_k,
562
565
  top_p=top_p if top_p is not None else self.default_top_p,
@@ -1281,33 +1284,180 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1281
1284
  "error": None
1282
1285
  }
1283
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
+
1284
1437
  def generate_with_mcp_rag(
1285
1438
  self,
1286
1439
  prompt: str,
1287
- rag_query_function: Callable[[str, Optional[str], int, float], List[Dict[str, Any]]],
1440
+ use_mcps: Union[None, bool, List[str]] = None,
1441
+ use_data_store: Union[None, Dict[str, Callable]] = None,
1288
1442
  system_prompt: str = None,
1289
1443
  objective_extraction_system_prompt="Extract objectives",
1290
1444
  images: Optional[List[str]] = None,
1291
- tools: Optional[List[Dict[str, Any]]] = None,
1292
1445
  max_tool_calls: int = 10,
1293
1446
  max_llm_iterations: int = 15,
1294
1447
  tool_call_decision_temperature: float = 0.0,
1295
1448
  final_answer_temperature: float = None,
1296
1449
  streaming_callback: Optional[Callable[[str, MSG_TYPE, Optional[Dict], Optional[List]], bool]] = None,
1297
1450
  build_plan: bool = True,
1298
- rag_vectorizer_name: Optional[str] = None,
1299
1451
  rag_top_k: int = 5,
1300
1452
  rag_min_similarity_percent: float = 70.0,
1301
1453
  **llm_generation_kwargs
1302
1454
  ) -> Dict[str, Any]:
1303
1455
  """
1304
1456
  Generates a response using a stateful agent that can choose between calling standard
1305
- MCP tools and querying a RAG database, all within a unified reasoning loop.
1457
+ MCP tools and querying one or more RAG databases, all within a unified reasoning loop.
1306
1458
  """
1307
1459
  if not self.binding:
1308
1460
  return {"final_answer": "", "tool_calls": [], "error": "LLM binding not initialized."}
1309
- if not self.mcp:
1310
- return {"final_answer": "", "tool_calls": [], "error": "MCP binding not initialized."}
1311
1461
 
1312
1462
  # --- Initialize Agent State ---
1313
1463
  turn_history: List[Dict[str, Any]] = []
@@ -1318,48 +1468,56 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1318
1468
  tool_calls_made_this_turn = []
1319
1469
  llm_iterations = 0
1320
1470
 
1321
- # --- 1. Discover MCP Tools and Inject the RAG Tool ---
1322
- if tools is None:
1323
- try:
1324
- mcp_tools = self.mcp.discover_tools(force_refresh=True)
1325
- if not mcp_tools: ASCIIColors.warning("No MCP tools discovered.")
1326
- except Exception as e_disc:
1327
- return {"final_answer": "", "tool_calls": [], "error": f"Failed to discover MCP tools: {e_disc}"}
1328
- else:
1329
- mcp_tools = tools
1330
-
1331
- # Define the RAG tool and add it to the list
1332
- rag_tool_definition = {
1333
- "name": "research::query_database",
1334
- "description": (
1335
- "Queries a vector database to find relevant text chunks based on a natural language query. "
1336
- "Use this to gather information, answer questions, or find context for a task before using other tools."
1337
- ),
1338
- "input_schema": {
1339
- "type": "object",
1340
- "properties": {
1341
- "query": {
1342
- "type": "string",
1343
- "description": "The natural language query to search for. Be specific to get the best results."
1471
+ # --- 1. Discover Available Tools (MCP and RAG) ---
1472
+ available_tools = []
1473
+
1474
+ # Discover MCP tools if requested
1475
+ 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
1484
+ if use_data_store:
1485
+ for store_name, _ in use_data_store.items():
1486
+ rag_tool_definition = {
1487
+ "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"]
1344
1501
  }
1345
- },
1346
- "required": ["query"]
1347
- }
1348
- }
1349
- available_tools = [rag_tool_definition] + mcp_tools
1502
+ }
1503
+ available_tools.append(rag_tool_definition)
1504
+
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}
1509
+
1350
1510
 
1351
- # --- 2. Optional Initial Objectives Extraction ---
1352
1511
  formatted_tools_list = "\n".join([
1353
1512
  f"- Full Tool Name: {t.get('name')}\n Description: {t.get('description')}\n Input Schema: {json.dumps(t.get('input_schema'))}"
1354
1513
  for t in available_tools
1355
- ])
1514
+ ])
1515
+
1516
+ # --- 2. Optional Initial Objectives Extraction ---
1356
1517
  if build_plan:
1357
1518
  if streaming_callback:
1358
1519
  streaming_callback("Extracting initial objectives...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "objectives_extraction"}, turn_history)
1359
1520
 
1360
- # The enhanced prompt is placed inside the original parenthesis format.
1361
- # The f-strings for tool lists and user prompts are preserved.
1362
-
1363
1521
  obj_prompt = (
1364
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"
1365
1523
  "Your plan must be the most direct and minimal path to the user's goal.\n\n"
@@ -1388,8 +1546,6 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1388
1546
 
1389
1547
  turn_history.append({"type": "initial_objectives", "content": current_objectives})
1390
1548
 
1391
-
1392
-
1393
1549
  # --- 3. Main Agent Loop ---
1394
1550
  while llm_iterations < max_llm_iterations:
1395
1551
  llm_iterations += 1
@@ -1401,15 +1557,19 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1401
1557
  if agent_work_history:
1402
1558
  history_parts = []
1403
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
+
1404
1565
  history_parts.append(
1405
1566
  f"### Step {i+1}:\n"
1406
1567
  f"**Thought:** {entry['thought']}\n"
1407
1568
  f"**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n"
1408
- f"**Observation:**\n```json\n{json.dumps(entry['tool_result'], indent=2)}\n```"
1569
+ f"**Observation:**\n```json\n{json.dumps(sanitized_hist_result, indent=2)}\n```"
1409
1570
  )
1410
1571
  formatted_agent_history = "\n\n".join(history_parts)
1411
1572
 
1412
- # Construct the "Thinking & Planning" prompt
1413
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.
1414
1574
 
1415
1575
  --- AVAILABLE TOOLS ---
@@ -1429,50 +1589,47 @@ Knowledge Scratchpad (our current understanding):
1429
1589
  --- INSTRUCTIONS ---
1430
1590
  1. **Analyze:** Review the entire work history, objectives, and scratchpad.
1431
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.
1432
- 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.
1433
-
1434
- --- OUTPUT FORMAT ---
1435
- Respond with a single JSON object inside a JSON markdown tag. Use this exact schema:
1436
- ```json
1437
- {{
1438
- "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).",
1439
- "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.",
1440
- "updated_objectives": "The full, potentially revised, list of objectives. If no change, repeat the current list.",
1441
- "action": "The chosen action: 'call_tool', 'final_answer', or 'clarify'.",
1442
- "tool_name": "(string, if action is 'call_tool') The full 'alias::tool_name' of the tool to use.",
1443
- "tool_params": {{"query": "...", "param2": "..."}},
1444
- "clarification_request": "(string, if action is 'clarify') Your question to the user."
1445
- }}
1446
- ```
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.
1447
1593
  """
1448
- raw_llm_decision_json = self.generate_text(
1449
- prompt=decision_prompt_template, n_predict=2048, temperature=tool_call_decision_temperature
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."
1603
+ }
1604
+ }
1605
+ llm_decision = self.generate_structured_content(
1606
+ prompt=decision_prompt_template,
1607
+ template=decision_template,
1608
+ temperature=tool_call_decision_temperature
1450
1609
  )
1610
+
1611
+ if not llm_decision:
1612
+ ASCIIColors.error("LLM failed to generate a valid decision JSON. Aborting loop.")
1613
+ break
1451
1614
 
1452
1615
  # --- 4. Parse LLM's plan and update state ---
1453
- try:
1454
- llm_decision = robust_json_parser(raw_llm_decision_json)
1455
- turn_history.append({"type": "llm_plan", "content": llm_decision})
1456
-
1457
- current_objectives = llm_decision.get("updated_objectives", current_objectives)
1458
- new_scratchpad = llm_decision.get("updated_scratchpad")
1459
-
1460
- if new_scratchpad and new_scratchpad != knowledge_scratchpad:
1461
- knowledge_scratchpad = new_scratchpad
1462
- if streaming_callback:
1463
- streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP, {"id": "scratchpad_update"}, turn_history)
1464
- streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id":"scratch_pad_update"}, turn_history)
1465
-
1466
- except (json.JSONDecodeError, AttributeError, KeyError) as e:
1467
- ASCIIColors.error(f"Failed to parse LLM decision JSON: {raw_llm_decision_json}. Error: {e}")
1468
- turn_history.append({"type": "error", "content": f"Failed to parse LLM plan: {raw_llm_decision_json}"})
1469
- break
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
+
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)
1470
1626
 
1471
1627
  if streaming_callback:
1472
1628
  streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history)
1473
1629
 
1474
1630
  # --- 5. Execute the chosen action ---
1475
1631
  action = llm_decision.get("action")
1632
+ action_details = llm_decision.get("action_details", {})
1476
1633
  tool_result = None
1477
1634
 
1478
1635
  if action == "call_tool":
@@ -1480,40 +1637,53 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
1480
1637
  ASCIIColors.warning("Max tool calls reached. Forcing final answer.")
1481
1638
  break
1482
1639
 
1483
- tool_name = llm_decision.get("tool_name")
1484
- tool_params = llm_decision.get("tool_params", {})
1640
+ tool_name = action_details.get("tool_name")
1641
+ tool_params = action_details.get("tool_params", {})
1485
1642
 
1486
1643
  if not tool_name or not isinstance(tool_params, dict):
1487
1644
  ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
1488
1645
  break
1489
1646
 
1490
1647
  if streaming_callback:
1491
- streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
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)
1492
1649
 
1493
1650
  try:
1494
1651
  # ** DYNAMIC TOOL/RAG DISPATCH **
1495
- if tool_name == "research::query_database":
1496
- query = tool_params.get("query")
1497
- if not query:
1498
- tool_result = {"error": "RAG tool called without a 'query' parameter."}
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."}
1499
1657
  else:
1500
- retrieved_chunks = rag_query_function(query, rag_vectorizer_name, rag_top_k, rag_min_similarity_percent)
1501
- if not retrieved_chunks:
1502
- tool_result = {"summary": "No relevant documents found for the query.", "chunks": []}
1658
+ query = tool_params.get("query")
1659
+ if not query:
1660
+ tool_result = {"error": "RAG tool called without a 'query' parameter."}
1503
1661
  else:
1504
- tool_result = {
1505
- "summary": f"Found {len(retrieved_chunks)} relevant document chunks.",
1506
- "chunks": retrieved_chunks
1507
- }
1508
- 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:
1509
1671
  # Standard MCP tool execution
1510
1672
  tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1673
+ else:
1674
+ tool_result = {"error": f"Tool '{tool_name}' cannot be executed. RAG store not found or MCP binding not configured."}
1511
1675
 
1512
1676
  except Exception as e_exec:
1513
1677
  trace_exception(e_exec)
1514
1678
  tool_result = {"error": f"An exception occurred while executing tool '{tool_name}': {e_exec}"}
1515
1679
 
1516
- # Record the work cycle in the agent's history
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
+
1517
1687
  work_entry = {
1518
1688
  "thought": llm_decision.get("thought", "N/A"),
1519
1689
  "tool_name": tool_name,
@@ -1522,13 +1692,9 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
1522
1692
  }
1523
1693
  agent_work_history.append(work_entry)
1524
1694
  tool_calls_made_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1525
-
1526
- if streaming_callback:
1527
- streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
1528
- streaming_callback(json.dumps(tool_result, indent=2), MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, tool_result, turn_history)
1529
1695
 
1530
1696
  elif action == "clarify":
1531
- clarification_request = llm_decision.get("clarification_request", "I need more information. Could you please clarify?")
1697
+ clarification_request = action_details.get("clarification_request", "I need more information. Could you please clarify?")
1532
1698
  return {"final_answer": clarification_request, "tool_calls": tool_calls_made_this_turn, "error": None, "clarification": True}
1533
1699
 
1534
1700
  elif action == "final_answer":
@@ -1542,7 +1708,8 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
1542
1708
  streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1543
1709
 
1544
1710
  if streaming_callback:
1545
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1711
+ streaming_callback(f"LLM reasoning loop finished.", MSG_TYPE.MSG_TYPE_STEP, {"id": "reasoning_loop_end"}, turn_history)
1712
+
1546
1713
  # --- 6. Generate Final Answer ---
1547
1714
  if streaming_callback:
1548
1715
  streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history)
@@ -1577,7 +1744,8 @@ Original User Request: "{original_user_prompt}"
1577
1744
  turn_history.append({"type":"final_answer_generated", "content": final_answer})
1578
1745
 
1579
1746
  return {"final_answer": final_answer, "tool_calls": tool_calls_made_this_turn, "error": None}
1580
-
1747
+
1748
+
1581
1749
  def generate_code(
1582
1750
  self,
1583
1751
  prompt,