lollms-client 1.3.1__py3-none-any.whl → 1.3.2__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 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.3.1" # Updated version
11
+ __version__ = "1.3.2" # Updated version
12
12
 
13
13
  # Optionally, you could define __all__ if you want to be explicit about exports
14
14
  __all__ = [
@@ -0,0 +1,361 @@
1
+ import json
2
+ import re
3
+ import uuid
4
+ import base64
5
+ import time
6
+ import asyncio
7
+ from typing import Dict, List, Any, Optional, Union, Callable, Tuple
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ import threading
11
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
+ import hashlib
13
+
14
+ class TaskStatus(Enum):
15
+ PENDING = "pending"
16
+ RUNNING = "running"
17
+ COMPLETED = "completed"
18
+ FAILED = "failed"
19
+ SKIPPED = "skipped"
20
+
21
+ class ConfidenceLevel(Enum):
22
+ LOW = "low"
23
+ MEDIUM = "medium"
24
+ HIGH = "high"
25
+ VERY_HIGH = "very_high"
26
+
27
+ @dataclass
28
+ class SubTask:
29
+ id: str
30
+ description: str
31
+ dependencies: List[str] = field(default_factory=list)
32
+ status: TaskStatus = TaskStatus.PENDING
33
+ result: Optional[Dict] = None
34
+ confidence: float = 0.0
35
+ tools_required: List[str] = field(default_factory=list)
36
+ estimated_complexity: int = 1 # 1-5 scale
37
+
38
+ @dataclass
39
+ class ExecutionPlan:
40
+ tasks: List[SubTask]
41
+ total_estimated_steps: int
42
+ execution_order: List[str]
43
+ fallback_strategies: Dict[str, List[str]] = field(default_factory=dict)
44
+
45
+ @dataclass
46
+ class MemoryEntry:
47
+ timestamp: float
48
+ context: str
49
+ action: str
50
+ result: Dict
51
+ confidence: float
52
+ success: bool
53
+ user_feedback: Optional[str] = None
54
+
55
+ @dataclass
56
+ class ToolPerformance:
57
+ success_rate: float = 0.0
58
+ avg_confidence: float = 0.0
59
+ total_calls: int = 0
60
+ avg_response_time: float = 0.0
61
+ last_used: float = 0.0
62
+ failure_patterns: List[str] = field(default_factory=list)
63
+
64
+ class TaskPlanner:
65
+ def __init__(self, llm_client):
66
+ self.llm_client = llm_client
67
+
68
+ def decompose_task(self, user_request: str, context: str = "") -> ExecutionPlan:
69
+ """Break down complex requests into manageable subtasks"""
70
+ decomposition_prompt = f"""
71
+ Analyze this user request and break it down into specific, actionable subtasks:
72
+
73
+ USER REQUEST: "{user_request}"
74
+ CONTEXT: {context}
75
+
76
+ Create a JSON plan with subtasks that are:
77
+ 1. Specific and actionable
78
+ 2. Have clear success criteria
79
+ 3. Include estimated complexity (1-5 scale)
80
+ 4. List required tool types
81
+
82
+ Output format:
83
+ {{
84
+ "tasks": [
85
+ {{
86
+ "id": "task_1",
87
+ "description": "specific action to take",
88
+ "dependencies": ["task_id"],
89
+ "estimated_complexity": 2,
90
+ "tools_required": ["tool_type"]
91
+ }}
92
+ ],
93
+ "execution_strategy": "sequential|parallel|hybrid"
94
+ }}
95
+ """
96
+
97
+ try:
98
+ plan_data = self.llm_client.generate_structured_content(
99
+ prompt=decomposition_prompt,
100
+ schema={"tasks": "array", "execution_strategy": "string"},
101
+ temperature=0.3
102
+ )
103
+
104
+ tasks = []
105
+ for task_data in plan_data.get("tasks", []):
106
+ task = SubTask(
107
+ id=task_data.get("id", str(uuid.uuid4())),
108
+ description=task_data.get("description", ""),
109
+ dependencies=task_data.get("dependencies", []),
110
+ estimated_complexity=task_data.get("estimated_complexity", 1),
111
+ tools_required=task_data.get("tools_required", [])
112
+ )
113
+ tasks.append(task)
114
+
115
+ execution_order = self._calculate_execution_order(tasks)
116
+ total_steps = sum(task.estimated_complexity for task in tasks)
117
+
118
+ return ExecutionPlan(
119
+ tasks=tasks,
120
+ total_estimated_steps=total_steps,
121
+ execution_order=execution_order
122
+ )
123
+
124
+ except Exception as e:
125
+ # Fallback: create single task
126
+ single_task = SubTask(
127
+ id="fallback_task",
128
+ description=user_request,
129
+ estimated_complexity=3
130
+ )
131
+ return ExecutionPlan(
132
+ tasks=[single_task],
133
+ total_estimated_steps=3,
134
+ execution_order=["fallback_task"]
135
+ )
136
+
137
+ def _calculate_execution_order(self, tasks: List[SubTask]) -> List[str]:
138
+ """Calculate optimal execution order based on dependencies"""
139
+ task_map = {task.id: task for task in tasks}
140
+ executed = set()
141
+ order = []
142
+
143
+ def can_execute(task_id: str) -> bool:
144
+ task = task_map[task_id]
145
+ return all(dep in executed for dep in task.dependencies)
146
+
147
+ while len(order) < len(tasks):
148
+ ready_tasks = [tid for tid in task_map.keys()
149
+ if tid not in executed and can_execute(tid)]
150
+
151
+ if not ready_tasks:
152
+ # Handle circular dependencies - execute remaining tasks
153
+ remaining = [tid for tid in task_map.keys() if tid not in executed]
154
+ ready_tasks = remaining[:1] if remaining else []
155
+
156
+ # Sort by complexity (simpler tasks first)
157
+ ready_tasks.sort(key=lambda tid: task_map[tid].estimated_complexity)
158
+
159
+ for task_id in ready_tasks:
160
+ order.append(task_id)
161
+ executed.add(task_id)
162
+
163
+ return order
164
+
165
+ class MemoryManager:
166
+ def __init__(self, max_entries: int = 1000):
167
+ self.memory: List[MemoryEntry] = []
168
+ self.max_entries = max_entries
169
+ self.cache: Dict[str, Any] = {}
170
+ self.cache_ttl: Dict[str, float] = {}
171
+
172
+ def add_memory(self, context: str, action: str, result: Dict,
173
+ confidence: float, success: bool, user_feedback: str = None):
174
+ """Add a new memory entry"""
175
+ entry = MemoryEntry(
176
+ timestamp=time.time(),
177
+ context=context,
178
+ action=action,
179
+ result=result,
180
+ confidence=confidence,
181
+ success=success,
182
+ user_feedback=user_feedback
183
+ )
184
+
185
+ self.memory.append(entry)
186
+
187
+ # Prune old memories
188
+ if len(self.memory) > self.max_entries:
189
+ self.memory = self.memory[-self.max_entries:]
190
+
191
+ def get_relevant_patterns(self, current_context: str, limit: int = 5) -> List[MemoryEntry]:
192
+ """Retrieve relevant past experiences"""
193
+ # Simple similarity scoring based on context overlap
194
+ scored_memories = []
195
+ current_words = set(current_context.lower().split())
196
+
197
+ for memory in self.memory:
198
+ memory_words = set(memory.context.lower().split())
199
+ overlap = len(current_words & memory_words)
200
+ if overlap > 0:
201
+ score = overlap / max(len(current_words), len(memory_words))
202
+ scored_memories.append((score, memory))
203
+
204
+ scored_memories.sort(key=lambda x: x[0], reverse=True)
205
+ return [memory for _, memory in scored_memories[:limit]]
206
+
207
+ def compress_scratchpad(self, scratchpad: str, current_goal: str,
208
+ max_length: int = 8000) -> str:
209
+ """Intelligently compress scratchpad while preserving key insights"""
210
+ if len(scratchpad) <= max_length:
211
+ return scratchpad
212
+
213
+ # Extract key sections
214
+ sections = re.split(r'\n### ', scratchpad)
215
+
216
+ # Prioritize recent steps and successful outcomes
217
+ important_sections = []
218
+ for section in sections[-10:]: # Keep last 10 sections
219
+ if any(keyword in section.lower() for keyword in
220
+ ['success', 'completed', 'found', 'generated', current_goal.lower()]):
221
+ important_sections.append(section)
222
+
223
+ # If still too long, summarize older sections
224
+ if len('\n### '.join(important_sections)) > max_length:
225
+ summary = f"### Previous Steps Summary\n- Completed {len(sections)-len(important_sections)} earlier steps\n- Working toward: {current_goal}\n"
226
+ return summary + '\n### '.join(important_sections[-5:])
227
+
228
+ return '\n### '.join(important_sections)
229
+
230
+ def cache_result(self, key: str, value: Any, ttl: int = 300):
231
+ """Cache expensive operation results"""
232
+ self.cache[key] = value
233
+ self.cache_ttl[key] = time.time() + ttl
234
+
235
+ def get_cached_result(self, key: str) -> Optional[Any]:
236
+ """Retrieve cached result if still valid"""
237
+ if key in self.cache:
238
+ if time.time() < self.cache_ttl.get(key, 0):
239
+ return self.cache[key]
240
+ else:
241
+ # Expired - remove
242
+ del self.cache[key]
243
+ if key in self.cache_ttl:
244
+ del self.cache_ttl[key]
245
+ return None
246
+
247
+ class ToolPerformanceTracker:
248
+ def __init__(self):
249
+ self.tool_stats: Dict[str, ToolPerformance] = {}
250
+ self.lock = threading.Lock()
251
+
252
+ def record_tool_usage(self, tool_name: str, success: bool,
253
+ confidence: float, response_time: float,
254
+ error_msg: str = None):
255
+ """Record tool usage statistics"""
256
+ with self.lock:
257
+ if tool_name not in self.tool_stats:
258
+ self.tool_stats[tool_name] = ToolPerformance()
259
+
260
+ stats = self.tool_stats[tool_name]
261
+ stats.total_calls += 1
262
+ stats.last_used = time.time()
263
+
264
+ # Update success rate
265
+ old_successes = stats.success_rate * (stats.total_calls - 1)
266
+ new_successes = old_successes + (1 if success else 0)
267
+ stats.success_rate = new_successes / stats.total_calls
268
+
269
+ # Update average confidence
270
+ old_conf_total = stats.avg_confidence * (stats.total_calls - 1)
271
+ stats.avg_confidence = (old_conf_total + confidence) / stats.total_calls
272
+
273
+ # Update response time
274
+ old_time_total = stats.avg_response_time * (stats.total_calls - 1)
275
+ stats.avg_response_time = (old_time_total + response_time) / stats.total_calls
276
+
277
+ # Record failure patterns
278
+ if not success and error_msg:
279
+ stats.failure_patterns.append(error_msg[:100])
280
+ # Keep only last 10 failure patterns
281
+ stats.failure_patterns = stats.failure_patterns[-10:]
282
+
283
+ def get_tool_reliability_score(self, tool_name: str) -> float:
284
+ """Calculate overall tool reliability score (0-1)"""
285
+ if tool_name not in self.tool_stats:
286
+ return 0.5 # Neutral for unknown tools
287
+
288
+ stats = self.tool_stats[tool_name]
289
+
290
+ # Weighted combination of success rate and confidence
291
+ reliability = (stats.success_rate * 0.7) + (stats.avg_confidence * 0.3)
292
+
293
+ # Penalty for tools not used recently (older than 1 hour)
294
+ if time.time() - stats.last_used > 3600:
295
+ reliability *= 0.8
296
+
297
+ return reliability
298
+
299
+ def rank_tools_for_task(self, available_tools: List[str],
300
+ task_description: str) -> List[Tuple[str, float]]:
301
+ """Rank tools by suitability for a specific task"""
302
+ tool_scores = []
303
+
304
+ for tool_name in available_tools:
305
+ base_score = self.get_tool_reliability_score(tool_name)
306
+
307
+ # Simple keyword matching bonus
308
+ task_lower = task_description.lower()
309
+ if any(keyword in tool_name.lower() for keyword in
310
+ ['search', 'research'] if 'find' in task_lower or 'search' in task_lower):
311
+ base_score *= 1.2
312
+ elif 'generate' in tool_name.lower() and 'create' in task_lower:
313
+ base_score *= 1.2
314
+
315
+ tool_scores.append((tool_name, min(base_score, 1.0)))
316
+
317
+ tool_scores.sort(key=lambda x: x[1], reverse=True)
318
+ return tool_scores
319
+
320
+ class UncertaintyManager:
321
+ @staticmethod
322
+ def calculate_confidence(reasoning_step: str, tool_results: List[Dict],
323
+ memory_patterns: List[MemoryEntry]) -> Tuple[float, ConfidenceLevel]:
324
+ """Calculate confidence in current reasoning step"""
325
+ base_confidence = 0.5
326
+
327
+ # Boost confidence if similar patterns succeeded before
328
+ if memory_patterns:
329
+ successful_patterns = [m for m in memory_patterns if m.success]
330
+ if successful_patterns:
331
+ avg_success_confidence = sum(m.confidence for m in successful_patterns) / len(successful_patterns)
332
+ base_confidence = (base_confidence + avg_success_confidence) / 2
333
+
334
+ # Adjust based on tool result consistency
335
+ if tool_results:
336
+ success_results = [r for r in tool_results if r.get('status') == 'success']
337
+ if success_results:
338
+ base_confidence += 0.2
339
+
340
+ # Check for consistent information across tools
341
+ if len(tool_results) > 1:
342
+ base_confidence += 0.1
343
+
344
+ # Reasoning quality indicators
345
+ if len(reasoning_step) > 50 and any(word in reasoning_step.lower()
346
+ for word in ['because', 'therefore', 'analysis', 'evidence']):
347
+ base_confidence += 0.1
348
+
349
+ confidence = max(0.0, min(1.0, base_confidence))
350
+
351
+ # Map to confidence levels
352
+ if confidence >= 0.8:
353
+ level = ConfidenceLevel.VERY_HIGH
354
+ elif confidence >= 0.6:
355
+ level = ConfidenceLevel.HIGH
356
+ elif confidence >= 0.4:
357
+ level = ConfidenceLevel.MEDIUM
358
+ else:
359
+ level = ConfidenceLevel.LOW
360
+
361
+ return confidence, level
@@ -14,6 +14,8 @@ from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingM
14
14
 
15
15
  from lollms_client.lollms_discussion import LollmsDiscussion
16
16
 
17
+ from lollms_client.lollms_agentic import TaskStatus, TaskPlanner, MemoryManager, UncertaintyManager, ToolPerformanceTracker
18
+
17
19
  from lollms_client.lollms_utilities import build_image_dicts, dict_to_markdown
18
20
  import json, re
19
21
  from enum import Enum
@@ -23,7 +25,8 @@ from typing import List, Optional, Callable, Union, Dict, Any
23
25
  import numpy as np
24
26
  from pathlib import Path
25
27
  import uuid
26
-
28
+ import hashlib
29
+ import time
27
30
  class LollmsClient():
28
31
  """
29
32
  Core client class for interacting with LOLLMS services, including LLM, TTS, TTI, STT, TTV, and TTM.
@@ -1442,375 +1445,369 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1442
1445
  )
1443
1446
  new_scratchpad_text = self.generate_text(prompt=synthesis_prompt, n_predict=1024, temperature=0.0)
1444
1447
  return self.remove_thinking_blocks(new_scratchpad_text).strip()
1448
+
1445
1449
 
1450
+ def _get_friendly_action_description(self, tool_name: str, requires_code: bool, requires_image: bool) -> str:
1451
+ """Convert technical tool names to user-friendly descriptions for logging."""
1452
+ # Handle specific, high-priority built-in tools first
1453
+ if tool_name == "local_tools::final_answer":
1454
+ return "📋 Ready to provide your final answer"
1455
+ elif tool_name == "local_tools::request_clarification":
1456
+ return "❓ Asking for more information to proceed"
1457
+ elif tool_name == "local_tools::generate_image":
1458
+ return "🎨 Creating an image based on your request"
1459
+
1460
+ # Handle RAG (data store) tools by their pattern
1461
+ elif "research::" in tool_name:
1462
+ # Extract the friendly name of the data source
1463
+ source_name = tool_name.split("::")[-1].replace("_", " ").title()
1464
+ return f"🔍 Searching {source_name} for relevant information"
1465
+
1466
+ # Handle generic actions based on their input requirements
1467
+ elif requires_code:
1468
+ return "💻 Working on a coding solution"
1469
+ elif requires_image:
1470
+ return "🖼️ Analyzing the provided image(s)"
1471
+
1472
+ # Default fallback for any other tool
1473
+ else:
1474
+ # Clean up the technical tool name for a more readable display
1475
+ clean_name = tool_name.replace("_", " ").replace("::", " - ").title()
1476
+ return f"🔧 Using the {clean_name} tool"
1446
1477
  def generate_with_mcp_rag(
1447
- self,
1448
- prompt: str,
1449
- context: Optional[str] = None,
1450
- use_mcps: Union[None, bool, List[str]] = None,
1451
- use_data_store: Union[None, Dict[str, Callable]] = None,
1452
- system_prompt: str|None = None,
1453
- reasoning_system_prompt: str = "You are a logical AI assistant. Your task is to achieve the user's goal by thinking step-by-step and using the available tools.",
1454
- images: Optional[List[str]] = None,
1455
- max_reasoning_steps: int = 10,
1456
- decision_temperature: float = 0.5,
1457
- final_answer_temperature: float = 0.7,
1458
- streaming_callback: Optional[Callable[[str, 'MSG_TYPE', Optional[Dict], Optional[List]], bool]] = None,
1459
- rag_top_k: int = 5,
1460
- rag_min_similarity_percent: float = 50.0,
1461
- output_summarization_threshold: int = 500,
1462
- force_mcp_use: bool = False,
1463
- debug: bool = False,
1464
- **llm_generation_kwargs
1465
- ) -> Dict[str, Any]:
1478
+ self,
1479
+ prompt: str,
1480
+ context: Optional[str] = None,
1481
+ use_mcps: Union[None, bool, List[str]] = None,
1482
+ use_data_store: Union[None, Dict[str, Callable]] = None,
1483
+ system_prompt: str|None = None,
1484
+ reasoning_system_prompt: str = "You are a logical AI assistant. Your task is to achieve the user's goal by thinking step-by-step and using the available tools.",
1485
+ images: Optional[List[str]] = None,
1486
+ max_reasoning_steps: int = 15,
1487
+ decision_temperature: float = 0.5,
1488
+ final_answer_temperature: float = 0.7,
1489
+ streaming_callback: Optional[Callable[[str, 'MSG_TYPE', Optional[Dict], Optional[List]], bool]] = None,
1490
+ rag_top_k: int = 5,
1491
+ rag_min_similarity_percent: float = 50.0,
1492
+ output_summarization_threshold: int = 500,
1493
+ force_mcp_use: bool = False,
1494
+ debug: bool = False,
1495
+ enable_parallel_execution: bool = True,
1496
+ enable_self_reflection: bool = True,
1497
+ **llm_generation_kwargs
1498
+ ) -> Dict[str, Any]:
1499
+
1466
1500
  if not self.llm:
1467
1501
  return {"final_answer": "", "tool_calls": [], "sources": [], "error": "LLM binding not initialized."}
1468
- if max_reasoning_steps is None:
1469
- max_reasoning_steps = 10
1470
1502
 
1471
1503
  def log_event(desc, event_type=MSG_TYPE.MSG_TYPE_CHUNK, meta=None, event_id=None) -> Optional[str]:
1472
- if not streaming_callback:
1473
- return None
1504
+ if not streaming_callback: return None
1474
1505
  is_start = event_type == MSG_TYPE.MSG_TYPE_STEP_START
1475
1506
  event_id = str(uuid.uuid4()) if is_start and not event_id else event_id
1476
1507
  params = {"type": event_type, "description": desc, **(meta or {})}
1477
- if event_id:
1478
- params["id"] = event_id
1508
+ if event_id: params["id"] = event_id
1479
1509
  streaming_callback(desc, event_type, params)
1480
1510
  return event_id
1481
1511
 
1482
1512
  def log_prompt(title: str, prompt_text: str):
1483
- if not debug:
1484
- return
1513
+ if not debug: return
1485
1514
  ASCIIColors.cyan(f"** DEBUG: {title} **")
1486
1515
  ASCIIColors.magenta(prompt_text[-15000:])
1487
1516
  prompt_size = self.count_tokens(prompt_text)
1488
1517
  ASCIIColors.red(f"Prompt size:{prompt_size}/{self.llm.default_ctx_size}")
1489
1518
  ASCIIColors.cyan(f"** DEBUG: DONE **")
1490
1519
 
1491
- original_user_prompt, tool_calls_this_turn, sources_this_turn = prompt, [], []
1492
- asset_store: Dict[str, Dict] = {}
1493
- initial_state_parts = ["### Initial State", "- My goal is to address the user's request comprehensively."]
1494
- if images:
1495
- for img_b64 in images:
1496
- img_uuid = str(uuid.uuid4())
1497
- asset_store[img_uuid] = {"type": "image", "content": img_b64}
1498
- initial_state_parts.append(f"- User provided image, asset ID: {img_uuid}")
1499
- if context:
1500
- code_blocks = re.findall(r"```(?:\w+)?\n([\s\S]+?)\n```", context)
1501
- if code_blocks:
1502
- last_code_block = code_blocks[-1]
1503
- code_uuid = str(uuid.uuid4())
1504
- asset_store[code_uuid] = {"type": "code", "content": last_code_block}
1505
- initial_state_parts.append(f"- A code block was found in the context. It has been registered as asset ID: {code_uuid}")
1506
- current_scratchpad = "\n".join(initial_state_parts)
1507
-
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
-
1520
+ discovery_step_id = log_event("🔧 Setting up capabilities...", MSG_TYPE.MSG_TYPE_STEP_START)
1521
+ all_discovered_tools, visible_tools, rag_registry, rag_tool_specs = [], [], {}, {}
1513
1522
  if use_mcps and hasattr(self, 'mcp'):
1514
1523
  mcp_tools = self.mcp.discover_tools(force_refresh=True)
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
-
1524
+ if isinstance(use_mcps, list): all_discovered_tools.extend([t for t in mcp_tools if t["name"] in use_mcps])
1525
+ elif use_mcps is True: all_discovered_tools.extend(mcp_tools)
1520
1526
  if use_data_store:
1521
1527
  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
1528
+ tool_name, description, call_fn = f"research::{name}", f"Queries the '{name}' knowledge base.", None
1529
+ if callable(info): call_fn = info
1527
1530
  elif isinstance(info, dict):
1528
- if "callable" in info and callable(info["callable"]):
1529
- call_fn = info["callable"]
1531
+ if "callable" in info and callable(info["callable"]): call_fn = info["callable"]
1530
1532
  description = info.get("description", description)
1531
1533
  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
- })
1534
+ visible_tools.append({"name": tool_name, "description": description, "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}})
1546
1535
  rag_registry[tool_name] = call_fn
1547
1536
  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
1537
  visible_tools.extend(all_discovered_tools)
1538
+ built_in_tools = [{"name": "local_tools::final_answer", "description": "Provide the final answer directly to the user.", "input_schema": {}}]
1539
+ if getattr(self, "tti", None): built_in_tools.append({"name": "local_tools::generate_image", "description": "Generate an image from a text description.", "input_schema": {"type": "object", "properties": {"prompt": {"type": "string"}}, "required": ["prompt"]}})
1540
+ all_visible_tools = visible_tools + built_in_tools
1541
+ tool_summary = "\n".join([f"- {t['name']}: {t['description']}" for t in all_visible_tools[:15]])
1542
+ log_event(f"✅ Ready with {len(all_visible_tools)} capabilities", MSG_TYPE.MSG_TYPE_STEP_END, event_id=discovery_step_id)
1552
1543
 
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
- ]
1544
+ triage_step_id = log_event("🤔 Analyzing the best approach...", MSG_TYPE.MSG_TYPE_STEP_START)
1545
+ strategy = "COMPLEX_PLAN"
1546
+ strategy_data = {}
1547
+ try:
1548
+ triage_prompt = f"""Analyze the user's request and determine the most efficient strategy.
1549
+ USER REQUEST: "{prompt}"
1550
+ AVAILABLE TOOLS:\n{tool_summary}
1551
+ Choose a strategy:
1552
+ - "DIRECT_ANSWER": For greetings or simple questions that need no tools.
1553
+ - "REQUEST_CLARIFICATION": If the request is ambiguous and you need more information from the user.
1554
+ - "SINGLE_TOOL": If the request can be resolved with one tool call.
1555
+ - "COMPLEX_PLAN": For multi-step requests requiring multiple tools or complex reasoning.
1556
+
1557
+ Provide your decision as JSON: {{"thought": "...", "strategy": "...", "text_output": "Your direct answer or clarification question.", "required_tool_name": "..."}}"""
1558
+
1559
+ triage_schema = {
1560
+ "thought": "string", "strategy": "string",
1561
+ "text_output": "string", "required_tool_name": "string"
1562
+ }
1563
+ strategy_data = self.generate_structured_content(prompt=triage_prompt, schema=triage_schema, temperature=0.1, **llm_generation_kwargs)
1564
+ strategy = strategy_data.get("strategy") if strategy_data else "COMPLEX_PLAN"
1565
+ except Exception as e:
1566
+ log_event(f"Triage failed, defaulting to complex plan. Error: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION, event_id=triage_step_id)
1557
1567
 
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
- })
1568
+ if force_mcp_use and strategy == "DIRECT_ANSWER":
1569
+ strategy = "COMPLEX_PLAN"
1570
+ log_event(f"✅ Approach decided: {strategy.replace('_', ' ').title()}", MSG_TYPE.MSG_TYPE_STEP_END, event_id=triage_step_id)
1564
1571
 
1565
- all_visible_tools = visible_tools + built_in_tools
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
- )
1572
+ if strategy == "DIRECT_ANSWER":
1573
+ final_answer = strategy_data.get("text_output", "I can help with that.")
1574
+ if streaming_callback: streaming_callback(final_answer, MSG_TYPE.MSG_TYPE_FULL, {})
1575
+ return {"final_answer": final_answer, "tool_calls": [], "sources": [], "error": None, "clarification_required": False, "final_scratchpad": f"Strategy: DIRECT_ANSWER\nThought: {strategy_data.get('thought')}"}
1573
1576
 
1574
- for i in range(max_reasoning_steps):
1575
- reasoning_step_id = log_event(f"Reasoning Step {i+1}/{max_reasoning_steps}", MSG_TYPE.MSG_TYPE_STEP_START)
1577
+ if strategy == "REQUEST_CLARIFICATION":
1578
+ clarification_question = strategy_data.get("text_output", "Could you please provide more details?")
1579
+ return {"final_answer": clarification_question, "tool_calls": [], "sources": [], "error": None, "clarification_required": True, "final_scratchpad": f"Strategy: REQUEST_CLARIFICATION\nThought: {strategy_data.get('thought')}"}
1580
+
1581
+ if strategy == "SINGLE_TOOL":
1582
+ synthesis_id = log_event("⚡ Taking a direct approach...", MSG_TYPE.MSG_TYPE_STEP_START)
1576
1583
  try:
1577
- reasoning_prompt = f"""--- AVAILABLE ACTIONS ---
1578
- {formatted_tools_list}
1584
+ tool_name = strategy_data.get("required_tool_name")
1585
+ tool_spec = next((t for t in all_visible_tools if t['name'] == tool_name), None)
1586
+ if not tool_spec:
1587
+ raise ValueError(f"LLM chose an unavailable tool: '{tool_name}'")
1588
+
1589
+ param_prompt = f"""Given the user request, generate the correct parameters for the selected tool.
1590
+ USER REQUEST: "{prompt}"
1591
+ SELECTED TOOL: {json.dumps(tool_spec, indent=2)}
1592
+ Output ONLY the JSON for the tool's parameters: {{"tool_params": {{...}}}}"""
1593
+ param_data = self.generate_structured_content(prompt=param_prompt, schema={"tool_params": "object"}, temperature=0.1, **llm_generation_kwargs)
1594
+ tool_params = param_data.get("tool_params", {}) if param_data else {}
1595
+
1596
+ start_time, sources, tool_result = time.time(), [], {}
1597
+ if tool_name in rag_registry:
1598
+ query = tool_params.get("query", prompt)
1599
+ rag_fn = rag_registry[tool_name]
1600
+ raw_results = rag_fn(query=query, rag_top_k=rag_top_k, rag_min_similarity_percent=rag_min_similarity_percent)
1601
+ docs = [d for d in (raw_results.get("results", []) if isinstance(raw_results, dict) else raw_results or [])]
1602
+ tool_result = {"status": "success", "results": docs}
1603
+ sources = [{"source": tool_name, "metadata": d.get("metadata", {}), "score": d.get("score", 0.0)} for d in docs]
1604
+ elif hasattr(self, "mcp") and "local_tools" not in tool_name:
1605
+ tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1606
+ else:
1607
+ tool_result = {"status": "failure", "error": f"Tool '{tool_name}' could not be executed in single-step mode."}
1608
+
1609
+ if tool_result.get("status") != "success":
1610
+ error_detail = tool_result.get("error", "Unknown tool error in single-step mode.")
1611
+ raise RuntimeError(error_detail)
1579
1612
 
1580
- --- YOUR INTERNAL SCRATCHPAD ---
1581
- {current_scratchpad}
1582
- --- END SCRATCHPAD ---
1613
+ response_time = time.time() - start_time
1614
+ tool_calls_this_turn = [{"name": tool_name, "params": tool_params, "result": tool_result, "response_time": response_time}]
1615
+
1616
+ synthesis_prompt = f"""The user asked: "{prompt}"
1617
+ I used the tool '{tool_name}' and got this result: {json.dumps(tool_result, indent=2)}
1618
+ Synthesize a direct, user-friendly final answer."""
1619
+ final_answer = self.generate_text(prompt=synthesis_prompt, system_prompt=system_prompt, stream=streaming_callback is not None, streaming_callback=streaming_callback, temperature=final_answer_temperature, **llm_generation_kwargs)
1620
+ final_answer = self.remove_thinking_blocks(final_answer)
1621
+
1622
+ log_event("✅ Direct answer ready!", MSG_TYPE.MSG_TYPE_STEP_END, event_id=synthesis_id)
1623
+ return {"final_answer": final_answer, "tool_calls": tool_calls_this_turn, "sources": sources, "error": None, "clarification_required": False, "final_scratchpad": f"Strategy: SINGLE_TOOL\nTool: {tool_name}\nResult: {json.dumps(tool_result)}"}
1583
1624
 
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:
1625
+ except Exception as e:
1626
+ log_event(f"Direct approach failed: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION, event_id=synthesis_id)
1627
+ log_event("Escalating to a more detailed plan.", MSG_TYPE.MSG_TYPE_INFO)
1628
+
1629
+ return self._execute_complex_reasoning_loop(
1630
+ prompt=prompt, context=context, system_prompt=system_prompt,
1631
+ reasoning_system_prompt=reasoning_system_prompt, images=images,
1632
+ max_reasoning_steps=max_reasoning_steps, decision_temperature=decision_temperature,
1633
+ final_answer_temperature=final_answer_temperature, streaming_callback=streaming_callback,
1634
+ debug=debug, enable_self_reflection=enable_self_reflection,
1635
+ all_visible_tools=all_visible_tools, rag_registry=rag_registry, rag_tool_specs=rag_tool_specs,
1636
+ log_event_fn=log_event, log_prompt_fn=log_prompt,
1637
+ **llm_generation_kwargs
1638
+ )
1588
1639
 
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
- }}
1640
+ def _execute_complex_reasoning_loop(
1641
+ self, prompt, context, system_prompt, reasoning_system_prompt, images,
1642
+ max_reasoning_steps, decision_temperature, final_answer_temperature,
1643
+ streaming_callback, debug, enable_self_reflection, all_visible_tools,
1644
+ rag_registry, rag_tool_specs, log_event_fn, log_prompt_fn, **llm_generation_kwargs
1645
+ ) -> Dict[str, Any]:
1646
+
1647
+ planner, memory_manager, performance_tracker = TaskPlanner(self), MemoryManager(), ToolPerformanceTracker()
1648
+
1649
+ def _get_friendly_action_description(tool_name, requires_code, requires_image):
1650
+ if tool_name == "local_tools::final_answer": return "📋 Ready to provide your answer"
1651
+ if tool_name == "local_tools::request_clarification": return "❓ Need to ask for clarification"
1652
+ if tool_name == "local_tools::generate_image": return "🎨 Creating an image for you"
1653
+ if "research::" in tool_name: return f"🔍 Searching {tool_name.split('::')[-1]} for information"
1654
+ if requires_code: return "💻 Working on a coding solution"
1655
+ if requires_image: return "🖼️ Analyzing the provided images"
1656
+ return f"🔧 Using {tool_name.replace('_', ' ').replace('::', ' - ').title()}"
1597
1657
 
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."
1615
- continue
1658
+ original_user_prompt, tool_calls_this_turn, sources_this_turn = prompt, [], []
1659
+ asset_store: Dict[str, Dict] = {}
1660
+
1661
+ planning_step_id = log_event_fn("📋 Creating a detailed plan...", MSG_TYPE.MSG_TYPE_STEP_START)
1662
+ execution_plan = planner.decompose_task(original_user_prompt, context or "")
1663
+ log_event_fn(f" Plan ready ({len(execution_plan.tasks)} steps)", MSG_TYPE.MSG_TYPE_STEP_END, event_id=planning_step_id)
1664
+
1665
+ initial_state_parts = [f"### Execution Plan\n- Total tasks: {len(execution_plan.tasks)}"]
1666
+ for i, task in enumerate(execution_plan.tasks): initial_state_parts.append(f" {i+1}. {task.description}")
1667
+ if images:
1668
+ for img_b64 in images:
1669
+ img_uuid = str(uuid.uuid4())
1670
+ asset_store[img_uuid] = {"type": "image", "content": img_b64, "source": "user"}
1671
+ initial_state_parts.append(f"- User provided image, asset ID: {img_uuid}")
1672
+ current_scratchpad = "\n".join(initial_state_parts)
1616
1673
 
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)
1621
- current_scratchpad += f"\n\n### Step {i+1}: Thought\n{thought}"
1622
- log_event("Decision taken", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name, "requires_code": requires_code, "requires_image": requires_image})
1674
+ formatted_tools_list = "\n".join([f"**{t['name']}**: {t['description']}" for t in all_visible_tools])
1675
+ completed_tasks, current_task_index = set(), 0
1676
+
1677
+ for i in range(max_reasoning_steps):
1678
+ step_desc = f"🤔 Working on: {execution_plan.tasks[current_task_index].description}" if current_task_index < len(execution_plan.tasks) else f"🤔 Analyzing next steps... ({i+1}/{max_reasoning_steps})"
1679
+ reasoning_step_id = log_event_fn(step_desc, MSG_TYPE.MSG_TYPE_STEP_START)
1680
+
1681
+ try:
1682
+ if len(current_scratchpad) > 12000:
1683
+ current_scratchpad = memory_manager.compress_scratchpad(current_scratchpad, original_user_prompt, 8000)
1684
+
1685
+ reasoning_prompt = f"""--- AVAILABLE ACTIONS ---\n{formatted_tools_list}\n--- YOUR INTERNAL SCRATCHPAD ---\n{current_scratchpad}\n--- END SCRATCHPAD ---\n
1686
+ INSTRUCTIONS: Observe, think, and then act. Choose the single best next action to achieve: "{original_user_prompt}".
1687
+ Produce ONLY this JSON: {{"thought": "short reasoning", "action": {{"tool_name": "...", "requires_code_input": false, "requires_image_input": false}}}}"""
1688
+ decision_data = self.generate_structured_content(prompt=reasoning_prompt, schema={"thought": "string", "action": "object"}, system_prompt=reasoning_system_prompt, temperature=decision_temperature, **llm_generation_kwargs)
1689
+
1690
+ if not (decision_data and isinstance(decision_data.get("action"), dict)):
1691
+ log_event_fn("LLM failed to produce a valid action JSON.", MSG_TYPE.MSG_TYPE_WARNING, event_id=reasoning_step_id)
1692
+ current_scratchpad += "\n\n### Step Failure\n- Error: Invalid decision JSON from LLM."
1693
+ continue
1623
1694
 
1624
- if tool_name == "local_tools::final_answer":
1625
- break
1695
+ action = decision_data.get("action", {})
1696
+ tool_name, requires_code, requires_image = action.get("tool_name"), action.get("requires_code_input", False), action.get("requires_image_input", False)
1697
+ current_scratchpad += f"\n\n### Step {i+1}: Thought\n{decision_data.get('thought', '')}"
1698
+
1699
+ log_event_fn(_get_friendly_action_description(tool_name, requires_code, requires_image), MSG_TYPE.MSG_TYPE_STEP)
1700
+ if tool_name == "local_tools::final_answer": break
1626
1701
  if tool_name == "local_tools::request_clarification":
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
- }
1702
+ clarification_prompt = f"Based on your thought process, what is the single question you need to ask the user?\n\nSCRATCHPAD:\n{current_scratchpad}\n\nQUESTION:"
1703
+ question = self.generate_text(clarification_prompt)
1704
+ return {"final_answer": self.remove_thinking_blocks(question), "clarification_required": True, "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "error": None}
1635
1705
 
1636
- prepared_assets = {}
1706
+ param_assets = {}
1637
1707
  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
-
1708
+ code_prompt = f"Generate only the raw code required for the current step.\n\nSCRATCHPAD:\n{current_scratchpad}\n\nCODE:"
1709
+ code_content = self.generate_code(prompt=code_prompt, **llm_generation_kwargs)
1710
+ code_uuid = f"code_asset_{uuid.uuid4()}"
1711
+ asset_store[code_uuid] = {"type": "code", "content": code_content}
1712
+ param_assets['code_asset_id'] = code_uuid
1713
+ log_event_fn("Code asset generated.", MSG_TYPE.MSG_TYPE_STEP)
1654
1714
  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"]
1694
- else:
1695
- log_event("Parameter generation returned empty", MSG_TYPE.MSG_TYPE_WARNING, meta={"param_raw": str(param_data)})
1696
-
1715
+ image_assets = [asset_id for asset_id, asset in asset_store.items() if asset['type'] == 'image' and asset.get('source') == 'user']
1716
+ if image_assets:
1717
+ param_assets['image_asset_id'] = image_assets[0]
1718
+
1719
+ param_prompt = f"""Fill the parameters for the tool: '{tool_name}'. Available assets: {json.dumps(param_assets)}.
1720
+ SCRATCHPAD:\n{current_scratchpad}\n
1721
+ Output only: {{"tool_params": {{...}}}}"""
1722
+ param_data = self.generate_structured_content(prompt=param_prompt, schema={"tool_params": "object"}, temperature=decision_temperature, **llm_generation_kwargs)
1723
+ tool_params = param_data.get("tool_params", {}) if param_data else {}
1724
+
1697
1725
  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)
1726
+ if isinstance(data, dict): return {k: _hydrate(v, store) for k, v in data.items()}
1727
+ if isinstance(data, list): return [_hydrate(item, store) for item in data]
1728
+ if isinstance(data, str) and "asset_" in data and data in store: return store[data].get("content", data)
1704
1729
  return data
1705
-
1706
1730
  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."}
1731
+
1732
+ start_time, tool_result = time.time(), {"status": "failure", "error": f"Tool '{tool_name}' failed to execute."}
1710
1733
  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:
1734
+ if tool_name in rag_registry:
1723
1735
  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 if top_k else None, filters=filters if filters else None)
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.'}"
1736
+ top_k, min_sim = rag_tool_specs[tool_name]["default_top_k"], rag_tool_specs[tool_name]["default_min_sim"]
1737
+ raw_results = rag_registry[tool_name](query=query, top_k=top_k)
1738
+ raw_iter = raw_results["results"] if isinstance(raw_results, dict) and "results" in raw_results else raw_results
1739
+ docs = [{"text": d.get("text", str(d)), "score": d.get("score", 0)*100, "metadata": d.get("metadata", {})} for d in raw_iter or []]
1740
+ kept = [x for x in docs if x['score'] >= min_sim]
1741
+ tool_result = {"status": "success", "results": kept, "dropped": len(docs) - len(kept)}
1742
+ sources_this_turn.extend([{"source": tool_name, "metadata": x["metadata"], "score": x["score"]} for x in kept])
1753
1743
  elif hasattr(self, "mcp"):
1754
- log_event("MCP tool call start", MSG_TYPE.MSG_TYPE_STEP, meta={"tool_name": tool_name})
1755
1744
  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
1745
  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)})
1746
+ error_msg = f"Exception during '{tool_name}' execution: {e}"
1747
+ log_event_fn(error_msg, MSG_TYPE.MSG_TYPE_EXCEPTION)
1748
+ tool_result = {"status": "failure", "error": error_msg}
1762
1749
 
1763
- sanitized_result = tool_result.copy() if isinstance(tool_result, dict) else {"raw_output": str(tool_result)}
1764
- observation_text = f"```json\n{json.dumps(sanitized_result, indent=2)}\n```"
1765
- tool_calls_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1750
+ response_time = time.time() - start_time
1751
+ success = tool_result.get("status") == "success"
1752
+ performance_tracker.record_tool_usage(tool_name, success, 0.8, response_time, tool_result.get("error"))
1753
+
1754
+ if success and current_task_index < len(execution_plan.tasks):
1755
+ execution_plan.tasks[current_task_index].status = TaskStatus.COMPLETED
1756
+ current_task_index += 1
1757
+
1758
+ observation_text = f"```json\n{json.dumps(tool_result, indent=2)}\n```"
1759
+ tool_calls_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result, "response_time": response_time})
1766
1760
  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})
1768
-
1769
- log_event(f"Finished reasoning step {i+1}", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id)
1761
+
1762
+ if success:
1763
+ log_event_fn(f" Step completed successfully", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id)
1764
+ else:
1765
+ error_detail = tool_result.get("error", "No error detail provided.")
1766
+ log_event_fn(f"Tool reported failure: {error_detail}", MSG_TYPE.MSG_TYPE_WARNING)
1767
+ log_event_fn(f"⚠️ Step completed with issues", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id, meta={"error": error_detail})
1768
+
1769
+ if len(completed_tasks) == len(execution_plan.tasks): break
1770
+
1770
1771
  except Exception as ex:
1772
+ log_event_fn(f"An unexpected error occurred in reasoning loop: {ex}", MSG_TYPE.MSG_TYPE_EXCEPTION, event_id=reasoning_step_id)
1771
1773
  trace_exception(ex)
1772
- log_event("Error in reasoning loop", MSG_TYPE.MSG_TYPE_EXCEPTION, meta={"error": str(ex)}, event_id=reasoning_step_id)
1773
-
1774
- synthesis_id = log_event("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START)
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."""
1784
- final_synthesis_images = [img for img in (images or [])] + [asset['content'] for asset in asset_store.values() if asset['type'] == 'image']
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
- )
1795
- if isinstance(final_answer_text, dict) and "error" in final_answer_text:
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"]}
1774
+ log_event_fn("⚠️ Encountered an issue, adjusting approach...", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id)
1797
1775
 
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'/>"
1776
+ if enable_self_reflection and len(tool_calls_this_turn) > 1:
1777
+ reflection_id = log_event_fn("🤔 Reviewing my work...", MSG_TYPE.MSG_TYPE_STEP_START)
1778
+ try:
1779
+ reflection_prompt = f"""Review the user request and your work. Was the goal achieved effectively?
1780
+ REQUEST: "{original_user_prompt}"
1781
+ SCRATCHPAD:\n{current_scratchpad}\n
1782
+ JSON assessment: {{"goal_achieved": true, "effectiveness_score": 0.8, "summary": "..."}}"""
1783
+ reflection_data = self.generate_structured_content(prompt=reflection_prompt, schema={"goal_achieved": "boolean", "effectiveness_score": "number", "summary": "string"}, temperature=0.3, **llm_generation_kwargs)
1784
+ if reflection_data: current_scratchpad += f"\n\n### Self-Reflection\n- Goal Achieved: {reflection_data.get('goal_achieved')}\n- Effectiveness: {reflection_data.get('effectiveness_score')}"
1785
+ log_event_fn("✅ Quality check completed", MSG_TYPE.MSG_TYPE_STEP_END, event_id=reflection_id)
1786
+ except Exception as e:
1787
+ log_event_fn(f"Self-review failed: {e}", MSG_TYPE.MSG_TYPE_WARNING, event_id=reflection_id)
1802
1788
 
1803
- log_event("Finished synthesizing answer.", MSG_TYPE.MSG_TYPE_STEP_END, event_id=synthesis_id)
1789
+ synthesis_id = log_event_fn("📝 Preparing your complete answer...", MSG_TYPE.MSG_TYPE_STEP_START)
1790
+ final_answer_prompt = f"""Synthesize a comprehensive, user-friendly final answer based on your complete analysis.
1791
+ USER REQUEST: "{original_user_prompt}"
1792
+ FULL SCRATCHPAD:\n{current_scratchpad}\n---
1793
+ FINAL ANSWER:"""
1794
+
1795
+ final_answer_text = self.generate_text(prompt=final_answer_prompt, system_prompt=system_prompt, stream=streaming_callback is not None, streaming_callback=streaming_callback, temperature=final_answer_temperature, **llm_generation_kwargs)
1796
+ if isinstance(final_answer_text, dict) and "error" in final_answer_text:
1797
+ return {"final_answer": "", "error": final_answer_text["error"], "final_scratchpad": current_scratchpad}
1798
+
1799
+ final_answer = self.remove_thinking_blocks(final_answer_text)
1800
+ log_event_fn("✅ Answer ready!", MSG_TYPE.MSG_TYPE_STEP_END, event_id=synthesis_id)
1804
1801
 
1802
+ overall_confidence = sum(c.get('confidence', 0.5) for c in tool_calls_this_turn) / max(len(tool_calls_this_turn), 1)
1805
1803
  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
1804
+ "final_answer": final_answer, "final_scratchpad": current_scratchpad,
1805
+ "tool_calls": tool_calls_this_turn, "sources": sources_this_turn,
1806
+ "performance_stats": {"total_steps": len(tool_calls_this_turn), "average_confidence": overall_confidence},
1807
+ "clarification_required": False, "overall_confidence": overall_confidence, "error": None
1812
1808
  }
1813
1809
 
1810
+
1814
1811
  def generate_code(
1815
1812
  self,
1816
1813
  prompt:str,
@@ -28,7 +28,8 @@ try:
28
28
  AutoPipelineForImage2Image,
29
29
  AutoPipelineForInpainting,
30
30
  DiffusionPipeline,
31
- StableDiffusionPipeline
31
+ StableDiffusionPipeline,
32
+
32
33
  )
33
34
  from diffusers.utils import load_image
34
35
  from PIL import Image
@@ -49,14 +50,14 @@ BindingName = "DiffusersTTIBinding_Impl"
49
50
  CIVITAI_MODELS = {
50
51
  "realistic-vision-v6": {
51
52
  "display_name": "Realistic Vision V6.0",
52
- "url": "https://civitai.com/api/download/models/130072",
53
+ "url": "https://civitai.com/api/download/models/501240?type=Model&format=SafeTensor&size=pruned&fp=fp16",
53
54
  "filename": "realisticVisionV60_v60B1.safetensors",
54
55
  "description": "Photorealistic SD1.5 checkpoint.",
55
56
  "owned_by": "civitai"
56
57
  },
57
58
  "absolute-reality": {
58
59
  "display_name": "Absolute Reality",
59
- "url": "https://civitai.com/api/download/models/132760",
60
+ "url": "https://civitai.com/api/download/models/132760?type=Model&format=SafeTensor&size=pruned&fp=fp16",
60
61
  "filename": "absolutereality_v181.safetensors",
61
62
  "description": "General realistic SD1.5.",
62
63
  "owned_by": "civitai"
@@ -77,57 +78,57 @@ CIVITAI_MODELS = {
77
78
  },
78
79
  "lyriel-v1.6": {
79
80
  "display_name": "Lyriel v1.6",
80
- "url": "https://civitai.com/api/download/models/92407",
81
+ "url": "https://civitai.com/api/download/models/72396?type=Model&format=SafeTensor&size=full&fp=fp16",
81
82
  "filename": "lyriel_v16.safetensors",
82
83
  "description": "Fantasy/stylized SD1.5.",
83
84
  "owned_by": "civitai"
84
85
  },
85
- "anything-v5": {
86
- "display_name": "Anything V5",
87
- "url": "https://civitai.com/api/download/models/9409",
88
- "filename": "anythingV5_PrtRE.safetensors",
89
- "description": "Anime SD1.5.",
86
+ "ui_icons": {
87
+ "display_name": "UI Icons",
88
+ "url": "https://civitai.com/api/download/models/367044?type=Model&format=SafeTensor&size=full&fp=fp16",
89
+ "filename": "uiIcons_v10.safetensors",
90
+ "description": "A model for generating UI icons.",
90
91
  "owned_by": "civitai"
91
92
  },
92
93
  "meinamix": {
93
94
  "display_name": "MeinaMix",
94
- "url": "https://civitai.com/api/download/models/119057",
95
+ "url": "https://civitai.com/api/download/models/948574?type=Model&format=SafeTensor&size=pruned&fp=fp16",
95
96
  "filename": "meinamix_meinaV11.safetensors",
96
97
  "description": "Anime/illustration SD1.5.",
97
98
  "owned_by": "civitai"
98
99
  },
99
100
  "rpg-v5": {
100
101
  "display_name": "RPG v5",
101
- "url": "https://civitai.com/api/download/models/137379",
102
+ "url": "https://civitai.com/api/download/models/124626?type=Model&format=SafeTensor&size=pruned&fp=fp16",
102
103
  "filename": "rpg_v5.safetensors",
103
104
  "description": "RPG assets SD1.5.",
104
105
  "owned_by": "civitai"
105
106
  },
106
107
  "pixel-art-xl": {
107
108
  "display_name": "Pixel Art XL",
108
- "url": "https://civitai.com/api/download/models/252919",
109
+ "url": "https://civitai.com/api/download/models/135931?type=Model&format=SafeTensor",
109
110
  "filename": "pixelartxl_v11.safetensors",
110
111
  "description": "Pixel art SDXL.",
111
112
  "owned_by": "civitai"
112
113
  },
113
114
  "lowpoly-world": {
114
115
  "display_name": "Lowpoly World",
115
- "url": "https://civitai.com/api/download/models/90299",
116
- "filename": "lowpoly_world_v10.safetensors",
116
+ "url": "https://civitai.com/api/download/models/146502?type=Model&format=SafeTensor",
117
+ "filename": "LowpolySDXL.safetensors",
117
118
  "description": "Lowpoly style SD1.5.",
118
119
  "owned_by": "civitai"
119
120
  },
120
121
  "toonyou": {
121
122
  "display_name": "ToonYou",
122
- "url": "https://civitai.com/api/download/models/152361",
123
+ "url": "https://civitai.com/api/download/models/125771?type=Model&format=SafeTensor&size=pruned&fp=fp16",
123
124
  "filename": "toonyou_beta6.safetensors",
124
125
  "description": "Cartoon/Disney SD1.5.",
125
126
  "owned_by": "civitai"
126
127
  },
127
128
  "papercut": {
128
129
  "display_name": "Papercut",
129
- "url": "https://civitai.com/api/download/models/45579",
130
- "filename": "papercut_v1.safetensors",
130
+ "url": "https://civitai.com/api/download/models/133503?type=Model&format=SafeTensor",
131
+ "filename": "papercut.safetensors",
131
132
  "description": "Paper cutout SD1.5.",
132
133
  "owned_by": "civitai"
133
134
  }
@@ -470,6 +471,7 @@ class DiffusersTTIBinding_Impl(LollmsTTIBinding):
470
471
  {"family": "SD 1.x", "model_name": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion 1.5", "desc": "Classic SD1.5."},
471
472
  {"family": "SD 2.x", "model_name": "stabilityai/stable-diffusion-2-1", "display_name": "Stable Diffusion 2.1", "desc": "SD2.1 base."},
472
473
  {"family": "SD3", "model_name": "stabilityai/stable-diffusion-3-medium-diffusers", "display_name": "Stable Diffusion 3 Medium", "desc": "SD3 medium."},
474
+ {"family": "Qwen", "model_name": "Qwen/Qwen-Image", "display_name": "Qwen Image Edit", "desc": "Dedicated image generation."},
473
475
  {"family": "Specialized", "model_name": "playgroundai/playground-v2.5-1024px-aesthetic", "display_name": "Playground v2.5", "desc": "High aesthetic 1024."},
474
476
  {"family": "Editors", "model_name": "Qwen/Qwen-Image-Edit", "display_name": "Qwen Image Edit", "desc": "Dedicated image editing."}
475
477
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lollms_client
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: A client library for LoLLMs generate endpoint
5
5
  Author-email: ParisNeo <parisneoai@gmail.com>
6
6
  License: Apache Software License
@@ -1,6 +1,7 @@
1
- lollms_client/__init__.py,sha256=SMot7i85VNJMIL7Zf7trWYlvTgWYlRmwZpF8qchlIyI,1146
1
+ lollms_client/__init__.py,sha256=_24wmEi1MdUFdBAMtSR43pb3i9zg3sNCIRIt_HqEZKc,1146
2
+ lollms_client/lollms_agentic.py,sha256=pQiMEuB_XkG29-SW6u4KTaMFPr6eKqacInggcCuCW3k,13914
2
3
  lollms_client/lollms_config.py,sha256=goEseDwDxYJf3WkYJ4IrLXwg3Tfw73CXV2Avg45M_hE,21876
3
- lollms_client/lollms_core.py,sha256=QIsKQfSWDSD2gzVrlVZmis3VdEf4_95d4ynYGB4DIQI,171085
4
+ lollms_client/lollms_core.py,sha256=mlkOVFS39oOd8n2MeY-vbAjntx7siRZA3kj2_swOOqs,176415
4
5
  lollms_client/lollms_discussion.py,sha256=4vOnXJp4nLDtL2gRmnkTB4-mjYyIHsgp35pRSJPeT9U,117527
5
6
  lollms_client/lollms_js_analyzer.py,sha256=01zUvuO2F_lnUe_0NLxe1MF5aHE1hO8RZi48mNPv-aw,8361
6
7
  lollms_client/lollms_llm_binding.py,sha256=5-Vknm0YILPd6ZiwZynsXMfns__Yd_1tDDc2fciRiiA,25020
@@ -48,7 +49,7 @@ lollms_client/stt_bindings/lollms/__init__.py,sha256=9Vmn1sQQZKLGLe7nZnc-0LnNeSY
48
49
  lollms_client/stt_bindings/whisper/__init__.py,sha256=1Ej67GdRKBy1bba14jMaYDYHiZkxJASkWm5eF07ztDQ,15363
49
50
  lollms_client/stt_bindings/whispercpp/__init__.py,sha256=xSAQRjAhljak3vWCpkP0Vmdb6WmwTzPjXyaIB85KLGU,21439
50
51
  lollms_client/tti_bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- lollms_client/tti_bindings/diffusers/__init__.py,sha256=e1qrhiAQI_J-C_PKGIz2EEmtonw-uKAa9bw2N4qUP68,40092
52
+ lollms_client/tti_bindings/diffusers/__init__.py,sha256=KKcIBIQvavAyIwuqappRqLghUOcm-XOP_Y4SL_pEcWY,40669
52
53
  lollms_client/tti_bindings/gemini/__init__.py,sha256=f9fPuqnrBZ1Z-obcoP6EVvbEXNbNCSg21cd5efLCk8U,16707
53
54
  lollms_client/tti_bindings/lollms/__init__.py,sha256=5Tnsn4b17djvieQkcjtIDBm3qf0pg5ZWWov-4_2wmo0,8762
54
55
  lollms_client/tti_bindings/openai/__init__.py,sha256=YWJolJSQfIzTJvrLQVe8rQewP7rddf6z87g4rnp-lTs,4932
@@ -63,8 +64,8 @@ lollms_client/tts_bindings/piper_tts/__init__.py,sha256=0IEWG4zH3_sOkSb9WbZzkeV5
63
64
  lollms_client/tts_bindings/xtts/__init__.py,sha256=FgcdUH06X6ZR806WQe5ixaYx0QoxtAcOgYo87a2qxYc,18266
64
65
  lollms_client/ttv_bindings/__init__.py,sha256=UZ8o2izQOJLQgtZ1D1cXoNST7rzqW22rL2Vufc7ddRc,3141
65
66
  lollms_client/ttv_bindings/lollms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- lollms_client-1.3.1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
67
- lollms_client-1.3.1.dist-info/METADATA,sha256=vxRJoe8JCZ1v_mnehbmuOTYEzDLjlWVYKk2hL9chuS8,58549
68
- lollms_client-1.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
- lollms_client-1.3.1.dist-info/top_level.txt,sha256=Bk_kz-ri6Arwsk7YG-T5VsRorV66uVhcHGvb_g2WqgE,14
70
- lollms_client-1.3.1.dist-info/RECORD,,
67
+ lollms_client-1.3.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
68
+ lollms_client-1.3.2.dist-info/METADATA,sha256=EtcaolEitu1FW-jEpqkS5Osx4Fke7F-Zku3te1cAQyo,58549
69
+ lollms_client-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ lollms_client-1.3.2.dist-info/top_level.txt,sha256=Bk_kz-ri6Arwsk7YG-T5VsRorV66uVhcHGvb_g2WqgE,14
71
+ lollms_client-1.3.2.dist-info/RECORD,,