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

@@ -1458,75 +1458,36 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1458
1458
  streaming_callback: Optional[Callable[[str, 'MSG_TYPE', Optional[Dict], Optional[List]], bool]] = None,
1459
1459
  rag_top_k: int = 5,
1460
1460
  rag_min_similarity_percent: float = 50.0,
1461
- output_summarization_threshold: int = 500, # In tokens
1461
+ output_summarization_threshold: int = 500,
1462
1462
  force_mcp_use: bool = False,
1463
1463
  debug: bool = False,
1464
1464
  **llm_generation_kwargs
1465
1465
  ) -> Dict[str, Any]:
1466
- """
1467
- Orchestrates a sophisticated and robust agentic process to generate a response.
1468
-
1469
- This method employs a dynamic "observe-think-act" loop with several advanced architectural
1470
- patterns for improved robustness and efficiency, particularly when handling code.
1471
-
1472
- Key Features:
1473
- - **Context-Aware Asset Ingestion**: The agent automatically detects if the `context`
1474
- parameter (representing the previous turn) contains code. If so, it registers that
1475
- code as an asset with a UUID, preventing the LLM from trying to paste large code
1476
- blocks into its prompts and avoiding JSON errors.
1477
- - **Tool Perception Filtering**: Identifies tools that directly consume code and HIDES
1478
- them from the LLM's view, forcing it to use the safer `generate_and_call` workflow.
1479
- - **Forced Safe Workflow**: The `generate_and_call` meta-tool is the ONLY way the agent
1480
- can execute code, ensuring a robust, error-free, and efficient process.
1481
- - **Verbose Internal Logging**: The `generate_and_call` tool is now fully instrumented
1482
- with detailed logging and robust error handling to ensure every failure is visible
1483
- and diagnosable, preventing silent loops.
1484
-
1485
- Args:
1486
- prompt: The user's initial prompt or question for the current turn.
1487
- context: An optional string containing the content of the previous turn.
1488
- use_mcps: Controls MCP tool usage.
1489
- use_data_store: Controls RAG usage.
1490
- system_prompt: Main system prompt for the final answer.
1491
- reasoning_system_prompt: System prompt for the decision-making process.
1492
- images: A list of base64-encoded images provided by the user for the current turn.
1493
- max_reasoning_steps: Maximum number of reasoning cycles.
1494
- decision_temperature: Temperature for LLM's decision-making.
1495
- final_answer_temperature: Temperature for final answer synthesis.
1496
- streaming_callback: Function for real-time output of tokens and steps.
1497
- rag_top_k: Number of top documents to retrieve during RAG.
1498
- rag_min_similarity_percent: Minimum similarity for RAG results.
1499
- output_summarization_threshold: Token count that triggers summarization.
1500
- force_mcp_use: If True, bypasses the "fast answer" check.
1501
- debug: If True, prints detailed prompting and response information.
1502
- **llm_generation_kwargs: Additional keyword arguments for LLM calls.
1503
-
1504
- Returns:
1505
- A dictionary containing the agent's full run.
1506
- """
1507
1466
  if not self.llm:
1508
1467
  return {"final_answer": "", "tool_calls": [], "sources": [], "error": "LLM binding not initialized."}
1509
1468
  if max_reasoning_steps is None:
1510
1469
  max_reasoning_steps = 10
1511
- # --- Helper Functions ---
1470
+
1512
1471
  def log_event(desc, event_type=MSG_TYPE.MSG_TYPE_CHUNK, meta=None, event_id=None) -> Optional[str]:
1513
- if not streaming_callback: return None
1472
+ if not streaming_callback:
1473
+ return None
1514
1474
  is_start = event_type == MSG_TYPE.MSG_TYPE_STEP_START
1515
1475
  event_id = str(uuid.uuid4()) if is_start and not event_id else event_id
1516
1476
  params = {"type": event_type, "description": desc, **(meta or {})}
1517
- if event_id: params["id"] = event_id
1477
+ if event_id:
1478
+ params["id"] = event_id
1518
1479
  streaming_callback(desc, event_type, params)
1519
1480
  return event_id
1520
1481
 
1521
1482
  def log_prompt(title: str, prompt_text: str):
1522
- if not debug: return
1483
+ if not debug:
1484
+ return
1523
1485
  ASCIIColors.cyan(f"** DEBUG: {title} **")
1524
1486
  ASCIIColors.magenta(prompt_text[-15000:])
1525
1487
  prompt_size = self.count_tokens(prompt_text)
1526
1488
  ASCIIColors.red(f"Prompt size:{prompt_size}/{self.llm.default_ctx_size}")
1527
1489
  ASCIIColors.cyan(f"** DEBUG: DONE **")
1528
1490
 
1529
- # --- 1. Initialize State & Context-Aware Asset Ingestion ---
1530
1491
  original_user_prompt, tool_calls_this_turn, sources_this_turn = prompt, [], []
1531
1492
  asset_store: Dict[str, Dict] = {}
1532
1493
  initial_state_parts = ["### Initial State", "- My goal is to address the user's request comprehensively."]
@@ -1541,121 +1502,314 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1541
1502
  last_code_block = code_blocks[-1]
1542
1503
  code_uuid = str(uuid.uuid4())
1543
1504
  asset_store[code_uuid] = {"type": "code", "content": last_code_block}
1544
- initial_state_parts.append(f"- The user's request likely refers to a code block from the previous turn's context. It has been registered as asset ID: {code_uuid}")
1505
+ initial_state_parts.append(f"- A code block was found in the context. It has been registered as asset ID: {code_uuid}")
1545
1506
  current_scratchpad = "\n".join(initial_state_parts)
1546
1507
 
1547
- # --- 2. Tool Discovery and Filtering ---
1548
- discovery_step_id = log_event("Discovering and filtering tools...", MSG_TYPE.MSG_TYPE_STEP_START)
1549
- all_discovered_tools, visible_tools, code_consuming_tools = [], [], set()
1508
+ discovery_step_id = log_event("Discovering tools...", MSG_TYPE.MSG_TYPE_STEP_START)
1509
+ all_discovered_tools, visible_tools = [], []
1510
+ rag_registry: Dict[str, Callable] = {}
1511
+ rag_tool_specs: Dict[str, Dict] = {}
1512
+
1550
1513
  if use_mcps and hasattr(self, 'mcp'):
1551
1514
  mcp_tools = self.mcp.discover_tools(force_refresh=True)
1552
- if isinstance(use_mcps, list): all_discovered_tools.extend([t for t in mcp_tools if t["name"] in use_mcps])
1553
- elif use_mcps is True: all_discovered_tools.extend(mcp_tools)
1554
- code_param_keywords = {'code', 'script', 'python_code', 'javascript', 'html', 'css'}
1555
- for tool in all_discovered_tools:
1556
- if any(p in code_param_keywords for p in tool.get("input_schema", {}).get("properties", {})): code_consuming_tools.add(tool['name'])
1557
- else: visible_tools.append(tool)
1515
+ if isinstance(use_mcps, list):
1516
+ all_discovered_tools.extend([t for t in mcp_tools if t["name"] in use_mcps])
1517
+ elif use_mcps is True:
1518
+ all_discovered_tools.extend(mcp_tools)
1519
+
1558
1520
  if use_data_store:
1559
- for name, info in use_data_store.items(): visible_tools.append({"name": f"research::{name}", "description": info.get("description", f"Queries '{name}'."), "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}})
1560
- log_event(f"Made {len(visible_tools)} tools visible (hid {len(code_consuming_tools)} code tools).", MSG_TYPE.MSG_TYPE_STEP_END, meta={"visible": len(visible_tools), "hidden": len(code_consuming_tools), "hidden_list": list(code_consuming_tools)}, event_id=discovery_step_id)
1521
+ for name, info in use_data_store.items():
1522
+ tool_name = f"research::{name}"
1523
+ description = f"Queries '{name}'."
1524
+ call_fn = None
1525
+ if callable(info):
1526
+ call_fn = info
1527
+ elif isinstance(info, dict):
1528
+ if "call" in info and callable(info["call"]):
1529
+ call_fn = info["call"]
1530
+ description = info.get("description", description)
1531
+ if call_fn:
1532
+ visible_tools.append({
1533
+ "name": tool_name,
1534
+ "description": description,
1535
+ "input_schema": {
1536
+ "type": "object",
1537
+ "properties": {
1538
+ "query": {"type": "string"},
1539
+ "top_k": {"type": "integer"},
1540
+ "min_similarity_percent": {"type": "number"},
1541
+ "filters": {"type": "object"}
1542
+ },
1543
+ "required": ["query"]
1544
+ }
1545
+ })
1546
+ rag_registry[tool_name] = call_fn
1547
+ rag_tool_specs[tool_name] = {"default_top_k": rag_top_k, "default_min_sim": rag_min_similarity_percent}
1548
+ else:
1549
+ log_event("RAG tool registration failed", MSG_TYPE.MSG_TYPE_WARNING, meta={"store_name": name})
1550
+
1551
+ visible_tools.extend(all_discovered_tools)
1552
+
1553
+ built_in_tools = [
1554
+ {"name": "local_tools::final_answer", "description": "Provide the final answer directly to the user.", "input_schema": {}},
1555
+ {"name": "local_tools::request_clarification", "description": "Ask the user for more information.", "input_schema": {"type": "object", "properties": {"question_to_user": {"type": "string"}}, "required": ["question_to_user"]}}
1556
+ ]
1557
+
1558
+ if getattr(self, "tti", None):
1559
+ built_in_tools.append({
1560
+ "name": "local_tools::generate_image",
1561
+ "description": "Generate an image from a text description. Returns a base64-encoded image.",
1562
+ "input_schema": {"type": "object", "properties": {"prompt": {"type": "string"}}, "required": ["prompt"]}
1563
+ })
1561
1564
 
1562
- # --- 3. Fast Answer Path (Not shown for brevity, but retained) ---
1563
-
1564
- # --- 4. Format Tools for Main Loop ---
1565
- CODE_PLACEHOLDER = "{GENERATED_CODE}"
1566
- built_in_tools = [{"name": "local_tools::generate_and_call", "description": f"CRITICAL: To run or modify code, you MUST use this tool. It generates code (e.g., to fix code from an asset) and then calls a tool with it. Refer to existing code using its asset ID. Use '{CODE_PLACEHOLDER}' in `next_tool_params` for the NEWLY generated code.", "input_schema": { "type": "object", "properties": { "code_generation_prompt": {"type": "string"}, "language": {"type": "string"}, "next_tool_name": {"type": "string"}, "next_tool_params": {"type": "object"}}, "required": ["code_generation_prompt", "next_tool_name", "next_tool_params"]}}, {"name": "local_tools::refactor_scratchpad", "description": "Rewrites the scratchpad.", "input_schema": {}}, {"name": "local_tools::request_clarification", "description": "Asks the user for more information.", "input_schema": {"type": "object", "properties": {"question_to_user": {"type": "string"}}, "required": ["question_to_user"]}}, {"name": "local_tools::final_answer", "description": "Provides the final answer.", "input_schema": {}}]
1567
1565
  all_visible_tools = visible_tools + built_in_tools
1568
1566
  formatted_tools_list = "\n".join([f"**{t['name']}**:\n- Description: {t['description']}" for t in all_visible_tools])
1567
+ log_event(
1568
+ f"Made {len(all_visible_tools)} tools visible.",
1569
+ MSG_TYPE.MSG_TYPE_STEP_END,
1570
+ meta={"visible": len(all_visible_tools), "rag_tools": list(rag_registry.keys())},
1571
+ event_id=discovery_step_id
1572
+ )
1569
1573
 
1570
- # --- 5. Dynamic Reasoning Loop ---
1571
1574
  for i in range(max_reasoning_steps):
1572
1575
  reasoning_step_id = log_event(f"Reasoning Step {i+1}/{max_reasoning_steps}", MSG_TYPE.MSG_TYPE_STEP_START)
1573
1576
  try:
1574
- reasoning_prompt = f"""--- AVAILABLE ACTIONS ---\n{formatted_tools_list}\n\n--- YOUR INTERNAL SCRATCHPAD ---\n{current_scratchpad}\n--- END SCRATCHPAD ---\n\n**INSTRUCTIONS:**\n1. **OBSERVE:** Review your scratchpad, especially available asset IDs.\n2. **THINK:** Based on '{original_user_prompt}', what is the single next logical action using ONLY the available actions?\n3. **ACT:** Formulate your decision as a JSON object. Do NOT paste large code blocks into parameters; use their asset IDs instead."""
1575
- action_schema = {"thought": "My reasoning.", "action": {"tool_name": "string", "tool_params": "object"}}
1576
- action_data = self.generate_structured_content(prompt=reasoning_prompt, schema=action_schema, system_prompt=reasoning_system_prompt, temperature=decision_temperature, **llm_generation_kwargs)
1577
-
1578
- if not action_data or not isinstance(action_data.get("action"), dict):
1579
- log_event("Failed to generate a valid JSON action. Will retry.", MSG_TYPE.MSG_TYPE_WARNING, event_id=reasoning_step_id)
1580
- current_scratchpad += "\n\n### Step Failure\n- **Error:** Failed to produce a valid JSON action."
1577
+ reasoning_prompt = f"""--- AVAILABLE ACTIONS ---
1578
+ {formatted_tools_list}
1579
+
1580
+ --- YOUR INTERNAL SCRATCHPAD ---
1581
+ {current_scratchpad}
1582
+ --- END SCRATCHPAD ---
1583
+
1584
+ INSTRUCTIONS:
1585
+ 1) OBSERVE the scratchpad and available assets.
1586
+ 2) THINK what is the single best next action to progress toward the user's goal: "{original_user_prompt}".
1587
+ 3) ACT: Produce only this JSON:
1588
+
1589
+ {{
1590
+ "thought": "short, concrete reasoning",
1591
+ "action": {{
1592
+ "tool_name": "string",
1593
+ "requires_code_input": true/false,
1594
+ "requires_image_input": true/false
1595
+ }}
1596
+ }}
1597
+
1598
+ You may choose "local_tools::final_answer" to answer directly without tools.
1599
+ """
1600
+ log_prompt("Decision Prompt", reasoning_prompt)
1601
+ decision_schema = {
1602
+ "thought": "My reasoning.",
1603
+ "action": {"tool_name": "string", "requires_code_input": "boolean", "requires_image_input": "boolean"}
1604
+ }
1605
+ decision_data = self.generate_structured_content(
1606
+ prompt=reasoning_prompt,
1607
+ schema=decision_schema,
1608
+ system_prompt=reasoning_system_prompt,
1609
+ temperature=decision_temperature,
1610
+ **llm_generation_kwargs
1611
+ )
1612
+ if not decision_data or not isinstance(decision_data.get("action"), dict):
1613
+ log_event("Invalid decision JSON", MSG_TYPE.MSG_TYPE_WARNING, meta={"decision_raw": str(decision_data)}, event_id=reasoning_step_id)
1614
+ current_scratchpad += "\n\n### Step Failure\n- Error: Invalid decision JSON."
1581
1615
  continue
1582
1616
 
1583
- thought, action = action_data.get("thought", ""), action_data.get("action", {})
1584
- tool_name, tool_params = action.get("tool_name"), action.get("tool_params", {})
1617
+ thought, action = decision_data.get("thought", ""), decision_data.get("action", {})
1618
+ tool_name = action.get("tool_name")
1619
+ requires_code = action.get("requires_code_input", False)
1620
+ requires_image = action.get("requires_image_input", False)
1585
1621
  current_scratchpad += f"\n\n### Step {i+1}: Thought\n{thought}"
1586
- log_event(thought, MSG_TYPE.MSG_TYPE_THOUGHT_CONTENT)
1622
+ log_event("Decision taken", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name, "requires_code": requires_code, "requires_image": requires_image})
1587
1623
 
1588
- if tool_name == "local_tools::final_answer": break
1624
+ if tool_name == "local_tools::final_answer":
1625
+ break
1589
1626
  if tool_name == "local_tools::request_clarification":
1590
- return {"final_answer": tool_params.get("question_to_user", "?"), "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1627
+ return {
1628
+ "final_answer": decision_data.get("question_to_user", "?"),
1629
+ "final_scratchpad": current_scratchpad,
1630
+ "tool_calls": tool_calls_this_turn,
1631
+ "sources": sources_this_turn,
1632
+ "clarification_required": True,
1633
+ "error": None
1634
+ }
1591
1635
 
1592
- tool_result = {"status": "failure", "error": f"Tool '{tool_name}' was called but did not execute properly."} # Default error
1593
- if tool_name == "local_tools::generate_and_call":
1594
- chain_id = log_event(f"Starting chained tool call...", MSG_TYPE.MSG_TYPE_STEP_START)
1595
- try:
1596
- code_gen_prompt, lang = tool_params.get("code_generation_prompt", ""), tool_params.get("language", "python")
1597
- next_tool_name, next_tool_params = tool_params.get("next_tool_name"), tool_params.get("next_tool_params", {})
1598
- log_event("Received parameters for chain", MSG_TYPE.MSG_TYPE_STEP, meta={"parent_id": chain_id, "params": tool_params})
1599
-
1600
- if not (use_mcps and hasattr(self, 'mcp')):
1601
- tool_result = {"status": "failure", "error": "MCPs are not enabled, cannot execute tools."}
1602
- elif next_tool_name not in code_consuming_tools:
1603
- tool_result = {"status": "failure", "error": f"Tool '{next_tool_name}' is not a valid code-consuming tool. Valid options are: {list(code_consuming_tools)}"}
1604
- else:
1605
- def _hydrate(text: str, store: Dict) -> str:
1606
- for k, v in store.items(): text = text.replace(k, v.get('content',''))
1607
- return text
1608
- hydrated_prompt = _hydrate(code_gen_prompt, asset_store)
1609
- log_event(f"Generating {lang} code for {next_tool_name}", MSG_TYPE.MSG_TYPE_STEP, meta={"parent_id": chain_id, "hydrated_prompt": hydrated_prompt})
1610
- generated_code = self.generate_code(prompt=hydrated_prompt, system_prompt=f"Generate ONLY raw {lang} code.", **llm_generation_kwargs)
1611
-
1612
- def _substitute(data: Any) -> Any:
1613
- if isinstance(data, dict): return {k: _substitute(v) for k, v in data.items()}
1614
- if isinstance(data, list): return [_substitute(item) for item in data]
1615
- if isinstance(data, str) and data == CODE_PLACEHOLDER: return generated_code
1616
- return data
1617
- hydrated_params = _substitute(next_tool_params)
1618
-
1619
- log_event(f"Calling tool: {next_tool_name}", MSG_TYPE.MSG_TYPE_TOOL_CALL, meta={"parent_id": chain_id, "name": next_tool_name, "parameters": hydrated_params})
1620
- tool_result = self.mcp.execute_tool(next_tool_name, hydrated_params, lollms_client_instance=self)
1621
- except Exception as e:
1622
- tool_result = {"status": "failure", "error": f"Exception in chained tool logic: {str(e)}"}
1623
- log_event(f"Finished chained tool call.", MSG_TYPE.MSG_TYPE_STEP_END, event_id=chain_id)
1624
- # ... other non-code tool handlers ...
1625
-
1626
- # --- Process and Sanitize ALL Tool Outputs for the Scratchpad ---
1627
- sanitized_result = {}
1628
- if isinstance(tool_result, dict):
1629
- sanitized_result = tool_result.copy()
1630
- for key, value in tool_result.items():
1631
- if isinstance(value, str) and value.startswith("data:image"):
1632
- img_uuid = str(uuid.uuid4())
1633
- asset_store[img_uuid] = {"type": "image", "content": value}
1634
- sanitized_result[key] = f"[Image asset generated: {img_uuid}]"
1636
+ prepared_assets = {}
1637
+ if requires_code:
1638
+ code_prompt = f"""--- ORIGINAL USER REQUEST ---
1639
+ "{original_user_prompt}"
1640
+
1641
+ --- YOUR INTERNAL SCRATCHPAD ---
1642
+ {current_scratchpad}
1643
+ --- END SCRATCHPAD ---
1644
+
1645
+ INSTRUCTIONS:
1646
+ Generate raw code only, with no explanations. The code must be self-contained and directly address the current next action."""
1647
+ log_prompt("Code Generation Prompt", code_prompt)
1648
+ generated_code = self.generate_code(prompt=code_prompt, system_prompt="Generate ONLY raw code.", **llm_generation_kwargs)
1649
+ code_uuid = str(uuid.uuid4())
1650
+ asset_store[code_uuid] = {"type": "code", "content": generated_code}
1651
+ prepared_assets["code_asset_id"] = code_uuid
1652
+ log_event("Code asset created", MSG_TYPE.MSG_TYPE_STEP, meta={"code_asset_id": code_uuid, "code_len": len(generated_code) if isinstance(generated_code, str) else None})
1653
+
1654
+ if requires_image:
1655
+ for img_b64 in images or []:
1656
+ img_uuid = str(uuid.uuid4())
1657
+ asset_store[img_uuid] = {"type": "image", "content": img_b64}
1658
+ prepared_assets.setdefault("image_asset_ids", []).append(img_uuid)
1659
+ log_event("Image assets prepared", MSG_TYPE.MSG_TYPE_STEP, meta={"image_asset_ids": prepared_assets.get("image_asset_ids", [])})
1660
+
1661
+ param_prompt = f"""--- SELECTED TOOL ---
1662
+ {tool_name}
1663
+
1664
+ --- AVAILABLE ASSETS ---
1665
+ code_asset_id: {prepared_assets.get("code_asset_id","<none>")}
1666
+ image_asset_ids: {prepared_assets.get("image_asset_ids","<none>")}
1667
+
1668
+ --- ORIGINAL USER REQUEST ---
1669
+ "{original_user_prompt}"
1670
+
1671
+ --- YOUR INTERNAL SCRATCHPAD ---
1672
+ {current_scratchpad}
1673
+ --- END SCRATCHPAD ---
1674
+
1675
+ INSTRUCTIONS:
1676
+ Fill the parameters for the selected tool. If code is required, do not paste code; use the code asset ID string exactly. If images are required, use the provided image asset IDs. Output only:
1677
+
1678
+ {{
1679
+ "tool_params": {{...}}
1680
+ }}
1681
+ """
1682
+ log_prompt("Parameter Generation Prompt", param_prompt)
1683
+ param_schema = {"tool_params": "object"}
1684
+ param_data = self.generate_structured_content(
1685
+ prompt=param_prompt,
1686
+ schema=param_schema,
1687
+ system_prompt=reasoning_system_prompt,
1688
+ temperature=decision_temperature,
1689
+ **llm_generation_kwargs
1690
+ )
1691
+ tool_params = {}
1692
+ if param_data and isinstance(param_data.get("tool_params"), dict):
1693
+ tool_params = param_data["tool_params"]
1635
1694
  else:
1636
- sanitized_result = {"raw_output": str(tool_result)}
1637
-
1695
+ log_event("Parameter generation returned empty", MSG_TYPE.MSG_TYPE_WARNING, meta={"param_raw": str(param_data)})
1696
+
1697
+ def _hydrate(data: Any, store: Dict) -> Any:
1698
+ if isinstance(data, dict):
1699
+ return {k: _hydrate(v, store) for k, v in data.items()}
1700
+ if isinstance(data, list):
1701
+ return [_hydrate(item, store) for item in data]
1702
+ if isinstance(data, str) and data in store:
1703
+ return store[data].get("content", data)
1704
+ return data
1705
+
1706
+ hydrated_params = _hydrate(tool_params, asset_store)
1707
+ log_event("Hydrated parameters", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name})
1708
+
1709
+ tool_result = {"status": "failure", "error": f"Tool '{tool_name}' failed."}
1710
+ try:
1711
+ if tool_name == "local_tools::generate_image":
1712
+ prompt_for_img = hydrated_params.get("prompt", "")
1713
+ log_event("TTI call start", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name})
1714
+ image_bytes = self.tti.generate_image(prompt=prompt_for_img)
1715
+ if not image_bytes:
1716
+ raise Exception("TTI binding returned empty image data.")
1717
+ b64_image = base64.b64encode(image_bytes).decode("utf-8")
1718
+ img_uuid = str(uuid.uuid4())
1719
+ asset_store[img_uuid] = {"type": "image", "content": f"data:image/png;base64,{b64_image}"}
1720
+ tool_result = {"status": "success", "image_asset": img_uuid, "html_tag": f"<img src='data:image/png;base64,{b64_image}' alt='Generated Image'/>"}
1721
+ log_event("TTI call success", MSG_TYPE.MSG_TYPE_STEP, meta={"image_asset": img_uuid})
1722
+ elif tool_name in rag_registry:
1723
+ query = hydrated_params.get("query", "")
1724
+ top_k = int(hydrated_params.get("top_k", rag_tool_specs[tool_name]["default_top_k"]))
1725
+ min_sim = float(hydrated_params.get("min_similarity_percent", rag_tool_specs[tool_name]["default_min_sim"]))
1726
+ filters = hydrated_params.get("filters", None)
1727
+ log_event("RAG call start", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name, "query": query, "top_k": top_k, "min_similarity_percent": min_sim, "has_filters": bool(filters)})
1728
+ rag_fn = rag_registry[tool_name]
1729
+ try:
1730
+ raw_results = rag_fn(query=query, top_k=top_k, filters=filters)
1731
+ except TypeError:
1732
+ raw_results = rag_fn(query)
1733
+ docs = []
1734
+ if isinstance(raw_results, dict) and "results" in raw_results:
1735
+ raw_iter = raw_results["results"]
1736
+ else:
1737
+ raw_iter = raw_results
1738
+ for d in raw_iter or []:
1739
+ text = d.get("text") if isinstance(d, dict) else str(d)
1740
+ score = d.get("score", 0.0) if isinstance(d, dict) else 0.0
1741
+ meta = d.get("metadata", {}) if isinstance(d, dict) else {}
1742
+ pct = score * 100.0 if score <= 1.0 else score
1743
+ docs.append({"text": text, "score": pct, "metadata": meta})
1744
+ docs.sort(key=lambda x: x.get("score", 0.0), reverse=True)
1745
+ kept = [x for x in docs if x.get("score", 0.0) >= min_sim][:top_k]
1746
+ dropped = len(docs) - len(kept)
1747
+ tool_result = {"status": "success", "results": kept, "dropped": dropped, "min_similarity_percent": min_sim, "top_k": top_k}
1748
+ sources_this_turn.extend([{"source": tool_name, "metadata": x.get("metadata", {}), "score": x.get("score", 0.0)} for x in kept])
1749
+ snippet_preview = [{"score": x["score"], "text": (x["text"][:200] + "…") if isinstance(x["text"], str) and len(x["text"]) > 200 else x["text"]} for x in kept]
1750
+ log_event("RAG call end", MSG_TYPE.MSG_TYPE_STEP_END, meta={"tool_name": tool_name, "kept": len(kept), "dropped": dropped, "preview": snippet_preview})
1751
+ rag_notes = "\n".join([f"- [{idx+1}] score={x['score']:.1f}% | {x['text'][:500]}" for idx, x in enumerate(kept)])
1752
+ current_scratchpad += f"\n\n### RAG Notes ({tool_name})\n{rag_notes if rag_notes else '- No results above threshold.'}"
1753
+ elif hasattr(self, "mcp"):
1754
+ log_event("MCP tool call start", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name})
1755
+ tool_result = self.mcp.execute_tool(tool_name, hydrated_params, lollms_client_instance=self)
1756
+ log_event("MCP tool call end", MSG_TYPE.MSG_TYPE_STEP_END, meta={"tool_name": tool_name})
1757
+ else:
1758
+ tool_result = {"status": "failure", "error": "No MCP instance available and tool is not RAG/TTI."}
1759
+ except Exception as e:
1760
+ tool_result = {"status": "failure", "error": str(e)}
1761
+ log_event("Tool call exception", MSG_TYPE.MSG_TYPE_EXCEPTION, meta={"tool_name": tool_name, "error": str(e)})
1762
+
1763
+ sanitized_result = tool_result.copy() if isinstance(tool_result, dict) else {"raw_output": str(tool_result)}
1638
1764
  observation_text = f"```json\n{json.dumps(sanitized_result, indent=2)}\n```"
1639
- log_event(f"Received output from: {tool_name}", MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, meta={"name": tool_name, "result": sanitized_result})
1640
1765
  tool_calls_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1641
- current_scratchpad += f"\n\n### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n- **Result:**\n{observation_text}"
1642
- log_event(f"Finished reasoning step {i+1}", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id)
1766
+ current_scratchpad += f"\n\n### Step {i+1}: Observation\n- Action: `{tool_name}`\n- Result:\n{observation_text}"
1767
+ log_event("Observation recorded", MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, meta={"tool_name": tool_name})
1643
1768
 
1769
+ log_event(f"Finished reasoning step {i+1}", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id)
1644
1770
  except Exception as ex:
1645
1771
  trace_exception(ex)
1646
- log_event(f"Error in reasoning loop: {str(ex)}", MSG_TYPE.MSG_TYPE_EXCEPTION, event_id=reasoning_step_id)
1647
-
1648
- # --- 6. Final Answer Synthesis ---
1772
+ log_event("Error in reasoning loop", MSG_TYPE.MSG_TYPE_EXCEPTION, meta={"error": str(ex)}, event_id=reasoning_step_id)
1773
+
1649
1774
  synthesis_id = log_event("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START)
1650
- final_answer_prompt = f"""--- Original User Request ---\n"{original_user_prompt}"\n\n--- Your Internal Scratchpad ---\n{current_scratchpad}\n\n--- INSTRUCTIONS ---\nSynthesize a clear, comprehensive, and friendly answer for the user based ONLY on your scratchpad."""
1775
+ final_answer_prompt = f"""--- ORIGINAL USER REQUEST ---
1776
+ "{original_user_prompt}"
1777
+
1778
+ --- YOUR INTERNAL SCRATCHPAD ---
1779
+ {current_scratchpad}
1780
+ --- END SCRATCHPAD ---
1781
+
1782
+ INSTRUCTIONS:
1783
+ Synthesize a clear, comprehensive, and friendly answer for the user based ONLY on your scratchpad. If relevant images were generated, refer to them naturally. Keep the answer concise but complete."""
1651
1784
  final_synthesis_images = [img for img in (images or [])] + [asset['content'] for asset in asset_store.values() if asset['type'] == 'image']
1652
- final_answer_text = self.generate_text(prompt=final_answer_prompt, system_prompt=system_prompt, images=final_synthesis_images, stream=streaming_callback is not None, streaming_callback=streaming_callback, temperature=final_answer_temperature, **llm_generation_kwargs)
1785
+ log_prompt("Final Synthesis Prompt", final_answer_prompt)
1786
+ final_answer_text = self.generate_text(
1787
+ prompt=final_answer_prompt,
1788
+ system_prompt=system_prompt,
1789
+ images=final_synthesis_images,
1790
+ stream=streaming_callback is not None,
1791
+ streaming_callback=streaming_callback,
1792
+ temperature=final_answer_temperature,
1793
+ **llm_generation_kwargs
1794
+ )
1653
1795
  if isinstance(final_answer_text, dict) and "error" in final_answer_text:
1654
1796
  return {"final_answer": "", "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": False, "error": final_answer_text["error"]}
1797
+
1655
1798
  final_answer = self.remove_thinking_blocks(final_answer_text)
1799
+ for asset_id, asset in asset_store.items():
1800
+ if asset["type"] == "image" and isinstance(asset.get("content"), str) and asset["content"].startswith("data:image"):
1801
+ final_answer += f"\n\n<img src='{asset['content']}' alt='Generated Image'/>"
1802
+
1656
1803
  log_event("Finished synthesizing answer.", MSG_TYPE.MSG_TYPE_STEP_END, event_id=synthesis_id)
1657
1804
 
1658
- return {"final_answer": final_answer, "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": False, "error": None}
1805
+ return {
1806
+ "final_answer": final_answer,
1807
+ "final_scratchpad": current_scratchpad,
1808
+ "tool_calls": tool_calls_this_turn,
1809
+ "sources": sources_this_turn,
1810
+ "clarification_required": False,
1811
+ "error": None
1812
+ }
1659
1813
 
1660
1814
  def generate_code(
1661
1815
  self,