DeepFabric 4.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepfabric/__init__.py +70 -0
- deepfabric/__main__.py +6 -0
- deepfabric/auth.py +382 -0
- deepfabric/builders.py +303 -0
- deepfabric/builders_agent.py +1304 -0
- deepfabric/cli.py +1288 -0
- deepfabric/config.py +899 -0
- deepfabric/config_manager.py +251 -0
- deepfabric/constants.py +94 -0
- deepfabric/dataset_manager.py +534 -0
- deepfabric/error_codes.py +581 -0
- deepfabric/evaluation/__init__.py +47 -0
- deepfabric/evaluation/backends/__init__.py +32 -0
- deepfabric/evaluation/backends/ollama_backend.py +137 -0
- deepfabric/evaluation/backends/tool_call_parsers.py +409 -0
- deepfabric/evaluation/backends/transformers_backend.py +326 -0
- deepfabric/evaluation/evaluator.py +845 -0
- deepfabric/evaluation/evaluators/__init__.py +13 -0
- deepfabric/evaluation/evaluators/base.py +104 -0
- deepfabric/evaluation/evaluators/builtin/__init__.py +5 -0
- deepfabric/evaluation/evaluators/builtin/tool_calling.py +93 -0
- deepfabric/evaluation/evaluators/registry.py +66 -0
- deepfabric/evaluation/inference.py +155 -0
- deepfabric/evaluation/metrics.py +397 -0
- deepfabric/evaluation/parser.py +304 -0
- deepfabric/evaluation/reporters/__init__.py +13 -0
- deepfabric/evaluation/reporters/base.py +56 -0
- deepfabric/evaluation/reporters/cloud_reporter.py +195 -0
- deepfabric/evaluation/reporters/file_reporter.py +61 -0
- deepfabric/evaluation/reporters/multi_reporter.py +56 -0
- deepfabric/exceptions.py +67 -0
- deepfabric/factory.py +26 -0
- deepfabric/generator.py +1084 -0
- deepfabric/graph.py +545 -0
- deepfabric/hf_hub.py +214 -0
- deepfabric/kaggle_hub.py +219 -0
- deepfabric/llm/__init__.py +41 -0
- deepfabric/llm/api_key_verifier.py +534 -0
- deepfabric/llm/client.py +1206 -0
- deepfabric/llm/errors.py +105 -0
- deepfabric/llm/rate_limit_config.py +262 -0
- deepfabric/llm/rate_limit_detector.py +278 -0
- deepfabric/llm/retry_handler.py +270 -0
- deepfabric/metrics.py +212 -0
- deepfabric/progress.py +262 -0
- deepfabric/prompts.py +290 -0
- deepfabric/schemas.py +1000 -0
- deepfabric/spin/__init__.py +6 -0
- deepfabric/spin/client.py +263 -0
- deepfabric/spin/models.py +26 -0
- deepfabric/stream_simulator.py +90 -0
- deepfabric/tools/__init__.py +5 -0
- deepfabric/tools/defaults.py +85 -0
- deepfabric/tools/loader.py +87 -0
- deepfabric/tools/mcp_client.py +677 -0
- deepfabric/topic_manager.py +303 -0
- deepfabric/topic_model.py +20 -0
- deepfabric/training/__init__.py +35 -0
- deepfabric/training/api_key_prompt.py +302 -0
- deepfabric/training/callback.py +363 -0
- deepfabric/training/metrics_sender.py +301 -0
- deepfabric/tree.py +438 -0
- deepfabric/tui.py +1267 -0
- deepfabric/update_checker.py +166 -0
- deepfabric/utils.py +150 -0
- deepfabric/validation.py +143 -0
- deepfabric-4.4.0.dist-info/METADATA +702 -0
- deepfabric-4.4.0.dist-info/RECORD +71 -0
- deepfabric-4.4.0.dist-info/WHEEL +4 -0
- deepfabric-4.4.0.dist-info/entry_points.txt +2 -0
- deepfabric-4.4.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Spin Framework HTTP client for tool execution."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from http import HTTPStatus
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .models import SpinComponentsResponse, SpinExecutionResult, SpinHealthResponse
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SpinClient:
|
|
18
|
+
"""HTTP client for communicating with Spin service."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
endpoint: str,
|
|
23
|
+
timeout: float = 30.0,
|
|
24
|
+
tool_execute_path: str | None = None,
|
|
25
|
+
):
|
|
26
|
+
"""Initialize Spin client.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
endpoint: Base URL of Spin service (e.g., "http://localhost:3000")
|
|
30
|
+
timeout: Request timeout in seconds
|
|
31
|
+
tool_execute_path: Custom path for tool execution (e.g., "/mock/execute").
|
|
32
|
+
When set, uses MCP-style payload format.
|
|
33
|
+
"""
|
|
34
|
+
self.endpoint = endpoint.rstrip("/")
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self.tool_execute_path = tool_execute_path
|
|
37
|
+
self._client: httpx.AsyncClient | None = None
|
|
38
|
+
|
|
39
|
+
async def _ensure_client(self) -> httpx.AsyncClient:
|
|
40
|
+
"""Ensure async client is initialized."""
|
|
41
|
+
if self._client is None:
|
|
42
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
43
|
+
return self._client
|
|
44
|
+
|
|
45
|
+
async def close(self) -> None:
|
|
46
|
+
"""Close the HTTP client."""
|
|
47
|
+
if self._client is not None:
|
|
48
|
+
await self._client.aclose()
|
|
49
|
+
self._client = None
|
|
50
|
+
|
|
51
|
+
async def health_check(self) -> SpinHealthResponse:
|
|
52
|
+
"""Check if Spin service is healthy.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
SpinHealthResponse with status and available components
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
httpx.HTTPError: If service is unreachable
|
|
59
|
+
"""
|
|
60
|
+
client = await self._ensure_client()
|
|
61
|
+
response = await client.get(f"{self.endpoint}/health")
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return SpinHealthResponse.model_validate(response.json())
|
|
64
|
+
|
|
65
|
+
async def get_components(self) -> list[str]:
|
|
66
|
+
"""Get list of available tool components.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of component names
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
httpx.HTTPError: If request fails
|
|
73
|
+
"""
|
|
74
|
+
client = await self._ensure_client()
|
|
75
|
+
response = await client.get(f"{self.endpoint}/components")
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
data = SpinComponentsResponse.model_validate(response.json())
|
|
78
|
+
return data.components
|
|
79
|
+
|
|
80
|
+
async def execute_tool(
|
|
81
|
+
self,
|
|
82
|
+
session_id: str,
|
|
83
|
+
tool_name: str,
|
|
84
|
+
arguments: dict[str, Any],
|
|
85
|
+
component: str | None = None,
|
|
86
|
+
) -> SpinExecutionResult:
|
|
87
|
+
"""Execute a tool via Spin.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
session_id: Session identifier for state isolation
|
|
91
|
+
tool_name: Name of the tool to execute
|
|
92
|
+
arguments: Tool arguments
|
|
93
|
+
component: Spin component that implements this tool (e.g., 'vfs', 'github')
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
SpinExecutionResult with success status and result/error
|
|
97
|
+
"""
|
|
98
|
+
client = await self._ensure_client()
|
|
99
|
+
|
|
100
|
+
# Use custom tool_execute_path if configured (MCP/mock style)
|
|
101
|
+
if self.tool_execute_path:
|
|
102
|
+
execute_url = f"{self.endpoint}{self.tool_execute_path}"
|
|
103
|
+
# MCP-style payload format
|
|
104
|
+
payload = {
|
|
105
|
+
"name": tool_name,
|
|
106
|
+
"arguments": arguments,
|
|
107
|
+
}
|
|
108
|
+
else:
|
|
109
|
+
# Standard component-based routing
|
|
110
|
+
if component:
|
|
111
|
+
execute_url = f"{self.endpoint}/{component}/execute"
|
|
112
|
+
else:
|
|
113
|
+
execute_url = f"{self.endpoint}/execute"
|
|
114
|
+
# Standard payload format
|
|
115
|
+
payload = {
|
|
116
|
+
"session_id": session_id,
|
|
117
|
+
"tool": tool_name,
|
|
118
|
+
"args": arguments,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
response = await client.post(
|
|
123
|
+
execute_url,
|
|
124
|
+
json=payload,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if response.status_code == HTTPStatus.OK:
|
|
128
|
+
data = response.json()
|
|
129
|
+
|
|
130
|
+
# Handle MCP/mock response format (has "result" key with nested data)
|
|
131
|
+
if self.tool_execute_path and "result" in data:
|
|
132
|
+
# Mock component returns {"result": {...}}
|
|
133
|
+
|
|
134
|
+
return SpinExecutionResult(
|
|
135
|
+
success=True,
|
|
136
|
+
result=json.dumps(data["result"]),
|
|
137
|
+
error_type=None,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Standard Spin component response format
|
|
141
|
+
return SpinExecutionResult(
|
|
142
|
+
success=data.get("success", False),
|
|
143
|
+
result=data.get("result", ""),
|
|
144
|
+
error_type=data.get("error_type"),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
error_data = response.json() if response.content else {}
|
|
148
|
+
return SpinExecutionResult(
|
|
149
|
+
success=False,
|
|
150
|
+
result=error_data.get("error", f"HTTP {response.status_code}"),
|
|
151
|
+
error_type="HTTPError",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
except httpx.TimeoutException:
|
|
155
|
+
return SpinExecutionResult(
|
|
156
|
+
success=False,
|
|
157
|
+
result="Tool execution timed out",
|
|
158
|
+
error_type="Timeout",
|
|
159
|
+
)
|
|
160
|
+
except httpx.HTTPError as e:
|
|
161
|
+
return SpinExecutionResult(
|
|
162
|
+
success=False,
|
|
163
|
+
result=f"HTTP error: {e!s}",
|
|
164
|
+
error_type="HTTPError",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def cleanup_session(self, session_id: str) -> bool:
|
|
168
|
+
"""Clean up all state for a session.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
session_id: Session to clean up
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if cleanup succeeded
|
|
175
|
+
"""
|
|
176
|
+
client = await self._ensure_client()
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
response = await client.delete(f"{self.endpoint}/session/{session_id}")
|
|
180
|
+
except httpx.HTTPError as e:
|
|
181
|
+
logger.warning("Failed to cleanup session %s: %s", session_id, e)
|
|
182
|
+
return False
|
|
183
|
+
else:
|
|
184
|
+
return response.status_code == HTTPStatus.OK
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class SpinSession:
|
|
188
|
+
"""Manages a Spin session with state persistence across tool calls."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, client: SpinClient, session_id: str | None = None):
|
|
191
|
+
"""Initialize a Spin session.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
client: SpinClient instance
|
|
195
|
+
session_id: Optional session ID (generates UUID if not provided)
|
|
196
|
+
"""
|
|
197
|
+
self.client = client
|
|
198
|
+
self.session_id = session_id or str(uuid.uuid4())
|
|
199
|
+
self._initialized = False
|
|
200
|
+
|
|
201
|
+
async def execute_tool(
|
|
202
|
+
self,
|
|
203
|
+
tool_name: str,
|
|
204
|
+
arguments: dict[str, Any],
|
|
205
|
+
component: str | None = None,
|
|
206
|
+
) -> SpinExecutionResult:
|
|
207
|
+
"""Execute a tool in this session.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
tool_name: Name of the tool
|
|
211
|
+
arguments: Tool arguments
|
|
212
|
+
component: Spin component that implements this tool (e.g., 'vfs', 'github')
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
SpinExecutionResult with execution outcome
|
|
216
|
+
"""
|
|
217
|
+
return await self.client.execute_tool(
|
|
218
|
+
session_id=self.session_id,
|
|
219
|
+
tool_name=tool_name,
|
|
220
|
+
arguments=arguments,
|
|
221
|
+
component=component,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def seed_files(self, files: dict[str, str]) -> bool:
|
|
225
|
+
"""Seed initial files into the session's virtual filesystem.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
files: Dictionary of file_path -> content
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if all files were seeded successfully
|
|
232
|
+
"""
|
|
233
|
+
for file_path, content in files.items():
|
|
234
|
+
result = await self.execute_tool(
|
|
235
|
+
tool_name="write_file",
|
|
236
|
+
arguments={"file_path": file_path, "content": content},
|
|
237
|
+
component="vfs", # Builtin VFS tool
|
|
238
|
+
)
|
|
239
|
+
if not result.success:
|
|
240
|
+
logger.error("Failed to seed file %s: %s", file_path, result.result)
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
self._initialized = True
|
|
244
|
+
logger.debug("Seeded %d files for session %s", len(files), self.session_id)
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
async def cleanup(self) -> bool:
|
|
248
|
+
"""Clean up this session's state.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
True if cleanup succeeded
|
|
252
|
+
"""
|
|
253
|
+
result = await self.client.cleanup_session(self.session_id)
|
|
254
|
+
self._initialized = False
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
async def __aenter__(self) -> "SpinSession":
|
|
258
|
+
"""Async context manager entry."""
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
262
|
+
"""Async context manager exit - cleanup session."""
|
|
263
|
+
await self.cleanup()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Data models for Spin integration."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SpinExecutionResult(BaseModel):
|
|
7
|
+
"""Result from Spin tool execution."""
|
|
8
|
+
|
|
9
|
+
success: bool = Field(description="Whether execution succeeded")
|
|
10
|
+
result: str = Field(description="Tool output or error message")
|
|
11
|
+
error_type: str | None = Field(
|
|
12
|
+
default=None, description="Error type if failed (e.g., 'FileNotFound', 'Timeout')"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SpinHealthResponse(BaseModel):
|
|
17
|
+
"""Response from Spin health check."""
|
|
18
|
+
|
|
19
|
+
status: str = Field(description="Service status")
|
|
20
|
+
components: list[str] = Field(default_factory=list, description="Available components")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SpinComponentsResponse(BaseModel):
|
|
24
|
+
"""Response from Spin components listing."""
|
|
25
|
+
|
|
26
|
+
components: list[str] = Field(default_factory=list, description="Available component names")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Buffered stream simulation for TUI preview.
|
|
2
|
+
|
|
3
|
+
This module provides a fire-and-forget streaming simulation that emits chunks
|
|
4
|
+
to the TUI preview without impacting generation performance. The simulation
|
|
5
|
+
runs in background while generation continues immediately.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from .constants import STREAM_SIM_CHUNK_DELAY_MS, STREAM_SIM_CHUNK_SIZE
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .progress import ProgressReporter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Track current stream task to prevent interleaving
|
|
21
|
+
class _StreamState:
|
|
22
|
+
current_task: asyncio.Task | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_stream_state = _StreamState()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StreamSimulatorConfig(BaseModel):
|
|
29
|
+
"""Configuration for buffered stream simulation."""
|
|
30
|
+
|
|
31
|
+
chunk_size: int = Field(default=STREAM_SIM_CHUNK_SIZE, ge=1, le=100)
|
|
32
|
+
chunk_delay_ms: float = Field(default=STREAM_SIM_CHUNK_DELAY_MS, ge=0.0, le=100.0)
|
|
33
|
+
enabled: bool = Field(default=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def simulate_stream(
|
|
37
|
+
progress_reporter: "ProgressReporter | None",
|
|
38
|
+
content: str,
|
|
39
|
+
source: str,
|
|
40
|
+
config: StreamSimulatorConfig | None = None,
|
|
41
|
+
**metadata,
|
|
42
|
+
) -> asyncio.Task | None:
|
|
43
|
+
"""Fire-and-forget stream simulation (non-blocking).
|
|
44
|
+
|
|
45
|
+
Starts simulation in background and returns immediately. This is the
|
|
46
|
+
primary interface for stream simulation throughout the codebase.
|
|
47
|
+
|
|
48
|
+
Cancels any in-flight stream task before starting a new one to prevent
|
|
49
|
+
interleaved chunks from multiple generations appearing scrambled in the TUI.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
progress_reporter: ProgressReporter instance or None
|
|
53
|
+
content: Text to simulate streaming
|
|
54
|
+
source: Source identifier for TUI routing
|
|
55
|
+
config: Optional StreamSimulatorConfig override
|
|
56
|
+
**metadata: Additional metadata passed to emit_chunk
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Task if started, None if no-op (no reporter or disabled)
|
|
60
|
+
"""
|
|
61
|
+
_config = config or StreamSimulatorConfig()
|
|
62
|
+
if not progress_reporter or not _config.enabled:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Cancel any in-flight stream to prevent interleaving
|
|
66
|
+
if _stream_state.current_task is not None and not _stream_state.current_task.done():
|
|
67
|
+
_stream_state.current_task.cancel()
|
|
68
|
+
|
|
69
|
+
async def _simulate_impl() -> None:
|
|
70
|
+
"""Internal implementation of chunk emission."""
|
|
71
|
+
if not content:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
delay = _config.chunk_delay_ms / 1000.0
|
|
75
|
+
chunk_size = _config.chunk_size
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
for i in range(0, len(content), chunk_size):
|
|
79
|
+
# Await before emitting to ensure cancellation is processed
|
|
80
|
+
# and event loop is not blocked when delay is 0
|
|
81
|
+
if i > 0:
|
|
82
|
+
await asyncio.sleep(delay)
|
|
83
|
+
chunk = content[i : i + chunk_size]
|
|
84
|
+
progress_reporter.emit_chunk(source, chunk, **metadata)
|
|
85
|
+
except asyncio.CancelledError:
|
|
86
|
+
# Gracefully handle cancellation
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
_stream_state.current_task = asyncio.create_task(_simulate_impl())
|
|
90
|
+
return _stream_state.current_task
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Default tool definitions for common use cases."""
|
|
2
|
+
|
|
3
|
+
from ..schemas import ToolDefinition, ToolParameter, ToolRegistry
|
|
4
|
+
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# Builtin Tools - Execute via Spin VFS component
|
|
7
|
+
# These are the only hardcoded tools. Custom components (github, slack, etc.)
|
|
8
|
+
# should be configured via YAML config with tools loaded from endpoints.
|
|
9
|
+
# =============================================================================
|
|
10
|
+
|
|
11
|
+
# Component mapping: "builtin" in config routes to "/vfs/execute" in Spin
|
|
12
|
+
BUILTIN_COMPONENT = "vfs"
|
|
13
|
+
|
|
14
|
+
READ_FILE_TOOL = ToolDefinition(
|
|
15
|
+
name="read_file",
|
|
16
|
+
description="Read content from a file",
|
|
17
|
+
parameters=[
|
|
18
|
+
ToolParameter(
|
|
19
|
+
name="file_path",
|
|
20
|
+
type="str",
|
|
21
|
+
description="Path to the file to read",
|
|
22
|
+
required=True,
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
returns="File content as a string",
|
|
26
|
+
category="filesystem",
|
|
27
|
+
component=BUILTIN_COMPONENT,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
WRITE_FILE_TOOL = ToolDefinition(
|
|
31
|
+
name="write_file",
|
|
32
|
+
description="Write content to a file",
|
|
33
|
+
parameters=[
|
|
34
|
+
ToolParameter(
|
|
35
|
+
name="file_path",
|
|
36
|
+
type="str",
|
|
37
|
+
description="Path to the file to write",
|
|
38
|
+
required=True,
|
|
39
|
+
),
|
|
40
|
+
ToolParameter(
|
|
41
|
+
name="content",
|
|
42
|
+
type="str",
|
|
43
|
+
description="Content to write to the file",
|
|
44
|
+
required=True,
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
returns="Confirmation message with bytes written",
|
|
48
|
+
category="filesystem",
|
|
49
|
+
component=BUILTIN_COMPONENT,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
LIST_FILES_TOOL = ToolDefinition(
|
|
53
|
+
name="list_files",
|
|
54
|
+
description="List all files in the current session",
|
|
55
|
+
parameters=[],
|
|
56
|
+
returns="JSON array of file paths",
|
|
57
|
+
category="filesystem",
|
|
58
|
+
component=BUILTIN_COMPONENT,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
DELETE_FILE_TOOL = ToolDefinition(
|
|
62
|
+
name="delete_file",
|
|
63
|
+
description="Delete a file",
|
|
64
|
+
parameters=[
|
|
65
|
+
ToolParameter(
|
|
66
|
+
name="file_path",
|
|
67
|
+
type="str",
|
|
68
|
+
description="Path to the file to delete",
|
|
69
|
+
required=True,
|
|
70
|
+
),
|
|
71
|
+
],
|
|
72
|
+
returns="Confirmation that file was deleted",
|
|
73
|
+
category="filesystem",
|
|
74
|
+
component=BUILTIN_COMPONENT,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Builtin tools registry
|
|
78
|
+
BUILTIN_TOOL_REGISTRY = ToolRegistry(
|
|
79
|
+
tools=[
|
|
80
|
+
READ_FILE_TOOL,
|
|
81
|
+
WRITE_FILE_TOOL,
|
|
82
|
+
LIST_FILES_TOOL,
|
|
83
|
+
DELETE_FILE_TOOL,
|
|
84
|
+
]
|
|
85
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tool loading and management utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from ..exceptions import ConfigurationError
|
|
13
|
+
from ..schemas import MCPToolDefinition, ToolDefinition, ToolRegistry
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_tools_from_endpoint(endpoint_url: str, timeout: float = 30.0) -> ToolRegistry:
|
|
19
|
+
"""Load tool definitions from an HTTP endpoint in MCP format.
|
|
20
|
+
|
|
21
|
+
Fetches tools from an endpoint like /mock/list-tools that returns
|
|
22
|
+
MCP-format tool definitions with inputSchema.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
endpoint_url: Full URL to fetch tools from (e.g., 'http://localhost:3000/mock/list-tools')
|
|
26
|
+
timeout: Request timeout in seconds
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ToolRegistry with loaded tools converted from MCP format
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ConfigurationError: If endpoint cannot be reached or returns invalid data
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
response = httpx.get(endpoint_url, timeout=timeout)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
data = response.json()
|
|
38
|
+
except httpx.RequestError as e:
|
|
39
|
+
raise ConfigurationError(f"Failed to connect to tools endpoint {endpoint_url}: {e}") from e
|
|
40
|
+
except httpx.HTTPStatusError as e:
|
|
41
|
+
raise ConfigurationError(
|
|
42
|
+
f"Tools endpoint returned error {e.response.status_code}: {e.response.text}"
|
|
43
|
+
) from e
|
|
44
|
+
except json.JSONDecodeError as e:
|
|
45
|
+
raise ConfigurationError(f"Invalid JSON from tools endpoint: {e}") from e
|
|
46
|
+
|
|
47
|
+
# Extract tools array - handle both {"tools": [...]} and direct array
|
|
48
|
+
if isinstance(data, dict) and "tools" in data:
|
|
49
|
+
tools_data = data["tools"]
|
|
50
|
+
elif isinstance(data, list):
|
|
51
|
+
tools_data = data
|
|
52
|
+
else:
|
|
53
|
+
raise ConfigurationError(
|
|
54
|
+
f"Invalid response from {endpoint_url}: expected 'tools' key or array"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Convert MCP tools to ToolDefinition
|
|
58
|
+
try:
|
|
59
|
+
tools = []
|
|
60
|
+
for tool_dict in tools_data:
|
|
61
|
+
mcp_tool = MCPToolDefinition.model_validate(tool_dict)
|
|
62
|
+
tools.append(ToolDefinition.from_mcp(mcp_tool))
|
|
63
|
+
|
|
64
|
+
logger.info("Loaded %d tools from endpoint %s", len(tools), endpoint_url)
|
|
65
|
+
return ToolRegistry(tools=tools)
|
|
66
|
+
|
|
67
|
+
except ValidationError as e:
|
|
68
|
+
raise ConfigurationError(f"Invalid MCP tool schema from {endpoint_url}: {e}") from e
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_tools_from_dict(tool_dicts: list[dict[str, Any]]) -> ToolRegistry:
|
|
72
|
+
"""Load tool definitions from a list of dictionaries.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
tool_dicts: List of tool definition dictionaries
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ToolRegistry with loaded tools
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ConfigurationError: If tool definitions are invalid
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
tools = [ToolDefinition.model_validate(tool_dict) for tool_dict in tool_dicts]
|
|
85
|
+
return ToolRegistry(tools=tools)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise ConfigurationError(f"Invalid tool definitions: {str(e)}") from e
|