lollms-client 0.20.3__py3-none-any.whl → 0.20.6__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/gradio_chat_app.py +228 -0
- examples/internet_search_with_rag.py +1 -2
- examples/run_remote_mcp_example copy.py +226 -0
- lollms_client/__init__.py +2 -2
- lollms_client/llm_bindings/llamacpp/__init__.py +104 -0
- lollms_client/llm_bindings/lollms/__init__.py +102 -1
- lollms_client/llm_bindings/ollama/__init__.py +99 -0
- lollms_client/llm_bindings/openai/__init__.py +109 -0
- lollms_client/lollms_core.py +60 -0
- lollms_client/lollms_discussion.py +478 -33
- lollms_client/lollms_llm_binding.py +43 -0
- lollms_client/mcp_bindings/remote_mcp/__init__.py +233 -132
- {lollms_client-0.20.3.dist-info → lollms_client-0.20.6.dist-info}/METADATA +1 -1
- {lollms_client-0.20.3.dist-info → lollms_client-0.20.6.dist-info}/RECORD +17 -15
- {lollms_client-0.20.3.dist-info → lollms_client-0.20.6.dist-info}/WHEEL +0 -0
- {lollms_client-0.20.3.dist-info → lollms_client-0.20.6.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.20.3.dist-info → lollms_client-0.20.6.dist-info}/top_level.txt +0 -0
|
@@ -1,68 +1,95 @@
|
|
|
1
|
-
# Conceptual: lollms_client/mcp_bindings/remote_mcp/__init__.py
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
4
2
|
from contextlib import AsyncExitStack
|
|
5
3
|
from typing import Optional, List, Dict, Any, Tuple
|
|
6
4
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding
|
|
7
5
|
from ascii_colors import ASCIIColors, trace_exception
|
|
8
6
|
import threading
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
9
|
try:
|
|
10
10
|
from mcp import ClientSession, types
|
|
11
|
-
# Import the specific network client from MCP SDK
|
|
12
11
|
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
12
|
MCP_LIBRARY_AVAILABLE = True
|
|
17
13
|
except ImportError:
|
|
18
|
-
# ... (error handling as in StandardMCPBinding) ...
|
|
19
14
|
MCP_LIBRARY_AVAILABLE = False
|
|
20
|
-
ClientSession = None
|
|
15
|
+
ClientSession = None
|
|
21
16
|
streamablehttp_client = None
|
|
22
17
|
|
|
23
18
|
|
|
24
19
|
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
20
|
TOOL_NAME_SEPARATOR = "::"
|
|
28
21
|
|
|
29
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
|
+
"""
|
|
30
27
|
def __init__(self,
|
|
31
|
-
|
|
32
|
-
alias: str = "remote_server", # An alias for this connection
|
|
33
|
-
auth_config: Optional[Dict[str, Any]] = None, # For API keys, OAuth, etc.
|
|
28
|
+
servers_infos: Dict[str, Dict[str, Any]],
|
|
34
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
|
+
"""
|
|
35
44
|
super().__init__(binding_name="remote_mcp")
|
|
36
45
|
|
|
37
46
|
if not MCP_LIBRARY_AVAILABLE:
|
|
38
|
-
ASCIIColors.error(f"{self.binding_name}: MCP library not available.")
|
|
47
|
+
ASCIIColors.error(f"{self.binding_name}: MCP library not available. This binding will be disabled.")
|
|
39
48
|
return
|
|
40
49
|
|
|
41
|
-
if not
|
|
42
|
-
ASCIIColors.error(f"{self.binding_name}:
|
|
43
|
-
# Or raise ValueError
|
|
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.")
|
|
44
52
|
return
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
self.alias = alias # Could be used to prefix tool names if managing multiple remotes
|
|
48
|
-
self.auth_config = auth_config or {}
|
|
54
|
+
### NEW: Store the overall configuration
|
|
49
55
|
self.config = {
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
"auth_config": self.auth_config
|
|
56
|
+
"servers_infos": servers_infos,
|
|
57
|
+
**other_config_params
|
|
53
58
|
}
|
|
54
|
-
self.config.update(other_config_params)
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
|
|
58
77
|
self._discovered_tools_cache: List[Dict[str, Any]] = []
|
|
59
|
-
|
|
78
|
+
|
|
79
|
+
### MODIFIED: These are now shared across all connections
|
|
60
80
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
61
81
|
self._thread: Optional[threading.Thread] = None
|
|
82
|
+
self._loop_started_event = threading.Event()
|
|
62
83
|
|
|
63
|
-
self.
|
|
84
|
+
if self.servers:
|
|
85
|
+
self._start_event_loop_thread()
|
|
86
|
+
else:
|
|
87
|
+
ASCIIColors.warning(f"{self.binding_name}: No valid servers configured.")
|
|
64
88
|
|
|
65
|
-
|
|
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):
|
|
66
93
|
if self._loop and self._loop.is_running(): return
|
|
67
94
|
self._loop = asyncio.new_event_loop()
|
|
68
95
|
self._thread = threading.Thread(target=self._run_loop_forever, daemon=True)
|
|
@@ -71,83 +98,115 @@ class RemoteMCPBinding(LollmsMCPBinding):
|
|
|
71
98
|
def _run_loop_forever(self):
|
|
72
99
|
if not self._loop: return
|
|
73
100
|
asyncio.set_event_loop(self._loop)
|
|
74
|
-
try:
|
|
101
|
+
try:
|
|
102
|
+
self._loop_started_event.set()
|
|
103
|
+
self._loop.run_forever()
|
|
75
104
|
finally:
|
|
76
|
-
# ... (loop cleanup as in StandardMCPBinding) ...
|
|
77
105
|
if not self._loop.is_closed(): self._loop.close()
|
|
78
106
|
|
|
79
|
-
def
|
|
80
|
-
if not self.
|
|
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.")
|
|
81
116
|
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
82
117
|
return future.result(timeout)
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
100
124
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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()
|
|
109
129
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
130
|
+
client_streams = await exit_stack.enter_async_context(
|
|
131
|
+
streamablehttp_client(server_url)
|
|
114
132
|
)
|
|
115
|
-
read_stream, write_stream,
|
|
133
|
+
read_stream, write_stream, _ = client_streams
|
|
116
134
|
|
|
117
|
-
|
|
135
|
+
session = await exit_stack.enter_async_context(
|
|
118
136
|
ClientSession(read_stream, write_stream)
|
|
119
137
|
)
|
|
120
|
-
await
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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})")
|
|
124
146
|
return True
|
|
125
147
|
except Exception as e:
|
|
126
148
|
trace_exception(e)
|
|
127
|
-
ASCIIColors.error(f"{self.binding_name}: Failed to connect to {
|
|
128
|
-
if
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
157
|
return False
|
|
133
158
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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}'")
|
|
140
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']})")
|
|
141
176
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
ASCIIColors.info(f"{self.binding_name}: Refreshing tools from
|
|
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
|
+
|
|
145
206
|
try:
|
|
146
|
-
list_tools_result = await
|
|
147
|
-
|
|
148
|
-
# ... (tool parsing logic similar to StandardMCPBinding, but no server alias prefix needed if one server per binding instance)
|
|
207
|
+
list_tools_result = await server_info["session"].list_tools()
|
|
208
|
+
server_tools = []
|
|
149
209
|
for tool_obj in list_tools_result.tools:
|
|
150
|
-
# ...
|
|
151
210
|
input_schema_dict = {}
|
|
152
211
|
tool_input_schema = getattr(tool_obj, 'inputSchema', getattr(tool_obj, 'input_schema', None))
|
|
153
212
|
if tool_input_schema:
|
|
@@ -156,86 +215,128 @@ class RemoteMCPBinding(LollmsMCPBinding):
|
|
|
156
215
|
elif isinstance(tool_input_schema, dict):
|
|
157
216
|
input_schema_dict = tool_input_schema
|
|
158
217
|
|
|
159
|
-
tool_name_for_client = f"{
|
|
218
|
+
tool_name_for_client = f"{alias}{TOOL_NAME_SEPARATOR}{tool_obj.name}"
|
|
160
219
|
|
|
161
|
-
|
|
162
|
-
"name": tool_name_for_client,
|
|
220
|
+
server_tools.append({
|
|
221
|
+
"name": tool_name_for_client,
|
|
163
222
|
"description": tool_obj.description or "",
|
|
164
223
|
"input_schema": input_schema_dict
|
|
165
224
|
})
|
|
166
|
-
self.
|
|
167
|
-
|
|
225
|
+
ASCIIColors.info(f"{self.binding_name}: Found {len(server_tools)} tools on server '{alias}'.")
|
|
226
|
+
return server_tools
|
|
168
227
|
except Exception as e:
|
|
169
228
|
trace_exception(e)
|
|
170
|
-
ASCIIColors.error(f"{self.binding_name}: Error refreshing tools from {
|
|
229
|
+
ASCIIColors.error(f"{self.binding_name}: Error refreshing tools from '{alias}': {e}")
|
|
230
|
+
return []
|
|
231
|
+
|
|
171
232
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
|
|
174
247
|
try:
|
|
175
|
-
self._ensure_initialized_sync(timeout=timeout_per_server)
|
|
176
248
|
if force_refresh or not self._discovered_tools_cache:
|
|
177
|
-
|
|
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))
|
|
178
251
|
return self._discovered_tools_cache
|
|
179
252
|
except Exception as e:
|
|
180
|
-
|
|
253
|
+
trace_exception(e)
|
|
254
|
+
ASCIIColors.error(f"{self.binding_name}: Problem during tool discovery: {e}")
|
|
181
255
|
return []
|
|
182
256
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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"]
|
|
186
261
|
|
|
187
|
-
|
|
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)}")
|
|
188
266
|
try:
|
|
189
|
-
mcp_call_result = await
|
|
190
|
-
# ... (result parsing as in StandardMCPBinding) ...
|
|
267
|
+
mcp_call_result = await server_info["session"].call_tool(name=actual_tool_name, arguments=params)
|
|
191
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 []
|
|
192
269
|
if not output_parts: return {"output": {"message": "Tool executed but returned no textual content."}, "status_code": 200}
|
|
270
|
+
|
|
193
271
|
combined_output_str = "\n".join(output_parts)
|
|
194
|
-
try:
|
|
195
|
-
|
|
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}
|
|
196
276
|
except Exception as e:
|
|
197
277
|
trace_exception(e)
|
|
198
|
-
return {"error": f"Error executing remote tool '{actual_tool_name}': {str(e)}", "status_code": 500}
|
|
199
|
-
|
|
278
|
+
return {"error": f"Error executing remote tool '{actual_tool_name}' on '{alias}': {str(e)}", "status_code": 500}
|
|
200
279
|
|
|
280
|
+
### MODIFIED: Parses alias from tool name and routes the call
|
|
201
281
|
def execute_tool(self, tool_name_with_alias: str, params: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
|
202
282
|
timeout = float(kwargs.get('timeout', 60.0))
|
|
203
283
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return {"error": f"Tool name '{tool_name_with_alias}' does not match expected alias '{self.alias}'.", "status_code": 400}
|
|
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}
|
|
212
291
|
|
|
213
292
|
try:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
except ConnectionError as e:
|
|
218
|
-
|
|
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}
|
|
219
300
|
except Exception as e:
|
|
220
301
|
trace_exception(e)
|
|
221
|
-
return {"error": f"{self.binding_name}: Failed to run remote MCP tool '{actual_tool_name}': {e}", "status_code": 500}
|
|
302
|
+
return {"error": f"{self.binding_name}: Failed to run remote MCP tool '{actual_tool_name}' on '{alias}': {e}", "status_code": 500}
|
|
222
303
|
|
|
304
|
+
### MODIFIED: Closes all connections
|
|
223
305
|
def close(self):
|
|
224
|
-
ASCIIColors.info(f"{self.binding_name}: Closing
|
|
225
|
-
|
|
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():
|
|
226
320
|
try:
|
|
227
|
-
|
|
228
|
-
self._run_async(self._exit_stack.aclose(), timeout=10.0)
|
|
321
|
+
self._run_async(_close_all_connections(), timeout=10.0)
|
|
229
322
|
except Exception as e:
|
|
230
|
-
ASCIIColors.error(f"{self.binding_name}: Error during async close
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
self.
|
|
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
|
+
})
|
|
234
332
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
|
|
238
339
|
ASCIIColors.info(f"{self.binding_name}: Remote connection binding closed.")
|
|
239
340
|
|
|
240
|
-
def get_binding_config(self) -> Dict[str, Any]:
|
|
341
|
+
def get_binding_config(self) -> Dict[str, Any]:
|
|
241
342
|
return self.config
|
|
@@ -2,9 +2,11 @@ examples/external_mcp.py,sha256=swx1KCOz6jk8jGTAycq-xu7GXPAhRMDe1x--SKocugE,1337
|
|
|
2
2
|
examples/function_calling_with_local_custom_mcp.py,sha256=g6wOFRB8-p9Cv7hKmQaGzPvtMX3H77gas01QVNEOduM,12407
|
|
3
3
|
examples/generate_a_benchmark_for_safe_store.py,sha256=bkSt0mrpNsN0krZAUShm0jgVM1ukrPpjI7VwSgcNdSA,3974
|
|
4
4
|
examples/generate_text_with_multihop_rag_example.py,sha256=riEyVYo97r6ZYdySL-NJkRhE4MnpwbZku1sN8RNvbvs,11519
|
|
5
|
-
examples/
|
|
5
|
+
examples/gradio_chat_app.py,sha256=ZZ_D1U0wvvwE9THmAPXUvNKkFG2gi7tQq1f2pQx_2ug,15315
|
|
6
|
+
examples/internet_search_with_rag.py,sha256=ioTb_WI2M6kFeh1Dg-EGcKjccphnCsIGD_e9PZgZshw,12314
|
|
6
7
|
examples/local_mcp.py,sha256=w40dgayvHYe01yvekEE0LjcbkpwKjWwJ-9v4_wGYsUk,9113
|
|
7
8
|
examples/openai_mcp.py,sha256=7IEnPGPXZgYZyiES_VaUbQ6viQjenpcUxGiHE-pGeFY,11060
|
|
9
|
+
examples/run_remote_mcp_example copy.py,sha256=pGT8A5iXK9oHtjGNEUCm8fnj9DQ37gcznjLYqAEI20o,10075
|
|
8
10
|
examples/run_standard_mcp_example.py,sha256=GSZpaACPf3mDPsjA8esBQVUsIi7owI39ca5avsmvCxA,9419
|
|
9
11
|
examples/simple_text_gen_test.py,sha256=RoX9ZKJjGMujeep60wh5WT_GoBn0O9YKJY6WOy-ZmOc,8710
|
|
10
12
|
examples/simple_text_gen_with_image_test.py,sha256=rR1O5Prcb52UHtJ3c6bv7VuTd1cvbkr5aNZU-v-Rs3Y,9263
|
|
@@ -23,12 +25,12 @@ examples/personality_test/chat_test.py,sha256=o2jlpoddFc-T592iqAiA29xk3x27KsdK5D
|
|
|
23
25
|
examples/personality_test/chat_with_aristotle.py,sha256=4X_fwubMpd0Eq2rCReS2bgVlUoAqJprjkLXk2Jz6pXU,1774
|
|
24
26
|
examples/personality_test/tesks_test.py,sha256=7LIiwrEbva9WWZOLi34fsmCBN__RZbPpxoUOKA_AtYk,1924
|
|
25
27
|
examples/test_local_models/local_chat.py,sha256=slakja2zaHOEAUsn2tn_VmI4kLx6luLBrPqAeaNsix8,456
|
|
26
|
-
lollms_client/__init__.py,sha256=
|
|
28
|
+
lollms_client/__init__.py,sha256=Q12uBDAS5nze6N8vwIoOrzdr5UQC1wogoKmgO31q2uk,912
|
|
27
29
|
lollms_client/lollms_config.py,sha256=goEseDwDxYJf3WkYJ4IrLXwg3Tfw73CXV2Avg45M_hE,21876
|
|
28
|
-
lollms_client/lollms_core.py,sha256=
|
|
29
|
-
lollms_client/lollms_discussion.py,sha256=
|
|
30
|
+
lollms_client/lollms_core.py,sha256=Jr9VQCvyxtsdy3VstNjOOoMCx4uS50VHSzaFuyu754o,118714
|
|
31
|
+
lollms_client/lollms_discussion.py,sha256=9QcmDIlzozgBWWuZK2EF0VlK6l3JYCxDfeXaf_KxkBA,20974
|
|
30
32
|
lollms_client/lollms_js_analyzer.py,sha256=01zUvuO2F_lnUe_0NLxe1MF5aHE1hO8RZi48mNPv-aw,8361
|
|
31
|
-
lollms_client/lollms_llm_binding.py,sha256=
|
|
33
|
+
lollms_client/lollms_llm_binding.py,sha256=E81g4yBlQn76WTSLicnTETJuQhf_WZUMZaxotgRnOcA,12096
|
|
32
34
|
lollms_client/lollms_mcp_binding.py,sha256=0rK9HQCBEGryNc8ApBmtOlhKE1Yfn7X7xIQssXxS2Zc,8933
|
|
33
35
|
lollms_client/lollms_python_analyzer.py,sha256=7gf1fdYgXCOkPUkBAPNmr6S-66hMH4_KonOMsADASxc,10246
|
|
34
36
|
lollms_client/lollms_stt_binding.py,sha256=jAUhLouEhh2hmm1bK76ianfw_6B59EHfY3FmLv6DU-g,5111
|
|
@@ -39,10 +41,10 @@ lollms_client/lollms_ttv_binding.py,sha256=KkTaHLBhEEdt4sSVBlbwr5i_g_TlhcrwrT-7D
|
|
|
39
41
|
lollms_client/lollms_types.py,sha256=CLiodudFgTbuXTGgupDt6IgMvJkrfiOHdw1clx_5UjA,2863
|
|
40
42
|
lollms_client/lollms_utilities.py,sha256=WiG-HHMdo86j3LBndcBQ-PbMqQ8kGKLp1e9WuLDzRVU,7048
|
|
41
43
|
lollms_client/llm_bindings/__init__.py,sha256=9sWGpmWSSj6KQ8H4lKGCjpLYwhnVdL_2N7gXCphPqh4,14
|
|
42
|
-
lollms_client/llm_bindings/llamacpp/__init__.py,sha256=
|
|
43
|
-
lollms_client/llm_bindings/lollms/__init__.py,sha256=
|
|
44
|
-
lollms_client/llm_bindings/ollama/__init__.py,sha256=
|
|
45
|
-
lollms_client/llm_bindings/openai/__init__.py,sha256=
|
|
44
|
+
lollms_client/llm_bindings/llamacpp/__init__.py,sha256=Qj5RvsgPeHGNfb5AEwZSzFwAp4BOWjyxmm9qBNtstrc,63716
|
|
45
|
+
lollms_client/llm_bindings/lollms/__init__.py,sha256=17TwGMDJMxRPjZjZZSysR8AwjMXZeRfDBy8RqWWuaIY,17769
|
|
46
|
+
lollms_client/llm_bindings/ollama/__init__.py,sha256=fno1UEcXIy37Z3-bZAwOMuCfbAYOzztcO4oBfLqK_JA,32538
|
|
47
|
+
lollms_client/llm_bindings/openai/__init__.py,sha256=ay_2JJi4La258Eg3alUhnh6Y5IRyOWnHaFLXqvN_4ao,19144
|
|
46
48
|
lollms_client/llm_bindings/openllm/__init__.py,sha256=xv2XDhJNCYe6NPnWBboDs24AQ1VJBOzsTuMcmuQ6xYY,29864
|
|
47
49
|
lollms_client/llm_bindings/pythonllamacpp/__init__.py,sha256=7dM42TCGKh0eV0njNL1tc9cInhyvBRIXzN3dcy12Gl0,33551
|
|
48
50
|
lollms_client/llm_bindings/tensor_rt/__init__.py,sha256=nPaNhGRd-bsG0UlYwcEqjd_UagCMEf5VEbBUW-GWu6A,32203
|
|
@@ -53,7 +55,7 @@ lollms_client/mcp_bindings/local_mcp/default_tools/file_writer/file_writer.py,sh
|
|
|
53
55
|
lollms_client/mcp_bindings/local_mcp/default_tools/generate_image_from_prompt/generate_image_from_prompt.py,sha256=THtZsMxNnXZiBdkwoBlfbWY2C5hhDdmPtnM-8cSKN6s,9488
|
|
54
56
|
lollms_client/mcp_bindings/local_mcp/default_tools/internet_search/internet_search.py,sha256=PLC31-D04QKTOTb1uuCHnrAlpysQjsk89yIJngK0VGc,4586
|
|
55
57
|
lollms_client/mcp_bindings/local_mcp/default_tools/python_interpreter/python_interpreter.py,sha256=McDCBVoVrMDYgU7EYtyOY7mCk1uEeTea0PSD69QqDsQ,6228
|
|
56
|
-
lollms_client/mcp_bindings/remote_mcp/__init__.py,sha256=
|
|
58
|
+
lollms_client/mcp_bindings/remote_mcp/__init__.py,sha256=2oSVcOvW6XkAIfZvCNmwc1dSwrZ4hTZF8mntauM7YnU,16482
|
|
57
59
|
lollms_client/mcp_bindings/standard_mcp/__init__.py,sha256=zpF4h8cTUxoERI-xcVjmS_V772LK0V4jegjz2k1PK98,31658
|
|
58
60
|
lollms_client/stt_bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
61
|
lollms_client/stt_bindings/lollms/__init__.py,sha256=jBz3285atdPRqQe9ZRrb-AvjqKRB4f8tjLXjma0DLfE,6082
|
|
@@ -75,8 +77,8 @@ lollms_client/tts_bindings/piper_tts/__init__.py,sha256=0IEWG4zH3_sOkSb9WbZzkeV5
|
|
|
75
77
|
lollms_client/tts_bindings/xtts/__init__.py,sha256=FgcdUH06X6ZR806WQe5ixaYx0QoxtAcOgYo87a2qxYc,18266
|
|
76
78
|
lollms_client/ttv_bindings/__init__.py,sha256=UZ8o2izQOJLQgtZ1D1cXoNST7rzqW22rL2Vufc7ddRc,3141
|
|
77
79
|
lollms_client/ttv_bindings/lollms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
|
-
lollms_client-0.20.
|
|
79
|
-
lollms_client-0.20.
|
|
80
|
-
lollms_client-0.20.
|
|
81
|
-
lollms_client-0.20.
|
|
82
|
-
lollms_client-0.20.
|
|
80
|
+
lollms_client-0.20.6.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
81
|
+
lollms_client-0.20.6.dist-info/METADATA,sha256=NG_lJ-ZX3YXxjvMYclHkNPI4aKvheU8CvcDR9gkqY6I,13374
|
|
82
|
+
lollms_client-0.20.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
83
|
+
lollms_client-0.20.6.dist-info/top_level.txt,sha256=NI_W8S4OYZvJjb0QWMZMSIpOrYzpqwPGYaklhyWKH2w,23
|
|
84
|
+
lollms_client-0.20.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|