lollms-client 0.22.0__py3-none-any.whl → 0.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lollms-client might be problematic. Click here for more details.

@@ -7,6 +7,26 @@ from pathlib import Path
7
7
  import json
8
8
  from lollms_client import LollmsClient
9
9
  import subprocess
10
+ from typing import Optional, List, Dict, Any
11
+
12
+
13
+ MOCK_KNOWLEDGE_BASE = {
14
+ "python_basics.md": [
15
+ {"chunk_id": 1, "text": "Python is a high-level, interpreted programming language known for its readability and versatility. It was created by Guido van Rossum and first released in 1991."},
16
+ {"chunk_id": 2, "text": "Key features of Python include dynamic typing, automatic memory management (garbage collection), and a large standard library. It supports multiple programming paradigms, such as procedural, object-oriented, and functional programming."},
17
+ {"chunk_id": 3, "text": "Common applications of Python include web development (e.g., Django, Flask), data science (e.g., Pandas, NumPy, Scikit-learn), machine learning, artificial intelligence, automation, and scripting."},
18
+ ],
19
+ "javascript_info.js": [
20
+ {"chunk_id": 1, "text": "JavaScript is a scripting language primarily used for front-end web development to create interactive effects within web browsers. It is also used in back-end development (Node.js), mobile app development, and game development."},
21
+ {"chunk_id": 2, "text": "JavaScript is dynamically typed, prototype-based, and multi-paradigm. Along with HTML and CSS, it is one of the core technologies of the World Wide Web."},
22
+ {"chunk_id": 3, "text": "Popular JavaScript frameworks and libraries include React, Angular, Vue.js for front-end, and Express.js for Node.js back-end applications."},
23
+ ],
24
+ "ai_concepts.txt": [
25
+ {"chunk_id": 1, "text": "Artificial Intelligence (AI) refers to the simulation of human intelligence in machines that are programmed to think like humans and mimic their actions. The term may also be applied to any machine that exhibits traits associated with a human mind such as learning and problem-solving."},
26
+ {"chunk_id": 2, "text": "Machine Learning (ML) is a subset of AI that provides systems the ability to automatically learn and improve from experience without being explicitly programmed. Deep Learning (DL) is a further subset of ML based on artificial neural networks with representation learning."},
27
+ {"chunk_id": 3, "text": "Retrieval Augmented Generation (RAG) is an AI framework for improving the quality of LLM-generated responses by grounding the model on external sources of knowledge to supplement the LLM’s internal representation of information."},
28
+ ]
29
+ }
10
30
  # --- Dynamically adjust Python path to find lollms_client ---
11
31
  # This assumes the example script is in a directory, and 'lollms_client' is
12
32
  # in a sibling directory or a known relative path. Adjust as needed.
@@ -180,10 +200,54 @@ def main():
180
200
  return True # Continue streaming
181
201
 
182
202
  # --- 4. Use generate_with_mcp ---
203
+
204
+ def mock_rag_query_function(
205
+ query_text: str,
206
+ vectorizer_name: Optional[str] = None, # Ignored in mock
207
+ top_k: int = 3,
208
+ min_similarity_percent: float = 0.0 # Ignored in mock, simple keyword match
209
+ ) -> List[Dict[str, Any]]:
210
+ """
211
+ A mock RAG query function.
212
+ Performs a simple keyword search in the MOCK_KNOWLEDGE_BASE.
213
+ """
214
+ ASCIIColors.magenta(f" [MOCK RAG] Querying with: '{query_text}', top_k={top_k}")
215
+ results = []
216
+ query_lower = query_text.lower()
217
+
218
+ all_chunks = []
219
+ for file_path, chunks_in_file in MOCK_KNOWLEDGE_BASE.items():
220
+ for chunk_data in chunks_in_file:
221
+ all_chunks.append({"file_path": file_path, **chunk_data})
222
+
223
+ # Simple keyword matching and scoring (very basic)
224
+ scored_chunks = []
225
+ for chunk_info in all_chunks:
226
+ score = 0
227
+ for keyword in query_lower.split():
228
+ if keyword in chunk_info["text"].lower() and len(keyword)>2: # Basic relevance
229
+ score += 1
230
+ if "python" in query_lower and "python" in chunk_info["file_path"].lower(): score+=5
231
+ if "javascript" in query_lower and "javascript" in chunk_info["file_path"].lower(): score+=5
232
+ if "ai" in query_lower and "ai" in chunk_info["file_path"].lower(): score+=3
233
+
234
+
235
+ if score > 0 : # Only include if some keywords match
236
+ # Simulate similarity percentage (higher score = higher similarity)
237
+ similarity = min(100.0, score * 20.0 + 40.0) # Arbitrary scaling
238
+ if similarity >= min_similarity_percent:
239
+ scored_chunks.append({
240
+ "file_path": chunk_info["file_path"],
241
+ "chunk_text": chunk_info["text"],
242
+ "similarity_percent": similarity,
243
+ "_score_for_ranking": score # Internal score for sorting
244
+ })
183
245
  ASCIIColors.magenta("\n2. Calling generate_with_mcp to get current time...")
184
246
  time_prompt = "Hey assistant, what time is it right now?"
185
- time_response = client.generate_with_mcp(
247
+ time_response = client.generate_with_mcp_rag(
186
248
  prompt=time_prompt,
249
+ use_mcps=True,
250
+ use_data_store={"coding_store":mock_rag_query_function},
187
251
  streaming_callback=mcp_streaming_callback,
188
252
  interactive_tool_execution=False # Set to True to test interactive mode
189
253
  )
lollms_client/__init__.py CHANGED
@@ -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
 
10
10
 
11
- __version__ = "0.22.0" # Updated version
11
+ __version__ = "0.23.0" # Updated version
12
12
 
13
13
  # Optionally, you could define __all__ if you want to be explicit about exports
14
14
  __all__ = [
@@ -526,7 +526,8 @@ class LollmsClient():
526
526
  seed: Optional[int] = None,
527
527
  n_threads: Optional[int] = None,
528
528
  ctx_size: Optional[int] = None,
529
- streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None
529
+ streaming_callback: Optional[Callable[[str, MSG_TYPE, Dict], bool]] = None,
530
+ **kwargs
530
531
  ) -> Union[str, dict]:
531
532
  """
532
533
  High-level method to perform a chat generation using a LollmsDiscussion object.
@@ -558,7 +559,7 @@ class LollmsClient():
558
559
  discussion=discussion,
559
560
  branch_tip_id=branch_tip_id,
560
561
  n_predict=n_predict if n_predict is not None else self.default_n_predict,
561
- stream=stream if stream is not None else self.default_stream,
562
+ stream=stream if stream is not None else True if streaming_callback is not None else self.default_stream,
562
563
  temperature=temperature if temperature is not None else self.default_temperature,
563
564
  top_k=top_k if top_k is not None else self.default_top_k,
564
565
  top_p=top_p if top_p is not None else self.default_top_p,
@@ -1283,33 +1284,180 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1283
1284
  "error": None
1284
1285
  }
1285
1286
 
1287
+ # --- Start of modified/added methods ---
1288
+ def _synthesize_knowledge(
1289
+ self,
1290
+ previous_scratchpad: str,
1291
+ tool_name: str,
1292
+ tool_params: dict,
1293
+ tool_result: dict
1294
+ ) -> str:
1295
+ """
1296
+ A dedicated LLM call to interpret a tool's output and update the knowledge scratchpad.
1297
+ """
1298
+ # Sanitize tool_result for LLM to avoid sending large binary/base64 data
1299
+ sanitized_result = tool_result.copy()
1300
+ if 'image_path' in sanitized_result:
1301
+ sanitized_result['summary'] = f"An image was successfully generated and saved to '{sanitized_result['image_path']}'."
1302
+ # Remove keys that might contain large data if they exist
1303
+ sanitized_result.pop('image_base64', None)
1304
+ elif 'file_path' in sanitized_result and 'content' in sanitized_result:
1305
+ sanitized_result['summary'] = f"Content was successfully written to '{sanitized_result['file_path']}'."
1306
+ sanitized_result.pop('content', None)
1307
+
1308
+
1309
+ synthesis_prompt = (
1310
+ "You are a data analyst assistant. Your sole job is to interpret the output of a tool and integrate it into the existing research summary (knowledge scratchpad).\n\n"
1311
+ "--- PREVIOUS KNOWLEDGE SCRATCHPAD ---\n"
1312
+ f"{previous_scratchpad}\n\n"
1313
+ "--- ACTION JUST TAKEN ---\n"
1314
+ f"Tool Called: `{tool_name}`\n"
1315
+ f"Parameters: {json.dumps(tool_params)}\n\n"
1316
+ "--- RAW TOOL OUTPUT ---\n"
1317
+ f"```json\n{json.dumps(sanitized_result, indent=2)}\n```\n\n"
1318
+ "--- YOUR TASK ---\n"
1319
+ "Read the 'RAW TOOL OUTPUT' and explain what it means in plain language. Then, integrate this new information with the 'PREVIOUS KNOWLEDGE SCRATCHPAD' to create a new, complete, and self-contained summary.\n"
1320
+ "Your output should be ONLY the text of the new scratchpad, with no extra commentary or formatting.\n\n"
1321
+ "--- NEW KNOWLEDGE SCRATCHPAD ---\n"
1322
+ )
1323
+ new_scratchpad_text = self.generate_text(prompt=synthesis_prompt, n_predict=1024, temperature=0.0)
1324
+ return self.remove_thinking_blocks(new_scratchpad_text).strip()
1325
+ def generate_structured_content(
1326
+ self,
1327
+ prompt: str,
1328
+ template: Union[dict, list],
1329
+ system_prompt: Optional[str] = None,
1330
+ images: Optional[List[str]] = None,
1331
+ max_retries: int = 3,
1332
+ **kwargs
1333
+ ) -> Union[dict, list, None]:
1334
+ """
1335
+ Generates structured content (JSON) from a prompt, ensuring it matches a given template.
1336
+
1337
+ This method repeatedly calls the LLM until a valid JSON object that can be parsed
1338
+ and somewhat matches the template is returned, or until max_retries is reached.
1339
+
1340
+ Args:
1341
+ prompt (str): The main prompt to guide the LLM.
1342
+ template (Union[dict, list]): A Python dict or list representing the desired JSON structure.
1343
+ system_prompt (Optional[str], optional): An optional system prompt. Defaults to None.
1344
+ images (Optional[List[str]], optional): A list of image paths for multimodal prompts. Defaults to None.
1345
+ max_retries (int, optional): The maximum number of times to retry generation if parsing fails. Defaults to 3.
1346
+ **kwargs: Additional keyword arguments to pass to the underlying generate_text method.
1347
+
1348
+ Returns:
1349
+ Union[dict, list, None]: The parsed JSON object (as a Python dict or list), or None if it fails after all retries.
1350
+ """
1351
+ template_str = json.dumps(template, indent=4)
1352
+
1353
+ if not system_prompt:
1354
+ system_prompt = "You are a highly intelligent AI assistant that excels at generating structured data in JSON format."
1355
+
1356
+ final_system_prompt = (
1357
+ f"{system_prompt}\n\n"
1358
+ "You MUST generate a response that is a single, valid JSON object matching the structure of the template provided by the user. "
1359
+ "Your entire response should be enclosed in a single ```json markdown code block. "
1360
+ "Do not include any other text, explanations, or apologies outside of the JSON code block.\n"
1361
+ f"Here is the JSON template you must follow:\n{template_str}"
1362
+ )
1363
+
1364
+ current_prompt = prompt
1365
+ for attempt in range(max_retries):
1366
+ raw_llm_output = self.generate_text(
1367
+ prompt=current_prompt,
1368
+ system_prompt=final_system_prompt,
1369
+ images=images,
1370
+ **kwargs
1371
+ )
1372
+
1373
+ if not raw_llm_output:
1374
+ ASCIIColors.warning(f"Structured content generation failed (Attempt {attempt + 1}/{max_retries}): LLM returned an empty response.")
1375
+ current_prompt = f"You previously returned an empty response. Please try again and adhere strictly to the JSON format. \nOriginal prompt was: {prompt}"
1376
+ continue
1377
+
1378
+ try:
1379
+ # Use robust_json_parser which handles cleanup of markdown tags, comments, etc.
1380
+ parsed_json = robust_json_parser(raw_llm_output)
1381
+ # Optional: Add validation against the template's structure here if needed
1382
+ return parsed_json
1383
+ except (ValueError, json.JSONDecodeError) as e:
1384
+ ASCIIColors.warning(f"Structured content parsing failed (Attempt {attempt + 1}/{max_retries}). Error: {e}")
1385
+ trace_exception(e)
1386
+ # Prepare for retry with more explicit instructions
1387
+ current_prompt = (
1388
+ "Your previous response could not be parsed as valid JSON. Please review the error and the required template and try again. "
1389
+ "Ensure your entire output is a single, clean JSON object inside a ```json code block.\n\n"
1390
+ f"--- PARSING ERROR ---\n{str(e)}\n\n"
1391
+ f"--- YOUR PREVIOUS INVALID RESPONSE ---\n{raw_llm_output}\n\n"
1392
+ f"--- REQUIRED JSON TEMPLATE ---\n{template_str}\n\n"
1393
+ f"--- ORIGINAL PROMPT ---\n{prompt}"
1394
+ )
1395
+
1396
+ ASCIIColors.error("Failed to generate valid structured content after multiple retries.")
1397
+ return None
1398
+
1399
+ def _synthesize_knowledge(
1400
+ self,
1401
+ previous_scratchpad: str,
1402
+ tool_name: str,
1403
+ tool_params: dict,
1404
+ tool_result: dict
1405
+ ) -> str:
1406
+ """
1407
+ A dedicated LLM call to interpret a tool's output and update the knowledge scratchpad.
1408
+ """
1409
+ # Sanitize tool_result for LLM to avoid sending large binary/base64 data
1410
+ sanitized_result = tool_result.copy()
1411
+ if 'image_path' in sanitized_result:
1412
+ sanitized_result['summary'] = f"An image was successfully generated and saved to '{sanitized_result['image_path']}'."
1413
+ # Remove keys that might contain large data if they exist
1414
+ sanitized_result.pop('image_base64', None)
1415
+ elif 'file_path' in sanitized_result and 'content' in sanitized_result:
1416
+ sanitized_result['summary'] = f"Content was successfully written to '{sanitized_result['file_path']}'."
1417
+ sanitized_result.pop('content', None)
1418
+
1419
+
1420
+ synthesis_prompt = (
1421
+ "You are a data analyst assistant. Your sole job is to interpret the output of a tool and integrate it into the existing research summary (knowledge scratchpad).\n\n"
1422
+ "--- PREVIOUS KNOWLEDGE SCRATCHPAD ---\n"
1423
+ f"{previous_scratchpad}\n\n"
1424
+ "--- ACTION JUST TAKEN ---\n"
1425
+ f"Tool Called: `{tool_name}`\n"
1426
+ f"Parameters: {json.dumps(tool_params)}\n\n"
1427
+ "--- RAW TOOL OUTPUT ---\n"
1428
+ f"```json\n{json.dumps(sanitized_result, indent=2)}\n```\n\n"
1429
+ "--- YOUR TASK ---\n"
1430
+ "Read the 'RAW TOOL OUTPUT' and explain what it means in plain language. Then, integrate this new information with the 'PREVIOUS KNOWLEDGE SCRATCHPAD' to create a new, complete, and self-contained summary.\n"
1431
+ "Your output should be ONLY the text of the new scratchpad, with no extra commentary or formatting.\n\n"
1432
+ "--- NEW KNOWLEDGE SCRATCHPAD ---\n"
1433
+ )
1434
+ new_scratchpad_text = self.generate_text(prompt=synthesis_prompt, n_predict=1024, temperature=0.0)
1435
+ return self.remove_thinking_blocks(new_scratchpad_text).strip()
1436
+
1286
1437
  def generate_with_mcp_rag(
1287
1438
  self,
1288
1439
  prompt: str,
1289
- rag_query_function: Callable[[str, Optional[str], int, float], List[Dict[str, Any]]],
1440
+ use_mcps: Union[None, bool, List[str]] = None,
1441
+ use_data_store: Union[None, Dict[str, Callable]] = None,
1290
1442
  system_prompt: str = None,
1291
1443
  objective_extraction_system_prompt="Extract objectives",
1292
1444
  images: Optional[List[str]] = None,
1293
- tools: Optional[List[Dict[str, Any]]] = None,
1294
1445
  max_tool_calls: int = 10,
1295
1446
  max_llm_iterations: int = 15,
1296
1447
  tool_call_decision_temperature: float = 0.0,
1297
1448
  final_answer_temperature: float = None,
1298
1449
  streaming_callback: Optional[Callable[[str, MSG_TYPE, Optional[Dict], Optional[List]], bool]] = None,
1299
1450
  build_plan: bool = True,
1300
- rag_vectorizer_name: Optional[str] = None,
1301
1451
  rag_top_k: int = 5,
1302
1452
  rag_min_similarity_percent: float = 70.0,
1303
1453
  **llm_generation_kwargs
1304
1454
  ) -> Dict[str, Any]:
1305
1455
  """
1306
1456
  Generates a response using a stateful agent that can choose between calling standard
1307
- MCP tools and querying a RAG database, all within a unified reasoning loop.
1457
+ MCP tools and querying one or more RAG databases, all within a unified reasoning loop.
1308
1458
  """
1309
1459
  if not self.binding:
1310
1460
  return {"final_answer": "", "tool_calls": [], "error": "LLM binding not initialized."}
1311
- if not self.mcp:
1312
- return {"final_answer": "", "tool_calls": [], "error": "MCP binding not initialized."}
1313
1461
 
1314
1462
  # --- Initialize Agent State ---
1315
1463
  turn_history: List[Dict[str, Any]] = []
@@ -1320,48 +1468,56 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1320
1468
  tool_calls_made_this_turn = []
1321
1469
  llm_iterations = 0
1322
1470
 
1323
- # --- 1. Discover MCP Tools and Inject the RAG Tool ---
1324
- if tools is None:
1325
- try:
1326
- mcp_tools = self.mcp.discover_tools(force_refresh=True)
1327
- if not mcp_tools: ASCIIColors.warning("No MCP tools discovered.")
1328
- except Exception as e_disc:
1329
- return {"final_answer": "", "tool_calls": [], "error": f"Failed to discover MCP tools: {e_disc}"}
1330
- else:
1331
- mcp_tools = tools
1332
-
1333
- # Define the RAG tool and add it to the list
1334
- rag_tool_definition = {
1335
- "name": "research::query_database",
1336
- "description": (
1337
- "Queries a vector database to find relevant text chunks based on a natural language query. "
1338
- "Use this to gather information, answer questions, or find context for a task before using other tools."
1339
- ),
1340
- "input_schema": {
1341
- "type": "object",
1342
- "properties": {
1343
- "query": {
1344
- "type": "string",
1345
- "description": "The natural language query to search for. Be specific to get the best results."
1471
+ # --- 1. Discover Available Tools (MCP and RAG) ---
1472
+ available_tools = []
1473
+
1474
+ # Discover MCP tools if requested
1475
+ if use_mcps and self.mcp:
1476
+ discovered_mcp_tools = self.mcp.discover_tools(force_refresh=True)
1477
+ if isinstance(use_mcps, list):
1478
+ # Filter for specific MCP tools
1479
+ available_tools.extend([t for t in discovered_mcp_tools if t['name'] in use_mcps])
1480
+ else: # use_mcps is True
1481
+ available_tools.extend(discovered_mcp_tools)
1482
+
1483
+ # Define and add RAG tools if requested
1484
+ if use_data_store:
1485
+ for store_name, _ in use_data_store.items():
1486
+ rag_tool_definition = {
1487
+ "name": f"research::{store_name}",
1488
+ "description": (
1489
+ f"Queries the '{store_name}' information database to find relevant text chunks based on a natural language query. "
1490
+ "Use this to gather information, answer questions, or find context for a task before using other tools."
1491
+ ),
1492
+ "input_schema": {
1493
+ "type": "object",
1494
+ "properties": {
1495
+ "query": {
1496
+ "type": "string",
1497
+ "description": "The natural language query to search for. Be specific to get the best results."
1498
+ }
1499
+ },
1500
+ "required": ["query"]
1346
1501
  }
1347
- },
1348
- "required": ["query"]
1349
- }
1350
- }
1351
- available_tools = [rag_tool_definition] + mcp_tools
1502
+ }
1503
+ available_tools.append(rag_tool_definition)
1504
+
1505
+ if not available_tools:
1506
+ # If no tools are available, just do a simple text generation
1507
+ final_answer_text = self.generate_text(prompt=prompt, system_prompt=system_prompt, stream=streaming_callback is not None, streaming_callback=streaming_callback)
1508
+ return {"final_answer": self.remove_thinking_blocks(final_answer_text), "tool_calls": [], "error": None}
1509
+
1352
1510
 
1353
- # --- 2. Optional Initial Objectives Extraction ---
1354
1511
  formatted_tools_list = "\n".join([
1355
1512
  f"- Full Tool Name: {t.get('name')}\n Description: {t.get('description')}\n Input Schema: {json.dumps(t.get('input_schema'))}"
1356
1513
  for t in available_tools
1357
- ])
1514
+ ])
1515
+
1516
+ # --- 2. Optional Initial Objectives Extraction ---
1358
1517
  if build_plan:
1359
1518
  if streaming_callback:
1360
1519
  streaming_callback("Extracting initial objectives...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "objectives_extraction"}, turn_history)
1361
1520
 
1362
- # The enhanced prompt is placed inside the original parenthesis format.
1363
- # The f-strings for tool lists and user prompts are preserved.
1364
-
1365
1521
  obj_prompt = (
1366
1522
  "You are a hyper-efficient and logical project planner. Your sole purpose is to analyze the user's request and create a concise, numbered list of actionable steps to fulfill it.\n\n"
1367
1523
  "Your plan must be the most direct and minimal path to the user's goal.\n\n"
@@ -1390,8 +1546,6 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1390
1546
 
1391
1547
  turn_history.append({"type": "initial_objectives", "content": current_objectives})
1392
1548
 
1393
-
1394
-
1395
1549
  # --- 3. Main Agent Loop ---
1396
1550
  while llm_iterations < max_llm_iterations:
1397
1551
  llm_iterations += 1
@@ -1403,15 +1557,19 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1403
1557
  if agent_work_history:
1404
1558
  history_parts = []
1405
1559
  for i, entry in enumerate(agent_work_history):
1560
+ # Sanitize the result for history display, similar to knowledge synthesis
1561
+ sanitized_hist_result = entry['tool_result'].copy()
1562
+ if 'image_base64' in sanitized_hist_result: sanitized_hist_result.pop('image_base64')
1563
+ if 'content' in sanitized_hist_result and len(sanitized_hist_result['content']) > 200: sanitized_hist_result['content'] = sanitized_hist_result['content'][:200] + '... (truncated)'
1564
+
1406
1565
  history_parts.append(
1407
1566
  f"### Step {i+1}:\n"
1408
1567
  f"**Thought:** {entry['thought']}\n"
1409
1568
  f"**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n"
1410
- f"**Observation:**\n```json\n{json.dumps(entry['tool_result'], indent=2)}\n```"
1569
+ f"**Observation:**\n```json\n{json.dumps(sanitized_hist_result, indent=2)}\n```"
1411
1570
  )
1412
1571
  formatted_agent_history = "\n\n".join(history_parts)
1413
1572
 
1414
- # Construct the "Thinking & Planning" prompt
1415
1573
  decision_prompt_template = f"""You are a strategic AI assistant. Your goal is to achieve a set of objectives by intelligently using research and system tools.
1416
1574
 
1417
1575
  --- AVAILABLE TOOLS ---
@@ -1431,50 +1589,47 @@ Knowledge Scratchpad (our current understanding):
1431
1589
  --- INSTRUCTIONS ---
1432
1590
  1. **Analyze:** Review the entire work history, objectives, and scratchpad.
1433
1591
  2. **Update State:** Based on the latest observations, update the scratchpad and refine the objectives. The scratchpad should be a comprehensive summary of ALL knowledge gathered.
1434
- 3. **Decide Next Action:** Choose ONE of the following: `call_tool`, `final_answer`, or `clarify`. Always prefer to gather information with `research::query_database` before attempting to use other tools if you lack context.
1435
-
1436
- --- OUTPUT FORMAT ---
1437
- Respond with a single JSON object inside a JSON markdown tag. Use this exact schema:
1438
- ```json
1439
- {{
1440
- "thought": "Your reasoning for the chosen action, analyzing how the work history informs your next step. Explain why you are choosing a specific tool (or to answer).",
1441
- "updated_scratchpad": "The new, complete, and comprehensive summary of all knowledge gathered. Integrate new findings with old ones. if no new knowledge is gathered, this should be an empty string.",
1442
- "updated_objectives": "The full, potentially revised, list of objectives. If no change, repeat the current list.",
1443
- "action": "The chosen action: 'call_tool', 'final_answer', or 'clarify'.",
1444
- "tool_name": "(string, if action is 'call_tool') The full 'alias::tool_name' of the tool to use.",
1445
- "tool_params": {{"query": "...", "param2": "..."}},
1446
- "clarification_request": "(string, if action is 'clarify') Your question to the user."
1447
- }}
1448
- ```
1592
+ 3. **Decide Next Action:** Choose ONE of the following: `call_tool`, `final_answer`, or `clarify`. Always prefer to gather information with a `research::` tool before attempting to use other tools if you lack context.
1449
1593
  """
1450
- raw_llm_decision_json = self.generate_text(
1451
- prompt=decision_prompt_template, n_predict=2048, temperature=tool_call_decision_temperature
1594
+ decision_template = {
1595
+ "thought": "Your reasoning for the chosen action, analyzing how the work history informs your next step. Explain why you are choosing a specific tool (or to answer).",
1596
+ "updated_scratchpad": "The new, complete, and comprehensive summary of all knowledge gathered. Integrate new findings with old ones. If no new knowledge is gathered, this should be an empty string.",
1597
+ "updated_objectives": "The full, potentially revised, list of objectives. If no change, repeat the current list.",
1598
+ "action": "The chosen action: 'call_tool', 'final_answer', or 'clarify'.",
1599
+ "action_details": {
1600
+ "tool_name": "(string, if action is 'call_tool') The full 'alias::tool_name' of the tool to use.",
1601
+ "tool_params": {"query": "...", "param2": "..."},
1602
+ "clarification_request": "(string, if action is 'clarify') Your question to the user."
1603
+ }
1604
+ }
1605
+ llm_decision = self.generate_structured_content(
1606
+ prompt=decision_prompt_template,
1607
+ template=decision_template,
1608
+ temperature=tool_call_decision_temperature
1452
1609
  )
1610
+
1611
+ if not llm_decision:
1612
+ ASCIIColors.error("LLM failed to generate a valid decision JSON. Aborting loop.")
1613
+ break
1453
1614
 
1454
1615
  # --- 4. Parse LLM's plan and update state ---
1455
- try:
1456
- llm_decision = robust_json_parser(raw_llm_decision_json)
1457
- turn_history.append({"type": "llm_plan", "content": llm_decision})
1458
-
1459
- current_objectives = llm_decision.get("updated_objectives", current_objectives)
1460
- new_scratchpad = llm_decision.get("updated_scratchpad")
1461
-
1462
- if new_scratchpad and new_scratchpad != knowledge_scratchpad:
1463
- knowledge_scratchpad = new_scratchpad
1464
- if streaming_callback:
1465
- streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP, {"id": "scratchpad_update"}, turn_history)
1466
- streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id":"scratch_pad_update"}, turn_history)
1467
-
1468
- except (json.JSONDecodeError, AttributeError, KeyError) as e:
1469
- ASCIIColors.error(f"Failed to parse LLM decision JSON: {raw_llm_decision_json}. Error: {e}")
1470
- turn_history.append({"type": "error", "content": f"Failed to parse LLM plan: {raw_llm_decision_json}"})
1471
- break
1616
+ turn_history.append({"type": "llm_plan", "content": llm_decision})
1617
+
1618
+ current_objectives = llm_decision.get("updated_objectives", current_objectives)
1619
+ new_scratchpad = llm_decision.get("updated_scratchpad")
1620
+
1621
+ if new_scratchpad and new_scratchpad.strip() and new_scratchpad != knowledge_scratchpad:
1622
+ knowledge_scratchpad = new_scratchpad
1623
+ if streaming_callback:
1624
+ streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP, {"id": "scratchpad_update"}, turn_history)
1625
+ streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id":"scratch_pad_update"}, turn_history)
1472
1626
 
1473
1627
  if streaming_callback:
1474
1628
  streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history)
1475
1629
 
1476
1630
  # --- 5. Execute the chosen action ---
1477
1631
  action = llm_decision.get("action")
1632
+ action_details = llm_decision.get("action_details", {})
1478
1633
  tool_result = None
1479
1634
 
1480
1635
  if action == "call_tool":
@@ -1482,40 +1637,53 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
1482
1637
  ASCIIColors.warning("Max tool calls reached. Forcing final answer.")
1483
1638
  break
1484
1639
 
1485
- tool_name = llm_decision.get("tool_name")
1486
- tool_params = llm_decision.get("tool_params", {})
1640
+ tool_name = action_details.get("tool_name")
1641
+ tool_params = action_details.get("tool_params", {})
1487
1642
 
1488
1643
  if not tool_name or not isinstance(tool_params, dict):
1489
1644
  ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
1490
1645
  break
1491
1646
 
1492
1647
  if streaming_callback:
1493
- streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
1648
+ streaming_callback(f"Executing tool: {tool_name}...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": f"tool_exec_{llm_iterations}", "tool_name": tool_name}, turn_history)
1494
1649
 
1495
1650
  try:
1496
1651
  # ** DYNAMIC TOOL/RAG DISPATCH **
1497
- if tool_name == "research::query_database":
1498
- query = tool_params.get("query")
1499
- if not query:
1500
- tool_result = {"error": "RAG tool called without a 'query' parameter."}
1652
+ if tool_name.startswith("research::") and use_data_store:
1653
+ store_name = tool_name.split("::")[1]
1654
+ rag_query_function_local = use_data_store.get(store_name)
1655
+ if not rag_query_function_local:
1656
+ tool_result = {"error": f"RAG data store '{store_name}' not found or provided."}
1501
1657
  else:
1502
- retrieved_chunks = rag_query_function(query, rag_vectorizer_name, rag_top_k, rag_min_similarity_percent)
1503
- if not retrieved_chunks:
1504
- tool_result = {"summary": "No relevant documents found for the query.", "chunks": []}
1658
+ query = tool_params.get("query")
1659
+ if not query:
1660
+ tool_result = {"error": "RAG tool called without a 'query' parameter."}
1505
1661
  else:
1506
- tool_result = {
1507
- "summary": f"Found {len(retrieved_chunks)} relevant document chunks.",
1508
- "chunks": retrieved_chunks
1509
- }
1510
- else:
1662
+ retrieved_chunks = rag_query_function_local.get("callable", lambda: {'Search error'})(query, rag_top_k, rag_min_similarity_percent)
1663
+ if not retrieved_chunks:
1664
+ tool_result = {"summary": "No relevant documents found for the query.", "chunks": []}
1665
+ else:
1666
+ tool_result = {
1667
+ "summary": f"Found {len(retrieved_chunks)} relevant document chunks.",
1668
+ "chunks": retrieved_chunks
1669
+ }
1670
+ elif use_mcps and self.mcp:
1511
1671
  # Standard MCP tool execution
1512
1672
  tool_result = self.mcp.execute_tool(tool_name, tool_params, lollms_client_instance=self)
1673
+ else:
1674
+ tool_result = {"error": f"Tool '{tool_name}' cannot be executed. RAG store not found or MCP binding not configured."}
1513
1675
 
1514
1676
  except Exception as e_exec:
1515
1677
  trace_exception(e_exec)
1516
1678
  tool_result = {"error": f"An exception occurred while executing tool '{tool_name}': {e_exec}"}
1517
1679
 
1518
- # Record the work cycle in the agent's history
1680
+ if streaming_callback:
1681
+ streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}", "result": tool_result}, turn_history)
1682
+
1683
+ knowledge_scratchpad = self._synthesize_knowledge(knowledge_scratchpad, tool_name, tool_params, tool_result)
1684
+ if streaming_callback:
1685
+ streaming_callback(f"Knowledge scratchpad updated after {tool_name} call.", MSG_TYPE.MSG_TYPE_INFO, {"id": "scratchpad_update"}, turn_history)
1686
+
1519
1687
  work_entry = {
1520
1688
  "thought": llm_decision.get("thought", "N/A"),
1521
1689
  "tool_name": tool_name,
@@ -1524,13 +1692,9 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
1524
1692
  }
1525
1693
  agent_work_history.append(work_entry)
1526
1694
  tool_calls_made_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1527
-
1528
- if streaming_callback:
1529
- streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
1530
- streaming_callback(json.dumps(tool_result, indent=2), MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, tool_result, turn_history)
1531
1695
 
1532
1696
  elif action == "clarify":
1533
- clarification_request = llm_decision.get("clarification_request", "I need more information. Could you please clarify?")
1697
+ clarification_request = action_details.get("clarification_request", "I need more information. Could you please clarify?")
1534
1698
  return {"final_answer": clarification_request, "tool_calls": tool_calls_made_this_turn, "error": None, "clarification": True}
1535
1699
 
1536
1700
  elif action == "final_answer":
@@ -1544,7 +1708,8 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
1544
1708
  streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1545
1709
 
1546
1710
  if streaming_callback:
1547
- streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
1711
+ streaming_callback(f"LLM reasoning loop finished.", MSG_TYPE.MSG_TYPE_STEP, {"id": "reasoning_loop_end"}, turn_history)
1712
+
1548
1713
  # --- 6. Generate Final Answer ---
1549
1714
  if streaming_callback:
1550
1715
  streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history)
@@ -1579,7 +1744,8 @@ Original User Request: "{original_user_prompt}"
1579
1744
  turn_history.append({"type":"final_answer_generated", "content": final_answer})
1580
1745
 
1581
1746
  return {"final_answer": final_answer, "tool_calls": tool_calls_made_this_turn, "error": None}
1582
-
1747
+
1748
+
1583
1749
  def generate_code(
1584
1750
  self,
1585
1751
  prompt,
@@ -26,10 +26,10 @@ try:
26
26
  except ImportError:
27
27
  ENCRYPTION_AVAILABLE = False
28
28
 
29
+ from lollms_client.lollms_types import MSG_TYPE
29
30
  # Type hint placeholders for classes defined externally
30
31
  if False:
31
32
  from lollms_client import LollmsClient
32
- from lollms_client.lollms_types import MSG_TYPE
33
33
  from lollms_personality import LollmsPersonality
34
34
 
35
35
  class EncryptedString(TypeDecorator):
@@ -341,102 +341,162 @@ class LollmsDiscussion:
341
341
  current_id = msg_orm.parent_id
342
342
  return [LollmsMessage(self, orm) for orm in reversed(branch_orms)]
343
343
 
344
- def chat(self, user_message: str, personality: Optional['LollmsPersonality'] = None, **kwargs) -> LollmsMessage:
344
+
345
+
346
+ def chat(
347
+ self,
348
+ user_message: str,
349
+ personality: Optional['LollmsPersonality'] = None,
350
+ use_mcps: Union[None, bool, List[str]] = None,
351
+ use_data_store: Union[None, Dict[str, Callable]] = None,
352
+ build_plan: bool = True,
353
+ add_user_message: bool = True, # New parameter
354
+ max_tool_calls = 10,
355
+ rag_top_k = 5,
356
+ **kwargs
357
+ ) -> Dict[str, 'LollmsMessage']: # Return type changed
358
+ """
359
+ Main interaction method for the discussion. It can perform a simple chat or
360
+ trigger a complex agentic loop with RAG and MCP tool use.
361
+
362
+ Args:
363
+ user_message (str): The new message from the user.
364
+ personality (Optional[LollmsPersonality], optional): The personality to use. Defaults to None.
365
+ use_mcps (Union[None, bool, List[str]], optional): Controls MCP tool usage. Defaults to None.
366
+ use_data_store (Union[None, Dict[str, Callable]], optional): Controls RAG usage. Defaults to None.
367
+ build_plan (bool, optional): If True, the agent will generate an initial plan. Defaults to True.
368
+ add_user_message (bool, optional): If True, a new user message is created from the prompt.
369
+ If False, it assumes regeneration on the current active user message. Defaults to True.
370
+ **kwargs: Additional keyword arguments passed to the underlying generation method.
371
+
372
+ Returns:
373
+ Dict[str, LollmsMessage]: A dictionary with 'user_message' and 'ai_message' objects.
374
+ """
345
375
  if self.max_context_size is not None:
346
376
  self.summarize_and_prune(self.max_context_size)
347
377
 
348
- if user_message:
349
- self.add_message(sender="user", sender_type="user", content=user_message)
350
-
378
+ # Add user message to the discussion or get the existing one
379
+ if add_user_message:
380
+ # Pass kwargs to capture images, etc., sent from the router
381
+ user_msg = self.add_message(sender="user", sender_type="user", content=user_message, **kwargs)
382
+ else:
383
+ # We are regenerating. The current active branch tip must be the user message.
384
+ if self.active_branch_id not in self._message_index:
385
+ raise ValueError("Regeneration failed: active branch tip not found or is invalid.")
386
+ user_msg_orm = self._message_index[self.active_branch_id]
387
+ if user_msg_orm.sender_type != 'user':
388
+ raise ValueError(f"Regeneration failed: active branch tip is a '{user_msg_orm.sender_type}' message, not 'user'.")
389
+ user_msg = LollmsMessage(self, user_msg_orm)
390
+
391
+ # --- (The existing generation logic remains the same) ---
392
+ is_agentic_turn = (use_mcps is not None and len(use_mcps)>0) or (use_data_store is not None and len(use_data_store)>0)
351
393
  rag_context = None
352
394
  original_system_prompt = self.system_prompt
353
395
  if personality:
354
396
  self.system_prompt = personality.system_prompt
355
- if user_message:
397
+ if user_message and not is_agentic_turn:
356
398
  rag_context = personality.get_rag_context(user_message)
357
-
358
399
  if rag_context:
359
400
  self.system_prompt = f"{original_system_prompt or ''}\n\n--- Relevant Information ---\n{rag_context}\n---"
360
-
361
- from lollms_client.lollms_types import MSG_TYPE
362
- is_streaming = "streaming_callback" in kwargs and kwargs.get("streaming_callback") is not None
363
-
364
- final_raw_response = ""
365
401
  start_time = datetime.now()
366
-
367
- if personality and personality.script_module and hasattr(personality.script_module, 'run'):
368
- try:
369
- print(f"[{personality.name}] Running custom script...")
370
- final_raw_response = personality.script_module.run(self, kwargs.get("streaming_callback"))
371
- except Exception as e:
372
- print(f"[{personality.name}] Error in custom script: {e}")
373
- final_raw_response = f"Error executing personality script: {e}"
402
+ if is_agentic_turn:
403
+ # --- FIX: Provide the full conversation context to the agent ---
404
+ # 1. Get the model's max context size.
405
+ max_ctx = self.lollmsClient.binding.get_ctx_size(self.lollmsClient.binding.model_name) if self.lollmsClient.binding else None
406
+
407
+ # 2. Format the entire discussion up to this point, including the new user message.
408
+ # This ensures the agent has the full history.
409
+ full_context_prompt = self.format_discussion(max_allowed_tokens=max_ctx)
410
+
411
+ # 3. Call the agent with the complete context.
412
+ # We pass the full context to the 'prompt' argument. The `system_prompt` is already
413
+ # included within the formatted text, so we don't pass it separately to avoid duplication.
414
+ agent_result = self.lollmsClient.generate_with_mcp_rag(
415
+ prompt=full_context_prompt,
416
+ use_mcps=use_mcps,
417
+ use_data_store=use_data_store,
418
+ build_plan=build_plan,
419
+ max_tool_calls = max_tool_calls,
420
+ rag_top_k= rag_top_k,
421
+ **kwargs
422
+ )
423
+ final_content = agent_result.get("final_answer", "")
424
+ thoughts_text = None
425
+ final_raw_response = json.dumps(agent_result)
374
426
  else:
375
- raw_response_accumulator = []
376
- if is_streaming:
377
- full_response_parts, token_buffer, in_thought_block = [], "", False
378
- original_callback = kwargs.get("streaming_callback")
379
- def accumulating_callback(token: str, msg_type: MSG_TYPE = MSG_TYPE.MSG_TYPE_CHUNK):
380
- nonlocal token_buffer, in_thought_block
381
- raw_response_accumulator.append(token)
382
- continue_streaming = True
383
- if token: token_buffer += token
384
- while True:
385
- if in_thought_block:
386
- end_tag_pos = token_buffer.find("</think>")
387
- if end_tag_pos != -1:
388
- thought_chunk = token_buffer[:end_tag_pos]
389
- if self.show_thoughts and original_callback and thought_chunk:
390
- if not original_callback(thought_chunk, MSG_TYPE.MSG_TYPE_THOUGHT_CHUNK): continue_streaming = False
391
- in_thought_block, token_buffer = False, token_buffer[end_tag_pos + len("</think>"):]
392
- else:
393
- if self.show_thoughts and original_callback and token_buffer:
394
- if not original_callback(token_buffer, MSG_TYPE.MSG_TYPE_THOUGHT_CHUNK): continue_streaming = False
395
- token_buffer = ""; break
396
- else:
397
- start_tag_pos = token_buffer.find("<think>")
398
- if start_tag_pos != -1:
399
- response_chunk = token_buffer[:start_tag_pos]
400
- if response_chunk:
401
- full_response_parts.append(response_chunk)
402
- if original_callback:
403
- if not original_callback(response_chunk, MSG_TYPE.MSG_TYPE_CHUNK): continue_streaming = False
404
- in_thought_block, token_buffer = True, token_buffer[start_tag_pos + len("<think>"):]
405
- else:
406
- if token_buffer:
407
- full_response_parts.append(token_buffer)
408
- if original_callback:
409
- if not original_callback(token_buffer, MSG_TYPE.MSG_TYPE_CHUNK): continue_streaming = False
410
- token_buffer = ""; break
411
- return continue_streaming
412
- kwargs["streaming_callback"], kwargs["stream"] = accumulating_callback, True
413
- self.lollmsClient.chat(self, **kwargs)
414
- final_raw_response = "".join(raw_response_accumulator)
427
+ if personality and personality.script_module and hasattr(personality.script_module, 'run'):
428
+ try:
429
+ final_raw_response = personality.script_module.run(self, kwargs.get("streaming_callback"))
430
+ except Exception as e:
431
+ final_raw_response = f"Error executing personality script: {e}"
415
432
  else:
416
- kwargs["stream"] = False
417
- final_raw_response = self.lollmsClient.chat(self, **kwargs) or ""
418
-
419
- end_time = datetime.now()
420
- if rag_context:
433
+ is_streaming = "streaming_callback" in kwargs and kwargs.get("streaming_callback") is not None
434
+ if is_streaming:
435
+ raw_response_accumulator = self.lollmsClient.chat(self, **kwargs)
436
+ final_raw_response = "".join(raw_response_accumulator)
437
+ else:
438
+ kwargs["stream"] = False
439
+ final_raw_response = self.lollmsClient.chat(self, **kwargs) or ""
440
+ thoughts_match = re.search(r"<think>(.*?)</think>", final_raw_response, re.DOTALL)
441
+ thoughts_text = thoughts_match.group(1).strip() if thoughts_match else None
442
+ final_content = self.lollmsClient.remove_thinking_blocks(final_raw_response)
443
+ if rag_context or (personality and self.system_prompt != original_system_prompt):
421
444
  self.system_prompt = original_system_prompt
422
-
445
+ end_time = datetime.now()
423
446
  duration = (end_time - start_time).total_seconds()
424
- thoughts_match = re.search(r"<think>(.*?)</think>", final_raw_response, re.DOTALL)
425
- thoughts_text = thoughts_match.group(1).strip() if thoughts_match else None
426
- final_content = self.lollmsClient.remove_thinking_blocks(final_raw_response)
427
447
  token_count = self.lollmsClient.count_tokens(final_content)
428
448
  tok_per_sec = (token_count / duration) if duration > 0 else 0
429
-
449
+ # --- (End of existing logic) ---
450
+
451
+ # --- FIX: Store agentic results in metadata ---
452
+ message_meta = {}
453
+ if is_agentic_turn and isinstance(agent_result, dict):
454
+ # We store the 'steps' and 'sources' if they exist in the agent result.
455
+ # This makes them available to the frontend in the final message object.
456
+ if "steps" in agent_result:
457
+ message_meta["steps"] = agent_result["steps"]
458
+ if "sources" in agent_result:
459
+ message_meta["sources"] = agent_result["sources"]
460
+
430
461
  ai_message_obj = self.add_message(
431
- sender="assistant", sender_type="assistant", content=final_content,
462
+ sender=personality.name if personality else "assistant", sender_type="assistant", content=final_content,
432
463
  raw_content=final_raw_response, thoughts=thoughts_text, tokens=token_count,
433
464
  binding_name=self.lollmsClient.binding.binding_name, model_name=self.lollmsClient.binding.model_name,
434
- generation_speed=tok_per_sec
465
+ generation_speed=tok_per_sec,
466
+ parent_id=user_msg.id, # Ensure the AI response is a child of the user message
467
+ metadata=message_meta # Pass the collected metadata here
435
468
  )
436
-
437
- if self._is_db_backed and not self.autosave:
469
+ if self._is_db_backed and self.autosave:
438
470
  self.commit()
439
- return ai_message_obj
471
+
472
+ return {"user_message": user_msg, "ai_message": ai_message_obj}
473
+
474
+ def regenerate_branch(self, **kwargs) -> Dict[str, 'LollmsMessage']:
475
+ if not self.active_branch_id or self.active_branch_id not in self._message_index:
476
+ raise ValueError("No active message to regenerate from.")
477
+
478
+ last_message_orm = self._message_index[self.active_branch_id]
479
+
480
+ # If the current active message is the assistant's, we need to delete it
481
+ # and set the active branch to its parent (the user message).
482
+ if last_message_orm.sender_type == 'assistant':
483
+ parent_id = last_message_orm.parent_id
484
+ if not parent_id:
485
+ raise ValueError("Cannot regenerate from an assistant message with no parent.")
486
+
487
+ last_message_id = last_message_orm.id
488
+ self._db_discussion.messages.remove(last_message_orm)
489
+ del self._message_index[last_message_id]
490
+ if self._is_db_backed:
491
+ self._messages_to_delete_from_db.add(last_message_id)
492
+
493
+ self.active_branch_id = parent_id
494
+ self.touch()
495
+
496
+ # The active branch is now guaranteed to be on a user message.
497
+ # Call chat, but do not add a new user message.
498
+ prompt_to_regenerate = self._message_index[self.active_branch_id].content
499
+ return self.chat(user_message=prompt_to_regenerate, add_user_message=False, **kwargs)
440
500
 
441
501
  def process_and_summarize(self, large_text: str, user_prompt: str, chunk_size: int = 4096, **kwargs) -> LollmsMessage:
442
502
  user_msg = self.add_message(sender="user", sender_type="user", content=user_prompt)
@@ -459,20 +519,6 @@ class LollmsDiscussion:
459
519
  self.commit()
460
520
  return ai_message_obj
461
521
 
462
- def regenerate_branch(self, **kwargs) -> LollmsMessage:
463
- if not self.active_branch_id or self.active_branch_id not in self._message_index:
464
- raise ValueError("No active message to regenerate from.")
465
- last_message_orm = self._message_index[self.active_branch_id]
466
- if last_message_orm.sender_type != 'assistant':
467
- raise ValueError("Can only regenerate from an assistant's message.")
468
- parent_id, last_message_id = last_message_orm.parent_id, last_message_orm.id
469
- self._db_discussion.messages.remove(last_message_orm)
470
- del self._message_index[last_message_id]
471
- if self._is_db_backed:
472
- self._messages_to_delete_from_db.add(last_message_id)
473
- self.active_branch_id = parent_id
474
- self.touch()
475
- return self.chat("", **kwargs)
476
522
 
477
523
  def delete_branch(self, message_id: str):
478
524
  if not self._is_db_backed:
@@ -115,6 +115,9 @@ class LollmsLLMBinding(ABC):
115
115
  """
116
116
  pass
117
117
 
118
+ def get_ctx_size(self, model_name):
119
+ return 32000
120
+
118
121
 
119
122
  @abstractmethod
120
123
  def tokenize(self, text: str) -> list:
@@ -63,4 +63,4 @@ class ELF_COMPLETION_FORMAT(Enum):
63
63
  raise ValueError(f"Invalid format string: {format_string}. Must be one of {list(format_mapping.keys())}.")
64
64
 
65
65
  def __str__(self):
66
- return self.name
66
+ return self.name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lollms_client
3
- Version: 0.22.0
3
+ Version: 0.23.0
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,16 +1,11 @@
1
1
  examples/console_discussion.py,sha256=JxjVaAxtt1WEXLN8vCJosu-cYNgfIX2JGO25kg1FFNY,20490
2
- examples/external_mcp.py,sha256=swx1KCOz6jk8jGTAycq-xu7GXPAhRMDe1x--SKocugE,13371
3
2
  examples/function_calling_with_local_custom_mcp.py,sha256=g6wOFRB8-p9Cv7hKmQaGzPvtMX3H77gas01QVNEOduM,12407
4
3
  examples/generate_a_benchmark_for_safe_store.py,sha256=bkSt0mrpNsN0krZAUShm0jgVM1ukrPpjI7VwSgcNdSA,3974
5
4
  examples/generate_text_with_multihop_rag_example.py,sha256=riEyVYo97r6ZYdySL-NJkRhE4MnpwbZku1sN8RNvbvs,11519
6
5
  examples/gradio_chat_app.py,sha256=ZZ_D1U0wvvwE9THmAPXUvNKkFG2gi7tQq1f2pQx_2ug,15315
7
6
  examples/gradio_lollms_chat.py,sha256=z5FDE62dmPU3nb16zbZX6jkVitML1PMfPxYyWr8VLz8,10135
8
7
  examples/internet_search_with_rag.py,sha256=ioTb_WI2M6kFeh1Dg-EGcKjccphnCsIGD_e9PZgZshw,12314
9
- examples/local_mcp.py,sha256=w40dgayvHYe01yvekEE0LjcbkpwKjWwJ-9v4_wGYsUk,9113
10
8
  examples/lollms_discussions_test.py,sha256=Jk1cCUDBBhTcK5glI50jAgzfB3IOiiUlnK3q7RYfMkA,6796
11
- examples/openai_mcp.py,sha256=7IEnPGPXZgYZyiES_VaUbQ6viQjenpcUxGiHE-pGeFY,11060
12
- examples/run_remote_mcp_example copy.py,sha256=pGT8A5iXK9oHtjGNEUCm8fnj9DQ37gcznjLYqAEI20o,10075
13
- examples/run_standard_mcp_example.py,sha256=GSZpaACPf3mDPsjA8esBQVUsIi7owI39ca5avsmvCxA,9419
14
9
  examples/simple_text_gen_test.py,sha256=RoX9ZKJjGMujeep60wh5WT_GoBn0O9YKJY6WOy-ZmOc,8710
15
10
  examples/simple_text_gen_with_image_test.py,sha256=rR1O5Prcb52UHtJ3c6bv7VuTd1cvbkr5aNZU-v-Rs3Y,9263
16
11
  examples/text_2_audio.py,sha256=MfL4AH_NNwl6m0I0ywl4BXRZJ0b9Y_9fRqDIe6O-Sbw,3523
@@ -24,13 +19,18 @@ examples/deep_analyze/deep_analyse.py,sha256=fZNmDrfEAuxEAfdbjAgJYIh1k6wbiuZ4Rvw
24
19
  examples/deep_analyze/deep_analyze_multiple_files.py,sha256=fOryShA33P4IFxcxUDe-nJ2kW0v9w9yW8KsToS3ETl8,1032
25
20
  examples/generate_and_speak/generate_and_speak.py,sha256=RAlvRwtEKXCh894l9M3iQbADe8CvF5N442jtRurK02I,13908
26
21
  examples/generate_game_sfx/generate_game_fx.py,sha256=MgLNGi4hGBRoyr4bqYuCUdCSqd-ldDVfF0VSDUjgzsg,10467
22
+ examples/mcp_examples/external_mcp.py,sha256=swx1KCOz6jk8jGTAycq-xu7GXPAhRMDe1x--SKocugE,13371
23
+ examples/mcp_examples/local_mcp.py,sha256=w40dgayvHYe01yvekEE0LjcbkpwKjWwJ-9v4_wGYsUk,9113
24
+ examples/mcp_examples/openai_mcp.py,sha256=7IEnPGPXZgYZyiES_VaUbQ6viQjenpcUxGiHE-pGeFY,11060
25
+ examples/mcp_examples/run_remote_mcp_example_v2.py,sha256=bbNn93NO_lKcFzfIsdvJJijGx2ePFTYfknofqZxMuRM,14626
26
+ examples/mcp_examples/run_standard_mcp_example.py,sha256=GSZpaACPf3mDPsjA8esBQVUsIi7owI39ca5avsmvCxA,9419
27
27
  examples/test_local_models/local_chat.py,sha256=slakja2zaHOEAUsn2tn_VmI4kLx6luLBrPqAeaNsix8,456
28
- lollms_client/__init__.py,sha256=Cd4G7paIm0kKNqc90yAJ7VJ8mpi2jfzoMBabmWRBZbY,1047
28
+ lollms_client/__init__.py,sha256=NPKqkS85rJkCPyOZt06EJpLHhQEiopFnON4xF0kXwJ8,1047
29
29
  lollms_client/lollms_config.py,sha256=goEseDwDxYJf3WkYJ4IrLXwg3Tfw73CXV2Avg45M_hE,21876
30
- lollms_client/lollms_core.py,sha256=y_KIVFCBsA0BYizGAbvQZrtWZCX5jB00yMuAgHlHaD4,143685
31
- lollms_client/lollms_discussion.py,sha256=zdAUOhbFod65-VZYfKaldHYURR7wWnuccqv6FJa1qrM,36291
30
+ lollms_client/lollms_core.py,sha256=xG5Fdm21a4rTq4m1UAh9G0FYFssjfdSI5s2mQLkKeXQ,153747
31
+ lollms_client/lollms_discussion.py,sha256=Z5Hs_faRdxldf4CbbjNSCdJ8FYnGEgL8wV43ZtanCUI,38685
32
32
  lollms_client/lollms_js_analyzer.py,sha256=01zUvuO2F_lnUe_0NLxe1MF5aHE1hO8RZi48mNPv-aw,8361
33
- lollms_client/lollms_llm_binding.py,sha256=E81g4yBlQn76WTSLicnTETJuQhf_WZUMZaxotgRnOcA,12096
33
+ lollms_client/lollms_llm_binding.py,sha256=Kpzhs5Jx8eAlaaUacYnKV7qIq2wbME5lOEtKSfJKbpg,12161
34
34
  lollms_client/lollms_mcp_binding.py,sha256=0rK9HQCBEGryNc8ApBmtOlhKE1Yfn7X7xIQssXxS2Zc,8933
35
35
  lollms_client/lollms_personality.py,sha256=dILUI5DZdzJ3NDDQiIsK2UptVF-jZK3XYXZ2bpXP_ew,8035
36
36
  lollms_client/lollms_python_analyzer.py,sha256=7gf1fdYgXCOkPUkBAPNmr6S-66hMH4_KonOMsADASxc,10246
@@ -39,7 +39,7 @@ lollms_client/lollms_tti_binding.py,sha256=afO0-d-Kqsmh8UHTijTvy6dZAt-XDB6R-IHmd
39
39
  lollms_client/lollms_ttm_binding.py,sha256=FjVVSNXOZXK1qvcKEfxdiX6l2b4XdGOSNnZ0utAsbDg,4167
40
40
  lollms_client/lollms_tts_binding.py,sha256=5cJYECj8PYLJAyB6SEH7_fhHYK3Om-Y3arkygCnZ24o,4342
41
41
  lollms_client/lollms_ttv_binding.py,sha256=KkTaHLBhEEdt4sSVBlbwr5i_g_TlhcrwrT-7DjOsjWQ,4131
42
- lollms_client/lollms_types.py,sha256=NfvTmICzRCgfjjy5zLMFeDaiW6zyUsdnxRF69gAEyAk,3110
42
+ lollms_client/lollms_types.py,sha256=c2vkdmyCU5aCyOCfWmfJE-q74T8w1vHMzFoMy8453jY,3108
43
43
  lollms_client/lollms_utilities.py,sha256=qK5iNmrFD7NGaEVW3nCWT6AtEhLIVHCXMzEpYxG_M5w,11293
44
44
  lollms_client/llm_bindings/__init__.py,sha256=9sWGpmWSSj6KQ8H4lKGCjpLYwhnVdL_2N7gXCphPqh4,14
45
45
  lollms_client/llm_bindings/llamacpp/__init__.py,sha256=Qj5RvsgPeHGNfb5AEwZSzFwAp4BOWjyxmm9qBNtstrc,63716
@@ -78,9 +78,8 @@ lollms_client/tts_bindings/piper_tts/__init__.py,sha256=0IEWG4zH3_sOkSb9WbZzkeV5
78
78
  lollms_client/tts_bindings/xtts/__init__.py,sha256=FgcdUH06X6ZR806WQe5ixaYx0QoxtAcOgYo87a2qxYc,18266
79
79
  lollms_client/ttv_bindings/__init__.py,sha256=UZ8o2izQOJLQgtZ1D1cXoNST7rzqW22rL2Vufc7ddRc,3141
80
80
  lollms_client/ttv_bindings/lollms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
- lollms_client-0.22.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
82
- personalities/parrot.py,sha256=-HdbK1h7Ixvp8FX69Nv92Z_El6_UVtsF8WuAvNpKbfg,478
83
- lollms_client-0.22.0.dist-info/METADATA,sha256=71v2Qlp7tSyr_fL9I8Nw-9LLlncfuUkYgtJkK1EKayc,13374
84
- lollms_client-0.22.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
- lollms_client-0.22.0.dist-info/top_level.txt,sha256=vgtOcmtJbKs9gEiIIPp2scIsvtvU2y5u3tngqlme1RU,37
86
- lollms_client-0.22.0.dist-info/RECORD,,
81
+ lollms_client-0.23.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
82
+ lollms_client-0.23.0.dist-info/METADATA,sha256=62EWlcBzFSaYzS_288ZzzGMiXMhe2lmBCONfstVSYLk,13374
83
+ lollms_client-0.23.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
84
+ lollms_client-0.23.0.dist-info/top_level.txt,sha256=NI_W8S4OYZvJjb0QWMZMSIpOrYzpqwPGYaklhyWKH2w,23
85
+ lollms_client-0.23.0.dist-info/RECORD,,
@@ -1,3 +1,2 @@
1
1
  examples
2
2
  lollms_client
3
- personalities
personalities/parrot.py DELETED
@@ -1,10 +0,0 @@
1
-
2
- def run(discussion, on_chunk_callback):
3
- # This script overrides the normal chat flow.
4
- user_message = discussion.get_branch(discussion.active_branch_id)[-1].content
5
- response = f"Squawk! {user_message}! Squawk!"
6
- if on_chunk_callback:
7
- # We need to simulate the message type for the callback
8
- from lollms_client import MSG_TYPE
9
- on_chunk_callback(response, MSG_TYPE.MSG_TYPE_CHUNK)
10
- return response # Return the full raw response
File without changes
File without changes