chuk-tool-processor 0.1.6__py3-none-any.whl → 0.2__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 chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/core/processor.py +345 -132
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +522 -71
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +559 -64
- chuk_tool_processor/execution/tool_executor.py +282 -24
- chuk_tool_processor/execution/wrappers/caching.py +465 -123
- chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
- chuk_tool_processor/execution/wrappers/retry.py +133 -23
- chuk_tool_processor/logging/__init__.py +83 -10
- chuk_tool_processor/logging/context.py +218 -22
- chuk_tool_processor/logging/formatter.py +56 -13
- chuk_tool_processor/logging/helpers.py +91 -16
- chuk_tool_processor/logging/metrics.py +75 -6
- chuk_tool_processor/mcp/mcp_tool.py +80 -35
- chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
- chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
- chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
- chuk_tool_processor/mcp/transport/sse_transport.py +351 -105
- chuk_tool_processor/models/execution_strategy.py +52 -3
- chuk_tool_processor/models/streaming_tool.py +110 -0
- chuk_tool_processor/models/tool_call.py +56 -4
- chuk_tool_processor/models/tool_result.py +115 -9
- chuk_tool_processor/models/validated_tool.py +15 -13
- chuk_tool_processor/plugins/discovery.py +115 -70
- chuk_tool_processor/plugins/parsers/base.py +13 -5
- chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
- chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
- chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
- chuk_tool_processor/registry/__init__.py +46 -7
- chuk_tool_processor/registry/auto_register.py +92 -28
- chuk_tool_processor/registry/decorators.py +134 -11
- chuk_tool_processor/registry/interface.py +48 -14
- chuk_tool_processor/registry/metadata.py +52 -6
- chuk_tool_processor/registry/provider.py +75 -36
- chuk_tool_processor/registry/providers/__init__.py +49 -10
- chuk_tool_processor/registry/providers/memory.py +59 -48
- chuk_tool_processor/registry/tool_export.py +208 -39
- chuk_tool_processor/utils/validation.py +18 -13
- chuk_tool_processor-0.2.dist-info/METADATA +401 -0
- chuk_tool_processor-0.2.dist-info/RECORD +58 -0
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/WHEEL +1 -1
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
- chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
- chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/transport/sse_transport.py
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Proper MCP SSE transport that follows the standard MCP SSE protocol.
|
|
4
|
+
|
|
5
|
+
This transport:
|
|
6
|
+
1. Connects to /sse for SSE stream
|
|
7
|
+
2. Listens for 'endpoint' event to get message URL
|
|
8
|
+
3. Sends MCP initialize handshake FIRST
|
|
9
|
+
4. Only then proceeds with tools/list and tool calls
|
|
10
|
+
5. Handles async responses via SSE message events
|
|
4
11
|
"""
|
|
5
12
|
from __future__ import annotations
|
|
6
13
|
|
|
@@ -16,7 +23,7 @@ from .base_transport import MCPBaseTransport
|
|
|
16
23
|
# --------------------------------------------------------------------------- #
|
|
17
24
|
# Helpers #
|
|
18
25
|
# --------------------------------------------------------------------------- #
|
|
19
|
-
DEFAULT_TIMEOUT =
|
|
26
|
+
DEFAULT_TIMEOUT = 30.0 # Longer timeout for real servers
|
|
20
27
|
HEADERS_JSON: Dict[str, str] = {"accept": "application/json"}
|
|
21
28
|
|
|
22
29
|
|
|
@@ -30,160 +37,399 @@ def _url(base: str, path: str) -> str:
|
|
|
30
37
|
# --------------------------------------------------------------------------- #
|
|
31
38
|
class SSETransport(MCPBaseTransport):
|
|
32
39
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
GET /events → <text/event-stream>
|
|
40
|
+
Proper MCP SSE transport that follows the standard protocol:
|
|
41
|
+
|
|
42
|
+
1. GET /sse → Establishes SSE connection
|
|
43
|
+
2. Waits for 'endpoint' event → Gets message URL
|
|
44
|
+
3. Sends MCP initialize handshake → Establishes session
|
|
45
|
+
4. POST to message URL → Sends tool calls
|
|
46
|
+
5. Waits for async responses via SSE message events
|
|
41
47
|
"""
|
|
42
48
|
|
|
43
|
-
EVENTS_PATH = "/events"
|
|
44
|
-
|
|
45
|
-
# ------------------------------------------------------------------ #
|
|
46
|
-
# Construction #
|
|
47
|
-
# ------------------------------------------------------------------ #
|
|
48
49
|
def __init__(self, url: str, api_key: Optional[str] = None) -> None:
|
|
49
50
|
self.base_url = url.rstrip("/")
|
|
50
51
|
self.api_key = api_key
|
|
51
52
|
|
|
52
53
|
# httpx client (None until initialise)
|
|
53
54
|
self._client: httpx.AsyncClient | None = None
|
|
54
|
-
self.session: httpx.AsyncClient | None = None
|
|
55
|
+
self.session: httpx.AsyncClient | None = None
|
|
55
56
|
|
|
56
|
-
#
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
57
|
+
# MCP SSE state
|
|
58
|
+
self._message_url: Optional[str] = None
|
|
59
|
+
self._session_id: Optional[str] = None
|
|
60
|
+
self._sse_task: Optional[asyncio.Task] = None
|
|
61
|
+
self._connected = asyncio.Event()
|
|
62
|
+
self._initialized = asyncio.Event() # NEW: Track MCP initialization
|
|
63
|
+
|
|
64
|
+
# Async message handling
|
|
65
|
+
self._pending_requests: Dict[str, asyncio.Future] = {}
|
|
66
|
+
self._message_lock = asyncio.Lock()
|
|
59
67
|
|
|
60
68
|
# ------------------------------------------------------------------ #
|
|
61
69
|
# Life-cycle #
|
|
62
70
|
# ------------------------------------------------------------------ #
|
|
63
71
|
async def initialize(self) -> bool:
|
|
64
|
-
"""
|
|
65
|
-
if self._client:
|
|
72
|
+
"""Initialize the MCP SSE transport."""
|
|
73
|
+
if self._client:
|
|
66
74
|
return True
|
|
67
75
|
|
|
76
|
+
headers = {}
|
|
77
|
+
if self.api_key:
|
|
78
|
+
headers["authorization"] = self.api_key
|
|
79
|
+
|
|
68
80
|
self._client = httpx.AsyncClient(
|
|
69
|
-
headers=
|
|
81
|
+
headers=headers,
|
|
70
82
|
timeout=DEFAULT_TIMEOUT,
|
|
71
83
|
)
|
|
72
|
-
self.session = self._client
|
|
84
|
+
self.session = self._client
|
|
73
85
|
|
|
74
|
-
#
|
|
75
|
-
self.
|
|
86
|
+
# Start SSE connection and wait for endpoint
|
|
87
|
+
self._sse_task = asyncio.create_task(self._handle_sse_connection())
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Wait for endpoint event (up to 10 seconds)
|
|
91
|
+
await asyncio.wait_for(self._connected.wait(), timeout=10.0)
|
|
92
|
+
|
|
93
|
+
# NEW: Send MCP initialize handshake
|
|
94
|
+
if await self._initialize_mcp_session():
|
|
95
|
+
return True
|
|
96
|
+
else:
|
|
97
|
+
print("❌ MCP initialization failed")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
except asyncio.TimeoutError:
|
|
101
|
+
print("❌ Timeout waiting for SSE endpoint event")
|
|
102
|
+
return False
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"❌ SSE initialization failed: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
async def _initialize_mcp_session(self) -> bool:
|
|
108
|
+
"""Send the required MCP initialize handshake."""
|
|
109
|
+
if not self._message_url:
|
|
110
|
+
print("❌ No message URL available for initialization")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
print("🔄 Sending MCP initialize handshake...")
|
|
115
|
+
|
|
116
|
+
# Required MCP initialize message
|
|
117
|
+
init_message = {
|
|
118
|
+
"jsonrpc": "2.0",
|
|
119
|
+
"id": "initialize",
|
|
120
|
+
"method": "initialize",
|
|
121
|
+
"params": {
|
|
122
|
+
"protocolVersion": "2024-11-05",
|
|
123
|
+
"capabilities": {
|
|
124
|
+
"tools": {},
|
|
125
|
+
"resources": {},
|
|
126
|
+
"prompts": {},
|
|
127
|
+
"sampling": {}
|
|
128
|
+
},
|
|
129
|
+
"clientInfo": {
|
|
130
|
+
"name": "chuk-tool-processor",
|
|
131
|
+
"version": "1.0.0"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
response = await self._send_message(init_message)
|
|
137
|
+
|
|
138
|
+
if "result" in response:
|
|
139
|
+
server_info = response["result"]
|
|
140
|
+
print(f"✅ MCP initialized: {server_info.get('serverInfo', {}).get('name', 'Unknown Server')}")
|
|
141
|
+
|
|
142
|
+
# Send initialized notification (required by MCP spec)
|
|
143
|
+
notification = {
|
|
144
|
+
"jsonrpc": "2.0",
|
|
145
|
+
"method": "notifications/initialized"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Send notification (don't wait for response)
|
|
149
|
+
await self._send_notification(notification)
|
|
150
|
+
self._initialized.set()
|
|
151
|
+
return True
|
|
152
|
+
else:
|
|
153
|
+
print(f"❌ MCP initialization failed: {response}")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"❌ MCP initialization error: {e}")
|
|
158
|
+
return False
|
|
76
159
|
|
|
77
|
-
|
|
78
|
-
|
|
160
|
+
async def _send_notification(self, notification: Dict[str, Any]) -> None:
|
|
161
|
+
"""Send a JSON-RPC notification (no response expected)."""
|
|
162
|
+
if not self._client or not self._message_url:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
headers = {"Content-Type": "application/json"}
|
|
167
|
+
await self._client.post(
|
|
168
|
+
self._message_url,
|
|
169
|
+
json=notification,
|
|
170
|
+
headers=headers
|
|
171
|
+
)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"⚠️ Failed to send notification: {e}")
|
|
79
174
|
|
|
80
175
|
async def close(self) -> None:
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
|
|
176
|
+
"""Close the transport."""
|
|
177
|
+
# Cancel any pending requests
|
|
178
|
+
for future in self._pending_requests.values():
|
|
179
|
+
if not future.done():
|
|
180
|
+
future.cancel()
|
|
181
|
+
self._pending_requests.clear()
|
|
182
|
+
|
|
183
|
+
if self._sse_task:
|
|
184
|
+
self._sse_task.cancel()
|
|
84
185
|
with contextlib.suppress(asyncio.CancelledError):
|
|
85
|
-
await self.
|
|
86
|
-
self.
|
|
186
|
+
await self._sse_task
|
|
187
|
+
self._sse_task = None
|
|
87
188
|
|
|
88
189
|
if self._client:
|
|
89
190
|
await self._client.aclose()
|
|
90
191
|
self._client = None
|
|
91
|
-
self.session = None
|
|
192
|
+
self.session = None
|
|
92
193
|
|
|
93
194
|
# ------------------------------------------------------------------ #
|
|
94
|
-
#
|
|
195
|
+
# SSE Connection Handler #
|
|
95
196
|
# ------------------------------------------------------------------ #
|
|
96
|
-
async def
|
|
197
|
+
async def _handle_sse_connection(self) -> None:
|
|
198
|
+
"""Handle the SSE connection and extract the endpoint URL."""
|
|
97
199
|
if not self._client:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
resp = await self._client.get(_url(self.base_url, path), headers=HEADERS_JSON)
|
|
101
|
-
resp.raise_for_status()
|
|
102
|
-
return resp.json()
|
|
200
|
+
return
|
|
103
201
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
202
|
+
try:
|
|
203
|
+
headers = {
|
|
204
|
+
"Accept": "text/event-stream",
|
|
205
|
+
"Cache-Control": "no-cache"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async with self._client.stream(
|
|
209
|
+
"GET", f"{self.base_url}/sse", headers=headers
|
|
210
|
+
) as response:
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
|
|
213
|
+
async for line in response.aiter_lines():
|
|
214
|
+
if not line:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Parse SSE events
|
|
218
|
+
if line.startswith("event: "):
|
|
219
|
+
event_type = line[7:].strip()
|
|
220
|
+
|
|
221
|
+
elif line.startswith("data: ") and 'event_type' in locals():
|
|
222
|
+
data = line[6:].strip()
|
|
223
|
+
|
|
224
|
+
if event_type == "endpoint":
|
|
225
|
+
# Got the endpoint URL for messages - construct full URL
|
|
226
|
+
self._message_url = f"{self.base_url}{data}"
|
|
227
|
+
|
|
228
|
+
# Extract session_id if present
|
|
229
|
+
if "session_id=" in data:
|
|
230
|
+
self._session_id = data.split("session_id=")[1].split("&")[0]
|
|
231
|
+
|
|
232
|
+
print(f"✅ Got message endpoint: {self._message_url}")
|
|
233
|
+
self._connected.set()
|
|
234
|
+
|
|
235
|
+
elif event_type == "message":
|
|
236
|
+
# Handle incoming JSON-RPC responses
|
|
237
|
+
try:
|
|
238
|
+
message = json.loads(data)
|
|
239
|
+
await self._handle_incoming_message(message)
|
|
240
|
+
except json.JSONDecodeError:
|
|
241
|
+
print(f"❌ Failed to parse message: {data}")
|
|
242
|
+
|
|
243
|
+
except asyncio.CancelledError:
|
|
244
|
+
pass
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(f"❌ SSE connection failed: {e}")
|
|
107
247
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
248
|
+
async def _handle_incoming_message(self, message: Dict[str, Any]) -> None:
|
|
249
|
+
"""Handle incoming JSON-RPC response messages."""
|
|
250
|
+
message_id = message.get("id")
|
|
251
|
+
if message_id and message_id in self._pending_requests:
|
|
252
|
+
# Complete the pending request
|
|
253
|
+
future = self._pending_requests.pop(message_id)
|
|
254
|
+
if not future.done():
|
|
255
|
+
future.set_result(message)
|
|
113
256
|
|
|
114
257
|
# ------------------------------------------------------------------ #
|
|
115
|
-
#
|
|
258
|
+
# MCP Protocol Methods #
|
|
116
259
|
# ------------------------------------------------------------------ #
|
|
117
260
|
async def send_ping(self) -> bool:
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
try:
|
|
121
|
-
await self._get_json("/ping")
|
|
122
|
-
return True
|
|
123
|
-
except Exception: # pragma: no cover
|
|
124
|
-
return False
|
|
261
|
+
"""Test if we have a working and initialized connection."""
|
|
262
|
+
return self._message_url is not None and self._initialized.is_set()
|
|
125
263
|
|
|
126
264
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
127
|
-
|
|
265
|
+
"""Get available tools using tools/list."""
|
|
266
|
+
# NEW: Wait for initialization before proceeding
|
|
267
|
+
if not self._initialized.is_set():
|
|
268
|
+
print("⏳ Waiting for MCP initialization...")
|
|
269
|
+
try:
|
|
270
|
+
await asyncio.wait_for(self._initialized.wait(), timeout=10.0)
|
|
271
|
+
except asyncio.TimeoutError:
|
|
272
|
+
print("❌ Timeout waiting for MCP initialization")
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
if not self._message_url:
|
|
128
276
|
return []
|
|
277
|
+
|
|
129
278
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
279
|
+
message = {
|
|
280
|
+
"jsonrpc": "2.0",
|
|
281
|
+
"id": "tools_list",
|
|
282
|
+
"method": "tools/list",
|
|
283
|
+
"params": {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
response = await self._send_message(message)
|
|
287
|
+
|
|
288
|
+
if "result" in response and "tools" in response["result"]:
|
|
289
|
+
return response["result"]["tools"]
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print(f"❌ Failed to get tools: {e}")
|
|
293
|
+
|
|
294
|
+
return []
|
|
134
295
|
|
|
135
296
|
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
297
|
+
"""Execute a tool call using the MCP protocol."""
|
|
298
|
+
# NEW: Ensure initialization before tool calls
|
|
299
|
+
if not self._initialized.is_set():
|
|
300
|
+
return {"isError": True, "error": "MCP session not initialized"}
|
|
301
|
+
|
|
302
|
+
if not self._message_url:
|
|
303
|
+
return {"isError": True, "error": "No message endpoint available"}
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
message = {
|
|
307
|
+
"jsonrpc": "2.0",
|
|
308
|
+
"id": f"call_{tool_name}",
|
|
309
|
+
"method": "tools/call",
|
|
310
|
+
"params": {
|
|
311
|
+
"name": tool_name,
|
|
312
|
+
"arguments": arguments
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
response = await self._send_message(message)
|
|
317
|
+
|
|
318
|
+
# Process MCP response
|
|
319
|
+
if "error" in response:
|
|
320
|
+
return {
|
|
321
|
+
"isError": True,
|
|
322
|
+
"error": response["error"].get("message", "Unknown error")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if "result" in response:
|
|
326
|
+
result = response["result"]
|
|
327
|
+
|
|
328
|
+
# Handle MCP tool response format
|
|
329
|
+
if "content" in result:
|
|
330
|
+
# Extract content from MCP format
|
|
331
|
+
content = result["content"]
|
|
332
|
+
if isinstance(content, list) and content:
|
|
333
|
+
# Take first content item
|
|
334
|
+
first_content = content[0]
|
|
335
|
+
if isinstance(first_content, dict) and "text" in first_content:
|
|
336
|
+
return {"isError": False, "content": first_content["text"]}
|
|
337
|
+
|
|
338
|
+
return {"isError": False, "content": content}
|
|
339
|
+
|
|
340
|
+
# Direct result
|
|
341
|
+
return {"isError": False, "content": result}
|
|
342
|
+
|
|
343
|
+
return {"isError": True, "error": "No result in response"}
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
return {"isError": True, "error": str(e)}
|
|
347
|
+
|
|
348
|
+
async def _send_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
349
|
+
"""Send a JSON-RPC message to the server and wait for async response."""
|
|
350
|
+
if not self._client or not self._message_url:
|
|
351
|
+
raise RuntimeError("Transport not properly initialized")
|
|
352
|
+
|
|
353
|
+
message_id = message.get("id")
|
|
354
|
+
if not message_id:
|
|
355
|
+
raise ValueError("Message must have an ID")
|
|
356
|
+
|
|
357
|
+
# Create a future for this request
|
|
358
|
+
future = asyncio.Future()
|
|
359
|
+
async with self._message_lock:
|
|
360
|
+
self._pending_requests[message_id] = future
|
|
139
361
|
|
|
140
362
|
try:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
363
|
+
headers = {"Content-Type": "application/json"}
|
|
364
|
+
|
|
365
|
+
# Send the request
|
|
366
|
+
response = await self._client.post(
|
|
367
|
+
self._message_url,
|
|
368
|
+
json=message,
|
|
369
|
+
headers=headers
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Check if server accepted the request
|
|
373
|
+
if response.status_code == 202:
|
|
374
|
+
# Server accepted - wait for async response via SSE
|
|
375
|
+
try:
|
|
376
|
+
response_message = await asyncio.wait_for(future, timeout=30.0)
|
|
377
|
+
return response_message
|
|
378
|
+
except asyncio.TimeoutError:
|
|
379
|
+
raise RuntimeError(f"Timeout waiting for response to message {message_id}")
|
|
380
|
+
else:
|
|
381
|
+
# Immediate response - parse and return
|
|
382
|
+
response.raise_for_status()
|
|
383
|
+
return response.json()
|
|
384
|
+
|
|
385
|
+
finally:
|
|
386
|
+
# Clean up pending request
|
|
387
|
+
async with self._message_lock:
|
|
388
|
+
self._pending_requests.pop(message_id, None)
|
|
145
389
|
|
|
146
|
-
#
|
|
390
|
+
# ------------------------------------------------------------------ #
|
|
391
|
+
# Additional MCP methods #
|
|
392
|
+
# ------------------------------------------------------------------ #
|
|
147
393
|
async def list_resources(self) -> List[Dict[str, Any]]:
|
|
148
|
-
|
|
394
|
+
"""List available resources."""
|
|
395
|
+
if not self._initialized.is_set() or not self._message_url:
|
|
149
396
|
return []
|
|
397
|
+
|
|
150
398
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
399
|
+
message = {
|
|
400
|
+
"jsonrpc": "2.0",
|
|
401
|
+
"id": "resources_list",
|
|
402
|
+
"method": "resources/list",
|
|
403
|
+
"params": {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
response = await self._send_message(message)
|
|
407
|
+
if "result" in response and "resources" in response["result"]:
|
|
408
|
+
return response["result"]["resources"]
|
|
409
|
+
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
return []
|
|
155
414
|
|
|
156
415
|
async def list_prompts(self) -> List[Dict[str, Any]]:
|
|
157
|
-
|
|
416
|
+
"""List available prompts."""
|
|
417
|
+
if not self._initialized.is_set() or not self._message_url:
|
|
158
418
|
return []
|
|
419
|
+
|
|
159
420
|
try:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
async with self._client.stream(
|
|
176
|
-
"GET", _url(self.base_url, self.EVENTS_PATH), headers=HEADERS_JSON
|
|
177
|
-
) as resp:
|
|
178
|
-
resp.raise_for_status()
|
|
179
|
-
async for line in resp.aiter_lines():
|
|
180
|
-
if not line:
|
|
181
|
-
continue
|
|
182
|
-
try:
|
|
183
|
-
await self._incoming_queue.put(json.loads(line))
|
|
184
|
-
except json.JSONDecodeError:
|
|
185
|
-
continue
|
|
186
|
-
except asyncio.CancelledError:
|
|
187
|
-
break
|
|
188
|
-
except Exception:
|
|
189
|
-
await asyncio.sleep(1.0) # back-off and retry
|
|
421
|
+
message = {
|
|
422
|
+
"jsonrpc": "2.0",
|
|
423
|
+
"id": "prompts_list",
|
|
424
|
+
"method": "prompts/list",
|
|
425
|
+
"params": {}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
response = await self._send_message(message)
|
|
429
|
+
if "result" in response and "prompts" in response["result"]:
|
|
430
|
+
return response["result"]["prompts"]
|
|
431
|
+
|
|
432
|
+
except Exception:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
return []
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
# chuk_tool_processor/models/execution_strategy.py
|
|
2
|
+
"""
|
|
3
|
+
Abstract base class for tool execution strategies.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
2
7
|
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import List, Optional
|
|
8
|
+
from typing import List, Optional, Dict, Any, AsyncIterator
|
|
4
9
|
|
|
5
10
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
6
11
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
7
12
|
|
|
8
|
-
|
|
9
13
|
class ExecutionStrategy(ABC):
|
|
10
14
|
"""
|
|
11
15
|
Strategy interface for executing ToolCall objects.
|
|
16
|
+
|
|
17
|
+
All execution strategies must implement at least the run method,
|
|
18
|
+
and optionally stream_run for streaming support.
|
|
12
19
|
"""
|
|
13
20
|
@abstractmethod
|
|
14
21
|
async def run(
|
|
@@ -16,4 +23,46 @@ class ExecutionStrategy(ABC):
|
|
|
16
23
|
calls: List[ToolCall],
|
|
17
24
|
timeout: Optional[float] = None
|
|
18
25
|
) -> List[ToolResult]:
|
|
19
|
-
|
|
26
|
+
"""
|
|
27
|
+
Execute a list of tool calls and return their results.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
calls: List of ToolCall objects to execute
|
|
31
|
+
timeout: Optional timeout in seconds for each call
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of ToolResult objects in the same order as the calls
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
async def stream_run(
|
|
39
|
+
self,
|
|
40
|
+
calls: List[ToolCall],
|
|
41
|
+
timeout: Optional[float] = None
|
|
42
|
+
) -> AsyncIterator[ToolResult]:
|
|
43
|
+
"""
|
|
44
|
+
Execute tool calls and yield results as they become available.
|
|
45
|
+
|
|
46
|
+
Default implementation executes all calls with run() and yields the results.
|
|
47
|
+
Subclasses can override for true streaming behavior.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
calls: List of ToolCall objects to execute
|
|
51
|
+
timeout: Optional timeout in seconds for each call
|
|
52
|
+
|
|
53
|
+
Yields:
|
|
54
|
+
ToolResult objects as they become available
|
|
55
|
+
"""
|
|
56
|
+
results = await self.run(calls, timeout=timeout)
|
|
57
|
+
for result in results:
|
|
58
|
+
yield result
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def supports_streaming(self) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check if this strategy supports true streaming.
|
|
64
|
+
|
|
65
|
+
Default implementation returns False. Streaming-capable strategies
|
|
66
|
+
should override this to return True.
|
|
67
|
+
"""
|
|
68
|
+
return False
|