lollms-client 0.20.4__tar.gz → 0.20.7__tar.gz
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.
- {lollms_client-0.20.4/lollms_client.egg-info → lollms_client-0.20.7}/PKG-INFO +1 -1
- lollms_client-0.20.7/examples/run_remote_mcp_example copy.py +226 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/__init__.py +2 -2
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_discussion.py +14 -3
- lollms_client-0.20.7/lollms_client/mcp_bindings/remote_mcp/__init__.py +342 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7/lollms_client.egg-info}/PKG-INFO +1 -1
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client.egg-info/SOURCES.txt +1 -0
- lollms_client-0.20.4/lollms_client/mcp_bindings/remote_mcp/__init__.py +0 -241
- {lollms_client-0.20.4 → lollms_client-0.20.7}/LICENSE +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/README.md +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/article_summary/article_summary.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/deep_analyze/deep_analyse.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/deep_analyze/deep_analyze_multiple_files.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/external_mcp.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/function_calling_with_local_custom_mcp.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_a_benchmark_for_safe_store.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_and_speak/generate_and_speak.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_game_sfx/generate_game_fx.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_text_with_multihop_rag_example.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/gradio_chat_app.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/internet_search_with_rag.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/local_mcp.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/openai_mcp.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/personality_test/chat_test.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/personality_test/chat_with_aristotle.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/personality_test/tesks_test.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/run_standard_mcp_example.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/simple_text_gen_test.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/simple_text_gen_with_image_test.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/test_local_models/local_chat.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/text_2_audio.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/text_2_image.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/text_2_image_diffusers.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/text_and_image_2_audio.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/text_gen.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/examples/text_gen_system_prompt.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/llamacpp/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/lollms/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/ollama/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/openai/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/openllm/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/pythonllamacpp/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/tensor_rt/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/transformers/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/vllm/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_config.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_core.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_js_analyzer.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_llm_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_mcp_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_python_analyzer.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_stt_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_tti_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_ttm_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_tts_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_ttv_binding.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_types.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/lollms_utilities.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/local_mcp/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/local_mcp/default_tools/file_writer/file_writer.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/local_mcp/default_tools/generate_image_from_prompt/generate_image_from_prompt.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/local_mcp/default_tools/internet_search/internet_search.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/local_mcp/default_tools/python_interpreter/python_interpreter.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/standard_mcp/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/stt_bindings/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/stt_bindings/lollms/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/stt_bindings/whisper/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/stt_bindings/whispercpp/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tti_bindings/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tti_bindings/dalle/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tti_bindings/diffusers/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tti_bindings/gemini/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tti_bindings/lollms/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttm_bindings/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttm_bindings/audiocraft/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttm_bindings/bark/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttm_bindings/lollms/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tts_bindings/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tts_bindings/bark/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tts_bindings/lollms/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tts_bindings/piper_tts/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tts_bindings/xtts/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttv_bindings/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttv_bindings/lollms/__init__.py +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client.egg-info/dependency_links.txt +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client.egg-info/requires.txt +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client.egg-info/top_level.txt +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/pyproject.toml +0 -0
- {lollms_client-0.20.4 → lollms_client-0.20.7}/setup.cfg +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# File: run_lollms_client_with_mcp_example.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import json
|
|
8
|
+
from lollms_client import LollmsClient
|
|
9
|
+
import subprocess
|
|
10
|
+
# --- Dynamically adjust Python path to find lollms_client ---
|
|
11
|
+
# This assumes the example script is in a directory, and 'lollms_client' is
|
|
12
|
+
# in a sibling directory or a known relative path. Adjust as needed.
|
|
13
|
+
# For example, if script is in 'lollms_client/examples/' and lollms_client code is in 'lollms_client/'
|
|
14
|
+
# then the parent of the script's parent is the project root.
|
|
15
|
+
|
|
16
|
+
# Get the directory of the current script
|
|
17
|
+
current_script_dir = Path(__file__).resolve().parent
|
|
18
|
+
|
|
19
|
+
# Option 1: If lollms_client is in the parent directory of this script's directory
|
|
20
|
+
# (e.g. script is in 'project_root/examples' and lollms_client is in 'project_root/lollms_client')
|
|
21
|
+
# project_root = current_script_dir.parent
|
|
22
|
+
# lollms_client_path = project_root / "lollms_client" # Assuming this is where lollms_client.py and bindings are
|
|
23
|
+
|
|
24
|
+
# Option 2: If lollms_client package is directly one level up
|
|
25
|
+
# (e.g. script is in 'lollms_client/examples' and lollms_client package is 'lollms_client')
|
|
26
|
+
project_root_for_lollms_client = current_script_dir.parent
|
|
27
|
+
if str(project_root_for_lollms_client) not in sys.path:
|
|
28
|
+
sys.path.insert(0, str(project_root_for_lollms_client))
|
|
29
|
+
print(f"Added to sys.path: {project_root_for_lollms_client}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- Ensure pipmaster is available (core LoLLMs dependency) ---
|
|
33
|
+
try:
|
|
34
|
+
import pipmaster as pm
|
|
35
|
+
except ImportError:
|
|
36
|
+
print("ERROR: pipmaster is not installed or not in PYTHONPATH.")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
# --- Import LollmsClient and supporting components ---
|
|
40
|
+
try:
|
|
41
|
+
|
|
42
|
+
from lollms_client.lollms_llm_binding import LollmsLLMBinding # Base for LLM
|
|
43
|
+
from ascii_colors import ASCIIColors, trace_exception
|
|
44
|
+
from lollms_client.lollms_types import MSG_TYPE # Assuming MSG_TYPE is here
|
|
45
|
+
except ImportError as e:
|
|
46
|
+
print(f"ERROR: Could not import LollmsClient components: {e}")
|
|
47
|
+
print("Ensure 'lollms_client' package structure is correct and accessible via PYTHONPATH.")
|
|
48
|
+
print(f"Current sys.path: {sys.path}")
|
|
49
|
+
trace_exception(e)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --- Dummy Server Scripts using FastMCP (as per previous successful iteration) ---
|
|
54
|
+
TIME_SERVER_PY = """
|
|
55
|
+
import asyncio
|
|
56
|
+
from datetime import datetime
|
|
57
|
+
from mcp.server.fastmcp import FastMCP
|
|
58
|
+
|
|
59
|
+
mcp_server = FastMCP("TimeMCP", description="A server that provides the current time.", host="localhost",
|
|
60
|
+
port=9624,
|
|
61
|
+
log_level="DEBUG")
|
|
62
|
+
|
|
63
|
+
@mcp_server.tool(description="Returns the current server time and echoes received parameters.")
|
|
64
|
+
def get_current_time(user_id: str = "unknown_user") -> dict:
|
|
65
|
+
return {"time": datetime.now().isoformat(), "params_received": {"user_id": user_id}, "server_name": "TimeServer"}
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
mcp_server.run(transport="streamable-http")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
CALCULATOR_SERVER_PY = """
|
|
72
|
+
import asyncio
|
|
73
|
+
from typing import List, Union
|
|
74
|
+
from mcp.server.fastmcp import FastMCP
|
|
75
|
+
|
|
76
|
+
mcp_server = FastMCP("CalculatorMCP", description="A server that performs addition.", host="localhost",
|
|
77
|
+
port=9625,
|
|
78
|
+
log_level="DEBUG")
|
|
79
|
+
|
|
80
|
+
@mcp_server.tool(description="Adds a list of numbers provided in the 'numbers' parameter.")
|
|
81
|
+
def add_numbers(numbers: List[Union[int, float]]) -> dict:
|
|
82
|
+
if not isinstance(numbers, list) or not all(isinstance(x, (int, float)) for x in numbers):
|
|
83
|
+
return {"error": "'numbers' must be a list of numbers."}
|
|
84
|
+
return {"sum": sum(numbers), "server_name": "CalculatorServer"}
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
mcp_server.run(transport="streamable-http")
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
ASCIIColors.red("--- Example: Using LollmsClient with StandardMCPBinding ---")
|
|
93
|
+
|
|
94
|
+
# --- 1. Setup Temporary Directory for Dummy MCP Servers ---
|
|
95
|
+
example_base_dir = Path(__file__).parent / "temp_mcp_example_servers"
|
|
96
|
+
if example_base_dir.exists():
|
|
97
|
+
shutil.rmtree(example_base_dir)
|
|
98
|
+
example_base_dir.mkdir(exist_ok=True)
|
|
99
|
+
|
|
100
|
+
time_server_script_path = example_base_dir / "time_server.py"
|
|
101
|
+
with open(time_server_script_path, "w") as f: f.write(TIME_SERVER_PY)
|
|
102
|
+
|
|
103
|
+
calculator_server_script_path = example_base_dir / "calculator_server.py"
|
|
104
|
+
with open(calculator_server_script_path, "w") as f: f.write(CALCULATOR_SERVER_PY)
|
|
105
|
+
|
|
106
|
+
subprocess.Popen(
|
|
107
|
+
[sys.executable, str(time_server_script_path.resolve())],
|
|
108
|
+
stdin=subprocess.DEVNULL,
|
|
109
|
+
stdout=subprocess.DEVNULL,
|
|
110
|
+
stderr=subprocess.DEVNULL,
|
|
111
|
+
start_new_session=True
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
subprocess.Popen(
|
|
115
|
+
[sys.executable, str(calculator_server_script_path.resolve())],
|
|
116
|
+
stdin=subprocess.DEVNULL,
|
|
117
|
+
stdout=subprocess.DEVNULL,
|
|
118
|
+
stderr=subprocess.DEVNULL,
|
|
119
|
+
start_new_session=True
|
|
120
|
+
)
|
|
121
|
+
# MCP Binding Configuration (for RemoteMCPBinding with multiple servers)
|
|
122
|
+
mcp_config = {
|
|
123
|
+
"servers_infos":{
|
|
124
|
+
"time_machine":{
|
|
125
|
+
"server_url": "http://localhost:9624/mcp",
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"calc_unit":{
|
|
129
|
+
"server_url": "http://localhost:9625/mcp",
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
ASCIIColors.magenta("\n1. Initializing LollmsClient...")
|
|
134
|
+
try:
|
|
135
|
+
client = LollmsClient(
|
|
136
|
+
binding_name="ollama", # Use the dummy LLM binding
|
|
137
|
+
model_name="mistral-nemo:latest",
|
|
138
|
+
mcp_binding_name="remote_mcp",
|
|
139
|
+
mcp_binding_config=mcp_config,
|
|
140
|
+
|
|
141
|
+
)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
ASCIIColors.error(f"Failed to initialize LollmsClient: {e}")
|
|
144
|
+
trace_exception(e)
|
|
145
|
+
shutil.rmtree(example_base_dir)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
if not client.binding:
|
|
149
|
+
ASCIIColors.error("LollmsClient's LLM binding (dummy_llm) failed to load.")
|
|
150
|
+
shutil.rmtree(example_base_dir)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
if not client.mcp:
|
|
153
|
+
ASCIIColors.error("LollmsClient's MCP binding (standard_mcp) failed to load.")
|
|
154
|
+
client.close() # Close LLM binding if it loaded
|
|
155
|
+
shutil.rmtree(example_base_dir)
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
ASCIIColors.green("LollmsClient initialized successfully with DummyLLM and StandardMCP bindings.")
|
|
159
|
+
|
|
160
|
+
# --- 3. Define a streaming callback for generate_with_mcp ---
|
|
161
|
+
def mcp_streaming_callback(chunk: str, msg_type: MSG_TYPE, metadata: dict = None, history: list = None) -> bool:
|
|
162
|
+
if metadata:
|
|
163
|
+
type_info = metadata.get('type', 'unknown_type')
|
|
164
|
+
if msg_type == MSG_TYPE.MSG_TYPE_STEP_START:
|
|
165
|
+
ASCIIColors.cyan(f"MCP Step Start ({type_info}): {chunk}")
|
|
166
|
+
elif msg_type == MSG_TYPE.MSG_TYPE_STEP_END:
|
|
167
|
+
ASCIIColors.cyan(f"MCP Step End ({type_info}): {chunk}")
|
|
168
|
+
elif msg_type == MSG_TYPE.MSG_TYPE_INFO:
|
|
169
|
+
ASCIIColors.yellow(f"MCP Info ({type_info}): {chunk}")
|
|
170
|
+
elif msg_type == MSG_TYPE.MSG_TYPE_CHUNK: # Part of final answer typically
|
|
171
|
+
ASCIIColors.green(chunk, end="") # type: ignore
|
|
172
|
+
else: # FULL, default, etc.
|
|
173
|
+
ASCIIColors.green(f"MCP Output ({str(msg_type)}, {type_info}): {chunk}")
|
|
174
|
+
else:
|
|
175
|
+
if msg_type == MSG_TYPE.MSG_TYPE_CHUNK:
|
|
176
|
+
ASCIIColors.green(chunk, end="") # type: ignore
|
|
177
|
+
else:
|
|
178
|
+
ASCIIColors.green(f"MCP Output ({str(msg_type)}): {chunk}")
|
|
179
|
+
sys.stdout.flush()
|
|
180
|
+
return True # Continue streaming
|
|
181
|
+
|
|
182
|
+
# --- 4. Use generate_with_mcp ---
|
|
183
|
+
ASCIIColors.magenta("\n2. Calling generate_with_mcp to get current time...")
|
|
184
|
+
time_prompt = "Hey assistant, what time is it right now?"
|
|
185
|
+
time_response = client.generate_with_mcp(
|
|
186
|
+
prompt=time_prompt,
|
|
187
|
+
streaming_callback=mcp_streaming_callback,
|
|
188
|
+
interactive_tool_execution=False # Set to True to test interactive mode
|
|
189
|
+
)
|
|
190
|
+
print() # Newline after streaming
|
|
191
|
+
ASCIIColors.blue(f"Final response for time prompt: {json.dumps(time_response, indent=2)}")
|
|
192
|
+
|
|
193
|
+
assert time_response.get("error") is None, f"Time prompt resulted in an error: {time_response.get('error')}"
|
|
194
|
+
assert time_response.get("final_answer"), "Time prompt did not produce a final answer."
|
|
195
|
+
assert len(time_response.get("tool_calls", [])) > 0, "Time prompt should have called a tool."
|
|
196
|
+
assert time_response["tool_calls"][0]["name"] == "time_machine::get_current_time", "Incorrect tool called for time."
|
|
197
|
+
assert "time" in time_response["tool_calls"][0].get("result", {}).get("output", {}), "Time tool result missing time."
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
ASCIIColors.magenta("\n3. Calling generate_with_mcp for calculation...")
|
|
201
|
+
calc_prompt = "Can you please calculate the sum of 50, 25, and 7.5 for me?"
|
|
202
|
+
calc_response = client.generate_with_mcp(
|
|
203
|
+
prompt=calc_prompt,
|
|
204
|
+
streaming_callback=mcp_streaming_callback
|
|
205
|
+
)
|
|
206
|
+
print() # Newline
|
|
207
|
+
ASCIIColors.blue(f"Final response for calc prompt: {json.dumps(calc_response, indent=2)}")
|
|
208
|
+
|
|
209
|
+
assert calc_response.get("error") is None, f"Calc prompt resulted in an error: {calc_response.get('error')}"
|
|
210
|
+
assert calc_response.get("final_answer"), "Calc prompt did not produce a final answer."
|
|
211
|
+
assert len(calc_response.get("tool_calls", [])) > 0, "Calc prompt should have called a tool."
|
|
212
|
+
assert calc_response["tool_calls"][0]["name"] == "calc_unit::add_numbers", "Incorrect tool called for calculation."
|
|
213
|
+
# The dummy LLM uses hardcoded params [1,2,3] for calc, so result will be 6.
|
|
214
|
+
# A real LLM would extract 50, 25, 7.5.
|
|
215
|
+
# For this dummy test, we check against the dummy's behavior.
|
|
216
|
+
assert calc_response["tool_calls"][0].get("result", {}).get("output", {}).get("sum") == 82.5, "Calculator tool result mismatch for dummy params."
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --- 5. Cleanup ---
|
|
220
|
+
ASCIIColors.info("Cleaning up temporary server scripts and dummy binding directory...")
|
|
221
|
+
shutil.rmtree(example_base_dir, ignore_errors=True)
|
|
222
|
+
|
|
223
|
+
ASCIIColors.red("\n--- LollmsClient with StandardMCPBinding Example Finished Successfully! ---")
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|
|
@@ -7,7 +7,7 @@ from lollms_client.lollms_utilities import PromptReshaper # Keep general utiliti
|
|
|
7
7
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
__version__ = "0.20.
|
|
10
|
+
__version__ = "0.20.7" # Updated version
|
|
11
11
|
|
|
12
12
|
# Optionally, you could define __all__ if you want to be explicit about exports
|
|
13
13
|
__all__ = [
|
|
@@ -19,4 +19,4 @@ __all__ = [
|
|
|
19
19
|
"PromptReshaper",
|
|
20
20
|
"LollmsMCPBinding", # Export LollmsMCPBinding ABC
|
|
21
21
|
"LollmsMCPBindingManager", # Export LollmsMCPBindingManager
|
|
22
|
-
]
|
|
22
|
+
]
|
|
@@ -9,6 +9,7 @@ from collections import defaultdict
|
|
|
9
9
|
@dataclass
|
|
10
10
|
class LollmsMessage:
|
|
11
11
|
sender: str
|
|
12
|
+
sender_type: str
|
|
12
13
|
content: str
|
|
13
14
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
14
15
|
parent_id: Optional[str] = None
|
|
@@ -18,6 +19,7 @@ class LollmsMessage:
|
|
|
18
19
|
def to_dict(self):
|
|
19
20
|
return {
|
|
20
21
|
'sender': self.sender,
|
|
22
|
+
'sender_type': self.sender_type,
|
|
21
23
|
'content': self.content,
|
|
22
24
|
'id': self.id,
|
|
23
25
|
'parent_id': self.parent_id,
|
|
@@ -51,21 +53,28 @@ class LollmsDiscussion:
|
|
|
51
53
|
def add_message(
|
|
52
54
|
self,
|
|
53
55
|
sender: str,
|
|
56
|
+
sender_type: str,
|
|
54
57
|
content: str,
|
|
55
58
|
metadata: Dict = {},
|
|
56
59
|
parent_id: Optional[str] = None,
|
|
57
|
-
images: Optional[List[Dict[str, str]]] = None
|
|
60
|
+
images: Optional[List[Dict[str, str]]] = None,
|
|
61
|
+
override_id: Optional[str] = None
|
|
58
62
|
) -> str:
|
|
59
63
|
if parent_id is None:
|
|
60
64
|
parent_id = self.active_branch_id
|
|
65
|
+
if parent_id is None:
|
|
66
|
+
parent_id = "main"
|
|
61
67
|
|
|
62
68
|
message = LollmsMessage(
|
|
63
69
|
sender=sender,
|
|
70
|
+
sender_type=sender_type,
|
|
64
71
|
content=content,
|
|
65
72
|
parent_id=parent_id,
|
|
66
73
|
metadata=str(metadata),
|
|
67
74
|
images=images or []
|
|
68
75
|
)
|
|
76
|
+
if override_id:
|
|
77
|
+
message.id = override_id
|
|
69
78
|
|
|
70
79
|
self.messages.append(message)
|
|
71
80
|
self.message_index[message.id] = message
|
|
@@ -146,8 +155,10 @@ class LollmsDiscussion:
|
|
|
146
155
|
# Legacy v1 format
|
|
147
156
|
prev_id = None
|
|
148
157
|
for msg_data in data:
|
|
158
|
+
sender = msg_data.get('sender',"unknown")
|
|
149
159
|
msg = LollmsMessage(
|
|
150
|
-
sender=
|
|
160
|
+
sender=sender,
|
|
161
|
+
sender_type=msg_data.get("sender_type", "user" if sender!="lollms" and sender!="assistant" else "assistant"),
|
|
151
162
|
content=msg_data['content'],
|
|
152
163
|
parent_id=prev_id,
|
|
153
164
|
id=msg_data.get('id', str(uuid.uuid4())),
|
|
@@ -491,4 +502,4 @@ if __name__ == "__main__":
|
|
|
491
502
|
final_message = new_discussion.message_index[new_discussion.active_branch_id]
|
|
492
503
|
assert len(final_message.images) == 2
|
|
493
504
|
assert final_message.images[1]['type'] == 'base64'
|
|
494
|
-
print("\n✅ Verification successful: Images were loaded correctly from the file.")
|
|
505
|
+
print("\n✅ Verification successful: Images were loaded correctly from the file.")
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import AsyncExitStack
|
|
3
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
4
|
+
from lollms_client.lollms_mcp_binding import LollmsMCPBinding
|
|
5
|
+
from ascii_colors import ASCIIColors, trace_exception
|
|
6
|
+
import threading
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from mcp import ClientSession, types
|
|
11
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
12
|
+
MCP_LIBRARY_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
MCP_LIBRARY_AVAILABLE = False
|
|
15
|
+
ClientSession = None
|
|
16
|
+
streamablehttp_client = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
BindingName = "RemoteMCPBinding"
|
|
20
|
+
TOOL_NAME_SEPARATOR = "::"
|
|
21
|
+
|
|
22
|
+
class RemoteMCPBinding(LollmsMCPBinding):
|
|
23
|
+
"""
|
|
24
|
+
This binding allows the connection to one or more remote MCP servers.
|
|
25
|
+
Tools from all connected servers are aggregated and prefixed with the server's alias.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self,
|
|
28
|
+
servers_infos: Dict[str, Dict[str, Any]],
|
|
29
|
+
**other_config_params: Any):
|
|
30
|
+
"""
|
|
31
|
+
Initializes the binding to connect to multiple MCP servers.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
servers_infos (Dict[str, Dict[str, Any]]): A dictionary where each key is a unique
|
|
35
|
+
alias for a server, and the value is another dictionary containing connection
|
|
36
|
+
details for that server.
|
|
37
|
+
Example:
|
|
38
|
+
{
|
|
39
|
+
"main_server": {"server_url": "http://localhost:8787", "auth_config": {}},
|
|
40
|
+
"experimental_server": {"server_url": "http://test.server:9000"}
|
|
41
|
+
}
|
|
42
|
+
**other_config_params (Any): Additional configuration parameters.
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(binding_name="remote_mcp")
|
|
45
|
+
|
|
46
|
+
if not MCP_LIBRARY_AVAILABLE:
|
|
47
|
+
ASCIIColors.error(f"{self.binding_name}: MCP library not available. This binding will be disabled.")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if not servers_infos or not isinstance(servers_infos, dict):
|
|
51
|
+
ASCIIColors.error(f"{self.binding_name}: `servers_infos` dictionary is required and cannot be empty.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
### NEW: Store the overall configuration
|
|
55
|
+
self.config = {
|
|
56
|
+
"servers_infos": servers_infos,
|
|
57
|
+
**other_config_params
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
### NEW: State management for multiple servers.
|
|
61
|
+
# The key is the server alias. The value is a dictionary holding the state for that server.
|
|
62
|
+
self.servers: Dict[str, Dict[str, Any]] = {}
|
|
63
|
+
for alias, info in servers_infos.items():
|
|
64
|
+
if "server_url" not in info:
|
|
65
|
+
ASCIIColors.warning(f"{self.binding_name}: Skipping server '{alias}' due to missing 'server_url'.")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
self.servers[alias] = {
|
|
69
|
+
"url": info["server_url"],
|
|
70
|
+
"auth_config": info.get("auth_config", {}),
|
|
71
|
+
"session": None, # Will hold the ClientSession
|
|
72
|
+
"exit_stack": None, # Will hold the AsyncExitStack
|
|
73
|
+
"initialized": False,
|
|
74
|
+
"initializing_lock": threading.Lock() # Prevents race conditions on initialization
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
self._discovered_tools_cache: List[Dict[str, Any]] = []
|
|
78
|
+
|
|
79
|
+
### MODIFIED: These are now shared across all connections
|
|
80
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
81
|
+
self._thread: Optional[threading.Thread] = None
|
|
82
|
+
self._loop_started_event = threading.Event()
|
|
83
|
+
|
|
84
|
+
if self.servers:
|
|
85
|
+
self._start_event_loop_thread()
|
|
86
|
+
else:
|
|
87
|
+
ASCIIColors.warning(f"{self.binding_name}: No valid servers configured.")
|
|
88
|
+
|
|
89
|
+
# _start_event_loop_thread, _run_loop_forever, _wait_for_loop, _run_async
|
|
90
|
+
# are utility methods for the shared event loop and do not need to be changed.
|
|
91
|
+
# They manage the async infrastructure for the entire binding instance.
|
|
92
|
+
def _start_event_loop_thread(self):
|
|
93
|
+
if self._loop and self._loop.is_running(): return
|
|
94
|
+
self._loop = asyncio.new_event_loop()
|
|
95
|
+
self._thread = threading.Thread(target=self._run_loop_forever, daemon=True)
|
|
96
|
+
self._thread.start()
|
|
97
|
+
|
|
98
|
+
def _run_loop_forever(self):
|
|
99
|
+
if not self._loop: return
|
|
100
|
+
asyncio.set_event_loop(self._loop)
|
|
101
|
+
try:
|
|
102
|
+
self._loop_started_event.set()
|
|
103
|
+
self._loop.run_forever()
|
|
104
|
+
finally:
|
|
105
|
+
if not self._loop.is_closed(): self._loop.close()
|
|
106
|
+
|
|
107
|
+
def _wait_for_loop(self, timeout=5.0):
|
|
108
|
+
if not self._loop_started_event.wait(timeout=timeout):
|
|
109
|
+
raise RuntimeError(f"{self.binding_name}: Event loop thread failed to start in time.")
|
|
110
|
+
if not self._loop or not self._loop.is_running():
|
|
111
|
+
raise RuntimeError(f"{self.binding_name}: Event loop is not running after start signal.")
|
|
112
|
+
|
|
113
|
+
def _run_async(self, coro, timeout=None):
|
|
114
|
+
if not self._loop or not self._loop.is_running():
|
|
115
|
+
raise RuntimeError("Event loop not running. This should have been caught earlier.")
|
|
116
|
+
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
117
|
+
return future.result(timeout)
|
|
118
|
+
|
|
119
|
+
### MODIFIED: Now operates on a specific server identified by alias
|
|
120
|
+
async def _initialize_connection_async(self, alias: str) -> bool:
|
|
121
|
+
server_info = self.servers[alias]
|
|
122
|
+
if server_info["initialized"]:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
server_url = server_info["url"]
|
|
126
|
+
ASCIIColors.info(f"{self.binding_name}: Initializing connection to '{alias}' ({server_url})...")
|
|
127
|
+
try:
|
|
128
|
+
exit_stack = AsyncExitStack()
|
|
129
|
+
|
|
130
|
+
client_streams = await exit_stack.enter_async_context(
|
|
131
|
+
streamablehttp_client(server_url)
|
|
132
|
+
)
|
|
133
|
+
read_stream, write_stream, _ = client_streams
|
|
134
|
+
|
|
135
|
+
session = await exit_stack.enter_async_context(
|
|
136
|
+
ClientSession(read_stream, write_stream)
|
|
137
|
+
)
|
|
138
|
+
await session.initialize()
|
|
139
|
+
|
|
140
|
+
# Update the state for this specific server
|
|
141
|
+
server_info["session"] = session
|
|
142
|
+
server_info["exit_stack"] = exit_stack
|
|
143
|
+
server_info["initialized"] = True
|
|
144
|
+
|
|
145
|
+
ASCIIColors.green(f"{self.binding_name}: Connected to '{alias}' ({server_url})")
|
|
146
|
+
return True
|
|
147
|
+
except Exception as e:
|
|
148
|
+
trace_exception(e)
|
|
149
|
+
ASCIIColors.error(f"{self.binding_name}: Failed to connect to '{alias}' ({server_url}): {e}")
|
|
150
|
+
if 'exit_stack' in locals() and exit_stack:
|
|
151
|
+
await exit_stack.aclose()
|
|
152
|
+
|
|
153
|
+
# Reset state for this server on failure
|
|
154
|
+
server_info["session"] = None
|
|
155
|
+
server_info["exit_stack"] = None
|
|
156
|
+
server_info["initialized"] = False
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
### MODIFIED: Ensures a specific server is initialized
|
|
160
|
+
def _ensure_initialized_sync(self, alias: str, timeout=30.0):
|
|
161
|
+
self._wait_for_loop()
|
|
162
|
+
|
|
163
|
+
server_info = self.servers.get(alias)
|
|
164
|
+
if not server_info:
|
|
165
|
+
raise ValueError(f"Unknown server alias: '{alias}'")
|
|
166
|
+
|
|
167
|
+
# Use a lock to prevent multiple threads trying to initialize the same server
|
|
168
|
+
with server_info["initializing_lock"]:
|
|
169
|
+
if not server_info["initialized"]:
|
|
170
|
+
success = self._run_async(self._initialize_connection_async(alias), timeout=timeout)
|
|
171
|
+
if not success:
|
|
172
|
+
raise ConnectionError(f"Failed to initialize remote MCP connection to '{alias}' ({server_info['url']})")
|
|
173
|
+
|
|
174
|
+
if not server_info.get("session"):
|
|
175
|
+
raise ConnectionError(f"MCP Session not valid after init attempt for '{alias}' ({server_info['url']})")
|
|
176
|
+
|
|
177
|
+
### MODIFIED: Refreshes tools from ALL connected servers and aggregates them
|
|
178
|
+
async def _refresh_all_tools_cache_async(self):
|
|
179
|
+
ASCIIColors.info(f"{self.binding_name}: Refreshing tools from all servers...")
|
|
180
|
+
all_tools = []
|
|
181
|
+
# Create a list of tasks to run concurrently
|
|
182
|
+
refresh_tasks = [
|
|
183
|
+
self._fetch_tools_from_server_async(alias) for alias in self.servers.keys()
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# Gather results from all tasks
|
|
187
|
+
results = await asyncio.gather(*refresh_tasks, return_exceptions=True)
|
|
188
|
+
|
|
189
|
+
for result in results:
|
|
190
|
+
if isinstance(result, Exception):
|
|
191
|
+
# Error already logged inside the fetch function
|
|
192
|
+
continue
|
|
193
|
+
if result:
|
|
194
|
+
all_tools.extend(result)
|
|
195
|
+
|
|
196
|
+
self._discovered_tools_cache = all_tools
|
|
197
|
+
ASCIIColors.green(f"{self.binding_name}: Tool refresh complete. Found {len(all_tools)} tools across all servers.")
|
|
198
|
+
|
|
199
|
+
### NEW: Helper async function to fetch tools from a single server
|
|
200
|
+
async def _fetch_tools_from_server_async(self, alias: str) -> List[Dict[str, Any]]:
|
|
201
|
+
server_info = self.servers[alias]
|
|
202
|
+
if not server_info["initialized"] or not server_info["session"]:
|
|
203
|
+
ASCIIColors.debug(f"{self.binding_name}: Skipping tool refresh for non-initialized server '{alias}'.")
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
list_tools_result = await server_info["session"].list_tools()
|
|
208
|
+
server_tools = []
|
|
209
|
+
for tool_obj in list_tools_result.tools:
|
|
210
|
+
input_schema_dict = {}
|
|
211
|
+
tool_input_schema = getattr(tool_obj, 'inputSchema', getattr(tool_obj, 'input_schema', None))
|
|
212
|
+
if tool_input_schema:
|
|
213
|
+
if hasattr(tool_input_schema, 'model_dump'):
|
|
214
|
+
input_schema_dict = tool_input_schema.model_dump(mode='json', exclude_none=True)
|
|
215
|
+
elif isinstance(tool_input_schema, dict):
|
|
216
|
+
input_schema_dict = tool_input_schema
|
|
217
|
+
|
|
218
|
+
tool_name_for_client = f"{alias}{TOOL_NAME_SEPARATOR}{tool_obj.name}"
|
|
219
|
+
|
|
220
|
+
server_tools.append({
|
|
221
|
+
"name": tool_name_for_client,
|
|
222
|
+
"description": tool_obj.description or "",
|
|
223
|
+
"input_schema": input_schema_dict
|
|
224
|
+
})
|
|
225
|
+
ASCIIColors.info(f"{self.binding_name}: Found {len(server_tools)} tools on server '{alias}'.")
|
|
226
|
+
return server_tools
|
|
227
|
+
except Exception as e:
|
|
228
|
+
trace_exception(e)
|
|
229
|
+
ASCIIColors.error(f"{self.binding_name}: Error refreshing tools from '{alias}': {e}")
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
### MODIFIED: Discovers tools from all configured servers
|
|
234
|
+
def discover_tools(self, force_refresh: bool = False, timeout_per_server: float = 30.0, **kwargs) -> List[Dict[str, Any]]:
|
|
235
|
+
if not self.servers:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
# Initialize all servers that are not yet initialized.
|
|
239
|
+
for alias in self.servers.keys():
|
|
240
|
+
try:
|
|
241
|
+
# _ensure_initialized_sync is internally locked and idempotent
|
|
242
|
+
self._ensure_initialized_sync(alias, timeout=timeout_per_server)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
# One server failing to connect shouldn't stop discovery on others.
|
|
245
|
+
ASCIIColors.warning(f"{self.binding_name}: Could not ensure connection to '{alias}' for discovery: {e}")
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
if force_refresh or not self._discovered_tools_cache:
|
|
249
|
+
# The timeout for refreshing all tools should be longer
|
|
250
|
+
self._run_async(self._refresh_all_tools_cache_async(), timeout=timeout_per_server * len(self.servers))
|
|
251
|
+
return self._discovered_tools_cache
|
|
252
|
+
except Exception as e:
|
|
253
|
+
trace_exception(e)
|
|
254
|
+
ASCIIColors.error(f"{self.binding_name}: Problem during tool discovery: {e}")
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
### MODIFIED: Now operates on a specific server identified by alias
|
|
258
|
+
async def _execute_tool_async(self, alias: str, actual_tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
259
|
+
server_info = self.servers[alias]
|
|
260
|
+
server_url = server_info["url"]
|
|
261
|
+
|
|
262
|
+
if not server_info["initialized"] or not server_info["session"]:
|
|
263
|
+
return {"error": f"Not connected to server '{alias}' ({server_url})", "status_code": 503}
|
|
264
|
+
|
|
265
|
+
ASCIIColors.info(f"{self.binding_name}: Executing remote tool '{actual_tool_name}' on '{alias}' ({server_url}) with params: {json.dumps(params)}")
|
|
266
|
+
try:
|
|
267
|
+
mcp_call_result = await server_info["session"].call_tool(name=actual_tool_name, arguments=params)
|
|
268
|
+
output_parts = [p.text for p in mcp_call_result.content if isinstance(p, types.TextContent) and p.text is not None] if mcp_call_result.content else []
|
|
269
|
+
if not output_parts: return {"output": {"message": "Tool executed but returned no textual content."}, "status_code": 200}
|
|
270
|
+
|
|
271
|
+
combined_output_str = "\n".join(output_parts)
|
|
272
|
+
try:
|
|
273
|
+
return {"output": json.loads(combined_output_str), "status_code": 200}
|
|
274
|
+
except json.JSONDecodeError:
|
|
275
|
+
return {"output": combined_output_str, "status_code": 200}
|
|
276
|
+
except Exception as e:
|
|
277
|
+
trace_exception(e)
|
|
278
|
+
return {"error": f"Error executing remote tool '{actual_tool_name}' on '{alias}': {str(e)}", "status_code": 500}
|
|
279
|
+
|
|
280
|
+
### MODIFIED: Parses alias from tool name and routes the call
|
|
281
|
+
def execute_tool(self, tool_name_with_alias: str, params: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
|
282
|
+
timeout = float(kwargs.get('timeout', 60.0))
|
|
283
|
+
|
|
284
|
+
if TOOL_NAME_SEPARATOR not in tool_name_with_alias:
|
|
285
|
+
return {"error": f"Invalid tool name format. Expected 'alias{TOOL_NAME_SEPARATOR}tool_name', but got '{tool_name_with_alias}'.", "status_code": 400}
|
|
286
|
+
|
|
287
|
+
alias, actual_tool_name = tool_name_with_alias.split(TOOL_NAME_SEPARATOR, 1)
|
|
288
|
+
|
|
289
|
+
if alias not in self.servers:
|
|
290
|
+
return {"error": f"Tool name '{tool_name_with_alias}' has an unknown server alias '{alias}'.", "status_code": 400}
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# Ensure this specific server is connected before executing
|
|
294
|
+
self._ensure_initialized_sync(alias, timeout=min(timeout, 30.0))
|
|
295
|
+
return self._run_async(self._execute_tool_async(alias, actual_tool_name, params), timeout=timeout)
|
|
296
|
+
except (ConnectionError, RuntimeError) as e:
|
|
297
|
+
return {"error": f"{self.binding_name}: Connection issue for server '{alias}': {e}", "status_code": 503}
|
|
298
|
+
except TimeoutError:
|
|
299
|
+
return {"error": f"{self.binding_name}: Remote tool '{actual_tool_name}' on '{alias}' timed out.", "status_code": 504}
|
|
300
|
+
except Exception as e:
|
|
301
|
+
trace_exception(e)
|
|
302
|
+
return {"error": f"{self.binding_name}: Failed to run remote MCP tool '{actual_tool_name}' on '{alias}': {e}", "status_code": 500}
|
|
303
|
+
|
|
304
|
+
### MODIFIED: Closes all connections
|
|
305
|
+
def close(self):
|
|
306
|
+
ASCIIColors.info(f"{self.binding_name}: Closing all remote connections...")
|
|
307
|
+
|
|
308
|
+
async def _close_all_connections():
|
|
309
|
+
close_tasks = []
|
|
310
|
+
for alias, server_info in self.servers.items():
|
|
311
|
+
if server_info.get("exit_stack"):
|
|
312
|
+
ASCIIColors.info(f"{self.binding_name}: Closing connection to '{alias}'...")
|
|
313
|
+
close_tasks.append(server_info["exit_stack"].aclose())
|
|
314
|
+
|
|
315
|
+
if close_tasks:
|
|
316
|
+
await asyncio.gather(*close_tasks, return_exceptions=True)
|
|
317
|
+
|
|
318
|
+
# Check if loop is running before trying to schedule work on it
|
|
319
|
+
if self._loop and self._loop.is_running():
|
|
320
|
+
try:
|
|
321
|
+
self._run_async(_close_all_connections(), timeout=10.0)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
ASCIIColors.error(f"{self.binding_name}: Error during async close: {e}")
|
|
324
|
+
|
|
325
|
+
# Reset all server states
|
|
326
|
+
for alias in self.servers:
|
|
327
|
+
self.servers[alias].update({
|
|
328
|
+
"exit_stack": None,
|
|
329
|
+
"session": None,
|
|
330
|
+
"initialized": False
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
if self._loop and self._loop.is_running():
|
|
334
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
335
|
+
|
|
336
|
+
if self._thread and self._thread.is_alive():
|
|
337
|
+
self._thread.join(timeout=5.0)
|
|
338
|
+
|
|
339
|
+
ASCIIColors.info(f"{self.binding_name}: Remote connection binding closed.")
|
|
340
|
+
|
|
341
|
+
def get_binding_config(self) -> Dict[str, Any]:
|
|
342
|
+
return self.config
|
|
@@ -9,6 +9,7 @@ examples/gradio_chat_app.py
|
|
|
9
9
|
examples/internet_search_with_rag.py
|
|
10
10
|
examples/local_mcp.py
|
|
11
11
|
examples/openai_mcp.py
|
|
12
|
+
examples/run_remote_mcp_example copy.py
|
|
12
13
|
examples/run_standard_mcp_example.py
|
|
13
14
|
examples/simple_text_gen_test.py
|
|
14
15
|
examples/simple_text_gen_with_image_test.py
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# Conceptual: lollms_client/mcp_bindings/remote_mcp/__init__.py
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from contextlib import AsyncExitStack
|
|
5
|
-
from typing import Optional, List, Dict, Any, Tuple
|
|
6
|
-
from lollms_client.lollms_mcp_binding import LollmsMCPBinding
|
|
7
|
-
from ascii_colors import ASCIIColors, trace_exception
|
|
8
|
-
import threading
|
|
9
|
-
try:
|
|
10
|
-
from mcp import ClientSession, types
|
|
11
|
-
# Import the specific network client from MCP SDK
|
|
12
|
-
from mcp.client.streamable_http import streamablehttp_client
|
|
13
|
-
# If supporting OAuth, you'd import auth components:
|
|
14
|
-
# from mcp.client.auth import OAuthClientProvider, TokenStorage
|
|
15
|
-
# from mcp.shared.auth import OAuthClientMetadata, OAuthToken
|
|
16
|
-
MCP_LIBRARY_AVAILABLE = True
|
|
17
|
-
except ImportError:
|
|
18
|
-
# ... (error handling as in StandardMCPBinding) ...
|
|
19
|
-
MCP_LIBRARY_AVAILABLE = False
|
|
20
|
-
ClientSession = None # etc.
|
|
21
|
-
streamablehttp_client = None
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
BindingName = "RemoteMCPBinding"
|
|
25
|
-
# No TOOL_NAME_SEPARATOR needed if connecting to one remote server per instance,
|
|
26
|
-
# or if server aliases are handled differently (e.g. part of URL or config)
|
|
27
|
-
TOOL_NAME_SEPARATOR = "::"
|
|
28
|
-
|
|
29
|
-
class RemoteMCPBinding(LollmsMCPBinding):
|
|
30
|
-
def __init__(self,
|
|
31
|
-
server_url: str, # e.g., "http://localhost:8000/mcp"
|
|
32
|
-
alias: str = "remote_server", # An alias for this connection
|
|
33
|
-
auth_config: Optional[Dict[str, Any]] = None, # For API keys, OAuth, etc.
|
|
34
|
-
**other_config_params: Any):
|
|
35
|
-
super().__init__(binding_name="remote_mcp")
|
|
36
|
-
|
|
37
|
-
if not MCP_LIBRARY_AVAILABLE:
|
|
38
|
-
ASCIIColors.error(f"{self.binding_name}: MCP library not available.")
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
if not server_url:
|
|
42
|
-
ASCIIColors.error(f"{self.binding_name}: server_url is required.")
|
|
43
|
-
# Or raise ValueError
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
self.server_url = server_url
|
|
47
|
-
self.alias = alias # Could be used to prefix tool names if managing multiple remotes
|
|
48
|
-
self.auth_config = auth_config or {}
|
|
49
|
-
self.config = {
|
|
50
|
-
"server_url": server_url,
|
|
51
|
-
"alias": alias,
|
|
52
|
-
"auth_config": self.auth_config
|
|
53
|
-
}
|
|
54
|
-
self.config.update(other_config_params)
|
|
55
|
-
|
|
56
|
-
self._mcp_session: Optional[ClientSession] = None
|
|
57
|
-
self._exit_stack: Optional[AsyncExitStack] = None
|
|
58
|
-
self._discovered_tools_cache: List[Dict[str, Any]] = []
|
|
59
|
-
self._is_initialized = False
|
|
60
|
-
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
61
|
-
self._thread: Optional[threading.Thread] = None
|
|
62
|
-
|
|
63
|
-
self._start_event_loop_thread() # Similar to StandardMCPBinding
|
|
64
|
-
|
|
65
|
-
def _start_event_loop_thread(self): # Simplified from StandardMCPBinding
|
|
66
|
-
if self._loop and self._loop.is_running(): return
|
|
67
|
-
self._loop = asyncio.new_event_loop()
|
|
68
|
-
self._thread = threading.Thread(target=self._run_loop_forever, daemon=True)
|
|
69
|
-
self._thread.start()
|
|
70
|
-
|
|
71
|
-
def _run_loop_forever(self):
|
|
72
|
-
if not self._loop: return
|
|
73
|
-
asyncio.set_event_loop(self._loop)
|
|
74
|
-
try: self._loop.run_forever()
|
|
75
|
-
finally:
|
|
76
|
-
# ... (loop cleanup as in StandardMCPBinding) ...
|
|
77
|
-
if not self._loop.is_closed(): self._loop.close()
|
|
78
|
-
|
|
79
|
-
def _run_async(self, coro, timeout=None): # Simplified
|
|
80
|
-
if not self._loop or not self._loop.is_running(): raise RuntimeError("Event loop not running.")
|
|
81
|
-
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
82
|
-
return future.result(timeout)
|
|
83
|
-
|
|
84
|
-
async def _initialize_connection_async(self) -> bool:
|
|
85
|
-
if self._is_initialized: return True
|
|
86
|
-
ASCIIColors.info(f"{self.binding_name}: Initializing connection to {self.server_url}...")
|
|
87
|
-
try:
|
|
88
|
-
self._exit_stack = AsyncExitStack()
|
|
89
|
-
|
|
90
|
-
# --- Authentication Setup (Conceptual) ---
|
|
91
|
-
# oauth_provider = None
|
|
92
|
-
# if self.auth_config.get("type") == "oauth":
|
|
93
|
-
# # oauth_provider = OAuthClientProvider(...) # Setup based on auth_config
|
|
94
|
-
# pass
|
|
95
|
-
# http_headers = {}
|
|
96
|
-
# if self.auth_config.get("type") == "api_key":
|
|
97
|
-
# key = self.auth_config.get("key")
|
|
98
|
-
# header_name = self.auth_config.get("header_name", "X-API-Key")
|
|
99
|
-
# if key: http_headers[header_name] = key
|
|
100
|
-
|
|
101
|
-
# Use streamablehttp_client from MCP SDK
|
|
102
|
-
# The `auth` parameter of streamablehttp_client takes an OAuthClientProvider
|
|
103
|
-
# For simple API key headers, you might need to use `httpx` directly
|
|
104
|
-
# or see if streamablehttp_client allows passing custom headers.
|
|
105
|
-
# The MCP client example for streamable HTTP doesn't show custom headers directly,
|
|
106
|
-
# it focuses on OAuth.
|
|
107
|
-
# If `streamablehttp_client` takes `**kwargs` that are passed to `httpx.AsyncClient`,
|
|
108
|
-
# then `headers=http_headers` might work.
|
|
109
|
-
|
|
110
|
-
# Assuming streamablehttp_client can take headers if needed, or auth provider
|
|
111
|
-
# For now, let's assume no auth for simplicity or that it's handled by underlying httpx if passed via kwargs
|
|
112
|
-
client_streams = await self._exit_stack.enter_async_context(
|
|
113
|
-
streamablehttp_client(self.server_url) # Add auth=oauth_provider or headers=http_headers if supported
|
|
114
|
-
)
|
|
115
|
-
read_stream, write_stream, _http_client_instance = client_streams # http_client_instance might be useful
|
|
116
|
-
|
|
117
|
-
self._mcp_session = await self._exit_stack.enter_async_context(
|
|
118
|
-
ClientSession(read_stream, write_stream)
|
|
119
|
-
)
|
|
120
|
-
await self._mcp_session.initialize()
|
|
121
|
-
self._is_initialized = True
|
|
122
|
-
ASCIIColors.green(f"{self.binding_name}: Connected to {self.server_url}")
|
|
123
|
-
await self._refresh_tools_cache_async()
|
|
124
|
-
return True
|
|
125
|
-
except Exception as e:
|
|
126
|
-
trace_exception(e)
|
|
127
|
-
ASCIIColors.error(f"{self.binding_name}: Failed to connect to {self.server_url}: {e}")
|
|
128
|
-
if self._exit_stack: await self._exit_stack.aclose() # Cleanup on failure
|
|
129
|
-
self._exit_stack = None
|
|
130
|
-
self._mcp_session = None
|
|
131
|
-
self._is_initialized = False
|
|
132
|
-
return False
|
|
133
|
-
|
|
134
|
-
def _ensure_initialized_sync(self, timeout=30.0):
|
|
135
|
-
if not self._is_initialized:
|
|
136
|
-
success = self._run_async(self._initialize_connection_async(), timeout=timeout)
|
|
137
|
-
if not success: raise ConnectionError(f"Failed to initialize remote MCP connection to {self.server_url}")
|
|
138
|
-
if not self._mcp_session: # Double check
|
|
139
|
-
raise ConnectionError(f"MCP Session not valid after init attempt for {self.server_url}")
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
async def _refresh_tools_cache_async(self):
|
|
143
|
-
if not self._is_initialized or not self._mcp_session: return
|
|
144
|
-
ASCIIColors.info(f"{self.binding_name}: Refreshing tools from {self.server_url}...")
|
|
145
|
-
try:
|
|
146
|
-
list_tools_result = await self._mcp_session.list_tools()
|
|
147
|
-
current_tools = []
|
|
148
|
-
# ... (tool parsing logic similar to StandardMCPBinding, but no server alias prefix needed if one server per binding instance)
|
|
149
|
-
for tool_obj in list_tools_result.tools:
|
|
150
|
-
# ...
|
|
151
|
-
input_schema_dict = {}
|
|
152
|
-
tool_input_schema = getattr(tool_obj, 'inputSchema', getattr(tool_obj, 'input_schema', None))
|
|
153
|
-
if tool_input_schema:
|
|
154
|
-
if hasattr(tool_input_schema, 'model_dump'):
|
|
155
|
-
input_schema_dict = tool_input_schema.model_dump(mode='json', exclude_none=True)
|
|
156
|
-
elif isinstance(tool_input_schema, dict):
|
|
157
|
-
input_schema_dict = tool_input_schema
|
|
158
|
-
|
|
159
|
-
tool_name_for_client = f"{self.alias}{TOOL_NAME_SEPARATOR}{tool_obj.name}" if TOOL_NAME_SEPARATOR else tool_obj.name
|
|
160
|
-
|
|
161
|
-
current_tools.append({
|
|
162
|
-
"name": tool_name_for_client, # Use self.alias to prefix
|
|
163
|
-
"description": tool_obj.description or "",
|
|
164
|
-
"input_schema": input_schema_dict
|
|
165
|
-
})
|
|
166
|
-
self._discovered_tools_cache = current_tools
|
|
167
|
-
ASCIIColors.green(f"{self.binding_name}: Tools refreshed for {self.server_url}. Found {len(current_tools)} tools.")
|
|
168
|
-
except Exception as e:
|
|
169
|
-
trace_exception(e)
|
|
170
|
-
ASCIIColors.error(f"{self.binding_name}: Error refreshing tools from {self.server_url}: {e}")
|
|
171
|
-
|
|
172
|
-
def discover_tools(self, force_refresh: bool = False, timeout_per_server: float = 10.0, **kwargs) -> List[Dict[str, Any]]:
|
|
173
|
-
# This binding instance connects to ONE server, so timeout_per_server is just 'timeout'
|
|
174
|
-
try:
|
|
175
|
-
self._ensure_initialized_sync(timeout=timeout_per_server)
|
|
176
|
-
if force_refresh or not self._discovered_tools_cache:
|
|
177
|
-
self._run_async(self._refresh_tools_cache_async(), timeout=timeout_per_server)
|
|
178
|
-
return self._discovered_tools_cache
|
|
179
|
-
except Exception as e:
|
|
180
|
-
ASCIIColors.error(f"{self.binding_name}: Problem during tool discovery for {self.server_url}: {e}")
|
|
181
|
-
return []
|
|
182
|
-
|
|
183
|
-
async def _execute_tool_async(self, actual_tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
184
|
-
if not self._is_initialized or not self._mcp_session:
|
|
185
|
-
return {"error": f"Not connected to {self.server_url}", "status_code": 503}
|
|
186
|
-
|
|
187
|
-
ASCIIColors.info(f"{self.binding_name}: Executing remote tool '{actual_tool_name}' on {self.server_url} with params: {json.dumps(params)}")
|
|
188
|
-
try:
|
|
189
|
-
mcp_call_result = await self._mcp_session.call_tool(name=actual_tool_name, arguments=params)
|
|
190
|
-
# ... (result parsing as in StandardMCPBinding) ...
|
|
191
|
-
output_parts = [p.text for p in mcp_call_result.content if isinstance(p, types.TextContent) and p.text is not None] if mcp_call_result.content else []
|
|
192
|
-
if not output_parts: return {"output": {"message": "Tool executed but returned no textual content."}, "status_code": 200}
|
|
193
|
-
combined_output_str = "\n".join(output_parts)
|
|
194
|
-
try: return {"output": json.loads(combined_output_str), "status_code": 200}
|
|
195
|
-
except json.JSONDecodeError: return {"output": combined_output_str, "status_code": 200}
|
|
196
|
-
except Exception as e:
|
|
197
|
-
trace_exception(e)
|
|
198
|
-
return {"error": f"Error executing remote tool '{actual_tool_name}': {str(e)}", "status_code": 500}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def execute_tool(self, tool_name_with_alias: str, params: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
|
202
|
-
timeout = float(kwargs.get('timeout', 60.0))
|
|
203
|
-
|
|
204
|
-
# If using alias prefixing (self.alias + TOOL_NAME_SEPARATOR + actual_name)
|
|
205
|
-
expected_prefix = f"{self.alias}{TOOL_NAME_SEPARATOR}"
|
|
206
|
-
if TOOL_NAME_SEPARATOR and tool_name_with_alias.startswith(expected_prefix):
|
|
207
|
-
actual_tool_name = tool_name_with_alias[len(expected_prefix):]
|
|
208
|
-
elif not TOOL_NAME_SEPARATOR and tool_name_with_alias: # No prefixing, tool_name is actual_tool_name
|
|
209
|
-
actual_tool_name = tool_name_with_alias
|
|
210
|
-
else:
|
|
211
|
-
return {"error": f"Tool name '{tool_name_with_alias}' does not match expected alias '{self.alias}'.", "status_code": 400}
|
|
212
|
-
|
|
213
|
-
try:
|
|
214
|
-
self._ensure_initialized_sync(timeout=min(timeout, 30.0))
|
|
215
|
-
return self._run_async(self._execute_tool_async(actual_tool_name, params), timeout=timeout)
|
|
216
|
-
# ... (error handling as in StandardMCPBinding) ...
|
|
217
|
-
except ConnectionError as e: return {"error": f"{self.binding_name}: Connection issue for '{self.server_url}': {e}", "status_code": 503}
|
|
218
|
-
except TimeoutError: return {"error": f"{self.binding_name}: Remote tool '{actual_tool_name}' on '{self.server_url}' timed out.", "status_code": 504}
|
|
219
|
-
except Exception as e:
|
|
220
|
-
trace_exception(e)
|
|
221
|
-
return {"error": f"{self.binding_name}: Failed to run remote MCP tool '{actual_tool_name}': {e}", "status_code": 500}
|
|
222
|
-
|
|
223
|
-
def close(self):
|
|
224
|
-
ASCIIColors.info(f"{self.binding_name}: Closing connection to {self.server_url}...")
|
|
225
|
-
if self._exit_stack:
|
|
226
|
-
try:
|
|
227
|
-
# The anyio task error might also occur here if not careful
|
|
228
|
-
self._run_async(self._exit_stack.aclose(), timeout=10.0)
|
|
229
|
-
except Exception as e:
|
|
230
|
-
ASCIIColors.error(f"{self.binding_name}: Error during async close for {self.server_url}: {e}")
|
|
231
|
-
self._exit_stack = None
|
|
232
|
-
self._mcp_session = None
|
|
233
|
-
self._is_initialized = False
|
|
234
|
-
|
|
235
|
-
# Stop event loop thread
|
|
236
|
-
if self._loop and self._loop.is_running(): self._loop.call_soon_threadsafe(self._loop.stop)
|
|
237
|
-
if self._thread and self._thread.is_alive(): self._thread.join(timeout=5.0)
|
|
238
|
-
ASCIIColors.info(f"{self.binding_name}: Remote connection binding closed.")
|
|
239
|
-
|
|
240
|
-
def get_binding_config(self) -> Dict[str, Any]: # LollmsMCPBinding might expect this
|
|
241
|
-
return self.config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/deep_analyze/deep_analyze_multiple_files.py
RENAMED
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/function_calling_with_local_custom_mcp.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_a_benchmark_for_safe_store.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_and_speak/generate_and_speak.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_game_sfx/generate_game_fx.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/generate_text_with_multihop_rag_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/examples/personality_test/chat_with_aristotle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/llamacpp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/openllm/__init__.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/pythonllamacpp/__init__.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/tensor_rt/__init__.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/llm_bindings/transformers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/local_mcp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/mcp_bindings/standard_mcp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/stt_bindings/whisper/__init__.py
RENAMED
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/stt_bindings/whispercpp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tti_bindings/diffusers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/ttm_bindings/audiocraft/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lollms_client-0.20.4 → lollms_client-0.20.7}/lollms_client/tts_bindings/piper_tts/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|