lollms-client 0.21.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/console_discussion.py +340 -99
- examples/gradio_lollms_chat.py +2 -2
- examples/lollms_discussions_test.py +3 -3
- examples/{run_remote_mcp_example copy.py → mcp_examples/run_remote_mcp_example_v2.py} +65 -1
- lollms_client/__init__.py +6 -3
- lollms_client/lollms_core.py +271 -103
- lollms_client/lollms_discussion.py +495 -418
- lollms_client/lollms_llm_binding.py +3 -0
- lollms_client/lollms_personality.py +182 -0
- lollms_client/lollms_types.py +1 -1
- {lollms_client-0.21.0.dist-info → lollms_client-0.23.0.dist-info}/METADATA +1 -1
- {lollms_client-0.21.0.dist-info → lollms_client-0.23.0.dist-info}/RECORD +19 -18
- /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.21.0.dist-info → lollms_client-0.23.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.21.0.dist-info → lollms_client-0.23.0.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.21.0.dist-info → lollms_client-0.23.0.dist-info}/top_level.txt +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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# lollms_client/__init__.py
|
|
2
2
|
from lollms_client.lollms_core import LollmsClient, ELF_COMPLETION_FORMAT
|
|
3
3
|
from lollms_client.lollms_types import MSG_TYPE # Assuming ELF_GENERATION_FORMAT is not directly used by users from here
|
|
4
|
-
from lollms_client.lollms_discussion import LollmsDiscussion,
|
|
4
|
+
from lollms_client.lollms_discussion import LollmsDiscussion, LollmsDataManager, LollmsMessage
|
|
5
|
+
from lollms_client.lollms_personality import LollmsPersonality
|
|
5
6
|
from lollms_client.lollms_utilities import PromptReshaper # Keep general utilities
|
|
6
7
|
# Import new MCP binding classes
|
|
7
8
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.23.0" # Updated version
|
|
11
12
|
|
|
12
13
|
# Optionally, you could define __all__ if you want to be explicit about exports
|
|
13
14
|
__all__ = [
|
|
@@ -15,7 +16,9 @@ __all__ = [
|
|
|
15
16
|
"ELF_COMPLETION_FORMAT",
|
|
16
17
|
"MSG_TYPE",
|
|
17
18
|
"LollmsDiscussion",
|
|
18
|
-
"
|
|
19
|
+
"LollmsMessage",
|
|
20
|
+
"LollmsPersonality",
|
|
21
|
+
"LollmsDataManager",
|
|
19
22
|
"PromptReshaper",
|
|
20
23
|
"LollmsMCPBinding", # Export LollmsMCPBinding ABC
|
|
21
24
|
"LollmsMCPBindingManager", # Export LollmsMCPBindingManager
|
lollms_client/lollms_core.py
CHANGED
|
@@ -76,7 +76,9 @@ class LollmsClient():
|
|
|
76
76
|
n_threads: int = 8,
|
|
77
77
|
streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None,
|
|
78
78
|
user_name ="user",
|
|
79
|
-
ai_name = "assistant"
|
|
79
|
+
ai_name = "assistant",
|
|
80
|
+
**kwargs
|
|
81
|
+
):
|
|
80
82
|
"""
|
|
81
83
|
Initialize the LollmsClient with LLM and optional modality bindings.
|
|
82
84
|
|
|
@@ -524,7 +526,8 @@ class LollmsClient():
|
|
|
524
526
|
seed: Optional[int] = None,
|
|
525
527
|
n_threads: Optional[int] = None,
|
|
526
528
|
ctx_size: Optional[int] = None,
|
|
527
|
-
streaming_callback: Optional[Callable[[str, MSG_TYPE],
|
|
529
|
+
streaming_callback: Optional[Callable[[str, MSG_TYPE, Dict], bool]] = None,
|
|
530
|
+
**kwargs
|
|
528
531
|
) -> Union[str, dict]:
|
|
529
532
|
"""
|
|
530
533
|
High-level method to perform a chat generation using a LollmsDiscussion object.
|
|
@@ -556,7 +559,7 @@ class LollmsClient():
|
|
|
556
559
|
discussion=discussion,
|
|
557
560
|
branch_tip_id=branch_tip_id,
|
|
558
561
|
n_predict=n_predict if n_predict is not None else self.default_n_predict,
|
|
559
|
-
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,
|
|
560
563
|
temperature=temperature if temperature is not None else self.default_temperature,
|
|
561
564
|
top_k=top_k if top_k is not None else self.default_top_k,
|
|
562
565
|
top_p=top_p if top_p is not None else self.default_top_p,
|
|
@@ -1281,33 +1284,180 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
|
|
|
1281
1284
|
"error": None
|
|
1282
1285
|
}
|
|
1283
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
|
+
|
|
1284
1437
|
def generate_with_mcp_rag(
|
|
1285
1438
|
self,
|
|
1286
1439
|
prompt: str,
|
|
1287
|
-
|
|
1440
|
+
use_mcps: Union[None, bool, List[str]] = None,
|
|
1441
|
+
use_data_store: Union[None, Dict[str, Callable]] = None,
|
|
1288
1442
|
system_prompt: str = None,
|
|
1289
1443
|
objective_extraction_system_prompt="Extract objectives",
|
|
1290
1444
|
images: Optional[List[str]] = None,
|
|
1291
|
-
tools: Optional[List[Dict[str, Any]]] = None,
|
|
1292
1445
|
max_tool_calls: int = 10,
|
|
1293
1446
|
max_llm_iterations: int = 15,
|
|
1294
1447
|
tool_call_decision_temperature: float = 0.0,
|
|
1295
1448
|
final_answer_temperature: float = None,
|
|
1296
1449
|
streaming_callback: Optional[Callable[[str, MSG_TYPE, Optional[Dict], Optional[List]], bool]] = None,
|
|
1297
1450
|
build_plan: bool = True,
|
|
1298
|
-
rag_vectorizer_name: Optional[str] = None,
|
|
1299
1451
|
rag_top_k: int = 5,
|
|
1300
1452
|
rag_min_similarity_percent: float = 70.0,
|
|
1301
1453
|
**llm_generation_kwargs
|
|
1302
1454
|
) -> Dict[str, Any]:
|
|
1303
1455
|
"""
|
|
1304
1456
|
Generates a response using a stateful agent that can choose between calling standard
|
|
1305
|
-
MCP tools and querying
|
|
1457
|
+
MCP tools and querying one or more RAG databases, all within a unified reasoning loop.
|
|
1306
1458
|
"""
|
|
1307
1459
|
if not self.binding:
|
|
1308
1460
|
return {"final_answer": "", "tool_calls": [], "error": "LLM binding not initialized."}
|
|
1309
|
-
if not self.mcp:
|
|
1310
|
-
return {"final_answer": "", "tool_calls": [], "error": "MCP binding not initialized."}
|
|
1311
1461
|
|
|
1312
1462
|
# --- Initialize Agent State ---
|
|
1313
1463
|
turn_history: List[Dict[str, Any]] = []
|
|
@@ -1318,48 +1468,56 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
|
|
|
1318
1468
|
tool_calls_made_this_turn = []
|
|
1319
1469
|
llm_iterations = 0
|
|
1320
1470
|
|
|
1321
|
-
# --- 1. Discover
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
"
|
|
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"]
|
|
1344
1501
|
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
+
|
|
1350
1510
|
|
|
1351
|
-
# --- 2. Optional Initial Objectives Extraction ---
|
|
1352
1511
|
formatted_tools_list = "\n".join([
|
|
1353
1512
|
f"- Full Tool Name: {t.get('name')}\n Description: {t.get('description')}\n Input Schema: {json.dumps(t.get('input_schema'))}"
|
|
1354
1513
|
for t in available_tools
|
|
1355
|
-
])
|
|
1514
|
+
])
|
|
1515
|
+
|
|
1516
|
+
# --- 2. Optional Initial Objectives Extraction ---
|
|
1356
1517
|
if build_plan:
|
|
1357
1518
|
if streaming_callback:
|
|
1358
1519
|
streaming_callback("Extracting initial objectives...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "objectives_extraction"}, turn_history)
|
|
1359
1520
|
|
|
1360
|
-
# The enhanced prompt is placed inside the original parenthesis format.
|
|
1361
|
-
# The f-strings for tool lists and user prompts are preserved.
|
|
1362
|
-
|
|
1363
1521
|
obj_prompt = (
|
|
1364
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"
|
|
1365
1523
|
"Your plan must be the most direct and minimal path to the user's goal.\n\n"
|
|
@@ -1388,8 +1546,6 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
|
|
|
1388
1546
|
|
|
1389
1547
|
turn_history.append({"type": "initial_objectives", "content": current_objectives})
|
|
1390
1548
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
1549
|
# --- 3. Main Agent Loop ---
|
|
1394
1550
|
while llm_iterations < max_llm_iterations:
|
|
1395
1551
|
llm_iterations += 1
|
|
@@ -1401,15 +1557,19 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
|
|
|
1401
1557
|
if agent_work_history:
|
|
1402
1558
|
history_parts = []
|
|
1403
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
|
+
|
|
1404
1565
|
history_parts.append(
|
|
1405
1566
|
f"### Step {i+1}:\n"
|
|
1406
1567
|
f"**Thought:** {entry['thought']}\n"
|
|
1407
1568
|
f"**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n"
|
|
1408
|
-
f"**Observation:**\n```json\n{json.dumps(
|
|
1569
|
+
f"**Observation:**\n```json\n{json.dumps(sanitized_hist_result, indent=2)}\n```"
|
|
1409
1570
|
)
|
|
1410
1571
|
formatted_agent_history = "\n\n".join(history_parts)
|
|
1411
1572
|
|
|
1412
|
-
# Construct the "Thinking & Planning" prompt
|
|
1413
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.
|
|
1414
1574
|
|
|
1415
1575
|
--- AVAILABLE TOOLS ---
|
|
@@ -1429,50 +1589,47 @@ Knowledge Scratchpad (our current understanding):
|
|
|
1429
1589
|
--- INSTRUCTIONS ---
|
|
1430
1590
|
1. **Analyze:** Review the entire work history, objectives, and scratchpad.
|
|
1431
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.
|
|
1432
|
-
3. **Decide Next Action:** Choose ONE of the following: `call_tool`, `final_answer`, or `clarify`. Always prefer to gather information with `research
|
|
1433
|
-
|
|
1434
|
-
--- OUTPUT FORMAT ---
|
|
1435
|
-
Respond with a single JSON object inside a JSON markdown tag. Use this exact schema:
|
|
1436
|
-
```json
|
|
1437
|
-
{{
|
|
1438
|
-
"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).",
|
|
1439
|
-
"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.",
|
|
1440
|
-
"updated_objectives": "The full, potentially revised, list of objectives. If no change, repeat the current list.",
|
|
1441
|
-
"action": "The chosen action: 'call_tool', 'final_answer', or 'clarify'.",
|
|
1442
|
-
"tool_name": "(string, if action is 'call_tool') The full 'alias::tool_name' of the tool to use.",
|
|
1443
|
-
"tool_params": {{"query": "...", "param2": "..."}},
|
|
1444
|
-
"clarification_request": "(string, if action is 'clarify') Your question to the user."
|
|
1445
|
-
}}
|
|
1446
|
-
```
|
|
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.
|
|
1447
1593
|
"""
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
|
1450
1609
|
)
|
|
1610
|
+
|
|
1611
|
+
if not llm_decision:
|
|
1612
|
+
ASCIIColors.error("LLM failed to generate a valid decision JSON. Aborting loop.")
|
|
1613
|
+
break
|
|
1451
1614
|
|
|
1452
1615
|
# --- 4. Parse LLM's plan and update state ---
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
if
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
streaming_callback(f"Knowledge scratchpad updated.", MSG_TYPE.MSG_TYPE_STEP, {"id": "scratchpad_update"}, turn_history)
|
|
1464
|
-
streaming_callback(f"New Scratchpad:\n{knowledge_scratchpad}", MSG_TYPE.MSG_TYPE_INFO, {"id":"scratch_pad_update"}, turn_history)
|
|
1465
|
-
|
|
1466
|
-
except (json.JSONDecodeError, AttributeError, KeyError) as e:
|
|
1467
|
-
ASCIIColors.error(f"Failed to parse LLM decision JSON: {raw_llm_decision_json}. Error: {e}")
|
|
1468
|
-
turn_history.append({"type": "error", "content": f"Failed to parse LLM plan: {raw_llm_decision_json}"})
|
|
1469
|
-
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)
|
|
1470
1626
|
|
|
1471
1627
|
if streaming_callback:
|
|
1472
1628
|
streaming_callback(f"LLM thought: {llm_decision.get('thought', 'N/A')}", MSG_TYPE.MSG_TYPE_INFO, {"id": "llm_thought"}, turn_history)
|
|
1473
1629
|
|
|
1474
1630
|
# --- 5. Execute the chosen action ---
|
|
1475
1631
|
action = llm_decision.get("action")
|
|
1632
|
+
action_details = llm_decision.get("action_details", {})
|
|
1476
1633
|
tool_result = None
|
|
1477
1634
|
|
|
1478
1635
|
if action == "call_tool":
|
|
@@ -1480,40 +1637,53 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
|
|
|
1480
1637
|
ASCIIColors.warning("Max tool calls reached. Forcing final answer.")
|
|
1481
1638
|
break
|
|
1482
1639
|
|
|
1483
|
-
tool_name =
|
|
1484
|
-
tool_params =
|
|
1640
|
+
tool_name = action_details.get("tool_name")
|
|
1641
|
+
tool_params = action_details.get("tool_params", {})
|
|
1485
1642
|
|
|
1486
1643
|
if not tool_name or not isinstance(tool_params, dict):
|
|
1487
1644
|
ASCIIColors.error(f"Invalid tool call from LLM: name={tool_name}, params={tool_params}")
|
|
1488
1645
|
break
|
|
1489
1646
|
|
|
1490
1647
|
if streaming_callback:
|
|
1491
|
-
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)
|
|
1492
1649
|
|
|
1493
1650
|
try:
|
|
1494
1651
|
# ** DYNAMIC TOOL/RAG DISPATCH **
|
|
1495
|
-
if tool_name
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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."}
|
|
1499
1657
|
else:
|
|
1500
|
-
|
|
1501
|
-
if not
|
|
1502
|
-
tool_result = {"
|
|
1658
|
+
query = tool_params.get("query")
|
|
1659
|
+
if not query:
|
|
1660
|
+
tool_result = {"error": "RAG tool called without a 'query' parameter."}
|
|
1503
1661
|
else:
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
"chunks":
|
|
1507
|
-
|
|
1508
|
-
|
|
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:
|
|
1509
1671
|
# Standard MCP tool execution
|
|
1510
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."}
|
|
1511
1675
|
|
|
1512
1676
|
except Exception as e_exec:
|
|
1513
1677
|
trace_exception(e_exec)
|
|
1514
1678
|
tool_result = {"error": f"An exception occurred while executing tool '{tool_name}': {e_exec}"}
|
|
1515
1679
|
|
|
1516
|
-
|
|
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
|
+
|
|
1517
1687
|
work_entry = {
|
|
1518
1688
|
"thought": llm_decision.get("thought", "N/A"),
|
|
1519
1689
|
"tool_name": tool_name,
|
|
@@ -1522,13 +1692,9 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
|
|
|
1522
1692
|
}
|
|
1523
1693
|
agent_work_history.append(work_entry)
|
|
1524
1694
|
tool_calls_made_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
|
|
1525
|
-
|
|
1526
|
-
if streaming_callback:
|
|
1527
|
-
streaming_callback(f"Tool {tool_name} finished.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"tool_exec_{llm_iterations}"}, turn_history)
|
|
1528
|
-
streaming_callback(json.dumps(tool_result, indent=2), MSG_TYPE.MSG_TYPE_TOOL_OUTPUT, tool_result, turn_history)
|
|
1529
1695
|
|
|
1530
1696
|
elif action == "clarify":
|
|
1531
|
-
clarification_request =
|
|
1697
|
+
clarification_request = action_details.get("clarification_request", "I need more information. Could you please clarify?")
|
|
1532
1698
|
return {"final_answer": clarification_request, "tool_calls": tool_calls_made_this_turn, "error": None, "clarification": True}
|
|
1533
1699
|
|
|
1534
1700
|
elif action == "final_answer":
|
|
@@ -1542,7 +1708,8 @@ Respond with a single JSON object inside a JSON markdown tag. Use this exact sch
|
|
|
1542
1708
|
streaming_callback(f"LLM reasoning step (iteration {llm_iterations})...", MSG_TYPE.MSG_TYPE_STEP_END, {"id": f"planning_step_{llm_iterations}"}, turn_history)
|
|
1543
1709
|
|
|
1544
1710
|
if streaming_callback:
|
|
1545
|
-
|
|
1711
|
+
streaming_callback(f"LLM reasoning loop finished.", MSG_TYPE.MSG_TYPE_STEP, {"id": "reasoning_loop_end"}, turn_history)
|
|
1712
|
+
|
|
1546
1713
|
# --- 6. Generate Final Answer ---
|
|
1547
1714
|
if streaming_callback:
|
|
1548
1715
|
streaming_callback("Synthesizing final answer...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "final_answer_synthesis"}, turn_history)
|
|
@@ -1577,7 +1744,8 @@ Original User Request: "{original_user_prompt}"
|
|
|
1577
1744
|
turn_history.append({"type":"final_answer_generated", "content": final_answer})
|
|
1578
1745
|
|
|
1579
1746
|
return {"final_answer": final_answer, "tool_calls": tool_calls_made_this_turn, "error": None}
|
|
1580
|
-
|
|
1747
|
+
|
|
1748
|
+
|
|
1581
1749
|
def generate_code(
|
|
1582
1750
|
self,
|
|
1583
1751
|
prompt,
|