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.
- examples/{run_remote_mcp_example copy.py → mcp_examples/run_remote_mcp_example_v2.py} +65 -1
- lollms_client/__init__.py +1 -1
- lollms_client/lollms_core.py +268 -102
- lollms_client/lollms_discussion.py +135 -89
- lollms_client/lollms_llm_binding.py +3 -0
- lollms_client/lollms_types.py +1 -1
- {lollms_client-0.22.0.dist-info → lollms_client-0.23.0.dist-info}/METADATA +1 -1
- {lollms_client-0.22.0.dist-info → lollms_client-0.23.0.dist-info}/RECORD +15 -16
- {lollms_client-0.22.0.dist-info → lollms_client-0.23.0.dist-info}/top_level.txt +0 -1
- personalities/parrot.py +0 -10
- /examples/{external_mcp.py → mcp_examples/external_mcp.py} +0 -0
- /examples/{local_mcp.py → mcp_examples/local_mcp.py} +0 -0
- /examples/{openai_mcp.py → mcp_examples/openai_mcp.py} +0 -0
- /examples/{run_standard_mcp_example.py → mcp_examples/run_standard_mcp_example.py} +0 -0
- {lollms_client-0.22.0.dist-info → lollms_client-0.23.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.22.0.dist-info → lollms_client-0.23.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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.
|
|
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.
|
|
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__ = [
|
lollms_client/lollms_core.py
CHANGED
|
@@ -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],
|
|
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
|
-
|
|
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
|
|
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
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
"
|
|
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
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1451
|
-
|
|
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
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
if
|
|
1463
|
-
|
|
1464
|
-
|
|
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 =
|
|
1486
|
-
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
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1503
|
-
if not
|
|
1504
|
-
tool_result = {"
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
"chunks":
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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:
|
lollms_client/lollms_types.py
CHANGED
|
@@ -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=
|
|
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=
|
|
31
|
-
lollms_client/lollms_discussion.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
82
|
-
|
|
83
|
-
lollms_client-0.
|
|
84
|
-
lollms_client-0.
|
|
85
|
-
lollms_client-0.
|
|
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,,
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|