lollms-client 1.1.3__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.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/lollms/__init__.py +2 -2
- lollms_client/llm_bindings/openai/__init__.py +3 -3
- lollms_client/lollms_core.py +285 -131
- lollms_client/lollms_discussion.py +419 -147
- lollms_client/lollms_tti_binding.py +32 -82
- lollms_client/tti_bindings/diffusers/__init__.py +371 -314
- lollms_client/tti_bindings/openai/__init__.py +124 -0
- {lollms_client-1.1.3.dist-info → lollms_client-1.3.0.dist-info}/METADATA +1 -1
- {lollms_client-1.1.3.dist-info → lollms_client-1.3.0.dist-info}/RECORD +13 -13
- lollms_client/tti_bindings/dalle/__init__.py +0 -454
- {lollms_client-1.1.3.dist-info → lollms_client-1.3.0.dist-info}/WHEEL +0 -0
- {lollms_client-1.1.3.dist-info → lollms_client-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.1.3.dist-info → lollms_client-1.3.0.dist-info}/top_level.txt +0 -0
lollms_client/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ from lollms_client.lollms_utilities import PromptReshaper # Keep general utiliti
|
|
|
8
8
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
|
|
9
9
|
from lollms_client.lollms_llm_binding import LollmsLLMBindingManager
|
|
10
10
|
|
|
11
|
-
__version__ = "1.
|
|
11
|
+
__version__ = "1.3.0" # Updated version
|
|
12
12
|
|
|
13
13
|
# Optionally, you could define __all__ if you want to be explicit about exports
|
|
14
14
|
__all__ = [
|
|
@@ -9,7 +9,7 @@ from lollms_client.lollms_discussion import LollmsDiscussion
|
|
|
9
9
|
from typing import Optional, Callable, List, Union
|
|
10
10
|
from ascii_colors import ASCIIColors, trace_exception
|
|
11
11
|
from typing import List, Dict
|
|
12
|
-
|
|
12
|
+
import httpx
|
|
13
13
|
import pipmaster as pm
|
|
14
14
|
|
|
15
15
|
pm.ensure_packages(["openai","tiktoken"])
|
|
@@ -49,7 +49,7 @@ class LollmsBinding(LollmsLLMBinding):
|
|
|
49
49
|
|
|
50
50
|
if not self.service_key:
|
|
51
51
|
self.service_key = os.getenv("LOLLMS_API_KEY", self.service_key)
|
|
52
|
-
self.client = openai.OpenAI(api_key=self.service_key, base_url=None if self.host_address is None else self.host_address if len(self.host_address)>0 else None)
|
|
52
|
+
self.client = openai.OpenAI(api_key=self.service_key, base_url=None if self.host_address is None else self.host_address if len(self.host_address)>0 else None, http_client=httpx.Client(verify=self.verify_ssl_certificate))
|
|
53
53
|
self.completion_format = ELF_COMPLETION_FORMAT.Chat
|
|
54
54
|
|
|
55
55
|
def lollms_listMountedPersonalities(self, host_address:str|None=None):
|
|
@@ -10,7 +10,7 @@ from typing import Optional, Callable, List, Union
|
|
|
10
10
|
from ascii_colors import ASCIIColors, trace_exception
|
|
11
11
|
from typing import List, Dict
|
|
12
12
|
import math
|
|
13
|
-
|
|
13
|
+
import httpx
|
|
14
14
|
import pipmaster as pm
|
|
15
15
|
|
|
16
16
|
pm.ensure_packages(["openai","tiktoken"])
|
|
@@ -47,7 +47,7 @@ class OpenAIBinding(LollmsLLMBinding):
|
|
|
47
47
|
|
|
48
48
|
if not self.service_key:
|
|
49
49
|
self.service_key = os.getenv("OPENAI_API_KEY", self.service_key)
|
|
50
|
-
self.client = openai.OpenAI(api_key=self.service_key, base_url=None if self.host_address is None else self.host_address if len(self.host_address)>0 else None)
|
|
50
|
+
self.client = openai.OpenAI(api_key=self.service_key, base_url=None if self.host_address is None else self.host_address if len(self.host_address)>0 else None, http_client=httpx.Client(verify=self.verify_ssl_certificate))
|
|
51
51
|
self.completion_format = ELF_COMPLETION_FORMAT.Chat
|
|
52
52
|
|
|
53
53
|
def _build_openai_params(self, messages: list, **kwargs) -> dict:
|
|
@@ -668,4 +668,4 @@ class OpenAIBinding(LollmsLLMBinding):
|
|
|
668
668
|
"""
|
|
669
669
|
self.model = model_name
|
|
670
670
|
self.model_name = model_name
|
|
671
|
-
return True
|
|
671
|
+
return True
|
lollms_client/lollms_core.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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"-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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):
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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():
|
|
1560
|
-
|
|
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
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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 =
|
|
1584
|
-
tool_name
|
|
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(
|
|
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":
|
|
1624
|
+
if tool_name == "local_tools::final_answer":
|
|
1625
|
+
break
|
|
1589
1626
|
if tool_name == "local_tools::request_clarification":
|
|
1590
|
-
return {
|
|
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
|
-
|
|
1593
|
-
if
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
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-
|
|
1642
|
-
log_event(
|
|
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(
|
|
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"""---
|
|
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
|
-
|
|
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 {
|
|
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,
|