rakam-systems-core 0.1.1rc7__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.
- rakam_systems_core/__init__.py +41 -0
- rakam_systems_core/ai_core/__init__.py +68 -0
- rakam_systems_core/ai_core/base.py +142 -0
- rakam_systems_core/ai_core/config.py +12 -0
- rakam_systems_core/ai_core/config_loader.py +580 -0
- rakam_systems_core/ai_core/config_schema.py +395 -0
- rakam_systems_core/ai_core/interfaces/__init__.py +30 -0
- rakam_systems_core/ai_core/interfaces/agent.py +83 -0
- rakam_systems_core/ai_core/interfaces/chat_history.py +122 -0
- rakam_systems_core/ai_core/interfaces/chunker.py +11 -0
- rakam_systems_core/ai_core/interfaces/embedding_model.py +10 -0
- rakam_systems_core/ai_core/interfaces/indexer.py +10 -0
- rakam_systems_core/ai_core/interfaces/llm_gateway.py +139 -0
- rakam_systems_core/ai_core/interfaces/loader.py +86 -0
- rakam_systems_core/ai_core/interfaces/reranker.py +10 -0
- rakam_systems_core/ai_core/interfaces/retriever.py +11 -0
- rakam_systems_core/ai_core/interfaces/tool.py +162 -0
- rakam_systems_core/ai_core/interfaces/tool_invoker.py +260 -0
- rakam_systems_core/ai_core/interfaces/tool_loader.py +374 -0
- rakam_systems_core/ai_core/interfaces/tool_registry.py +287 -0
- rakam_systems_core/ai_core/interfaces/vectorstore.py +37 -0
- rakam_systems_core/ai_core/mcp/README.md +545 -0
- rakam_systems_core/ai_core/mcp/__init__.py +0 -0
- rakam_systems_core/ai_core/mcp/mcp_server.py +334 -0
- rakam_systems_core/ai_core/tracking.py +602 -0
- rakam_systems_core/ai_core/vs_core.py +55 -0
- rakam_systems_core/ai_utils/__init__.py +16 -0
- rakam_systems_core/ai_utils/logging.py +126 -0
- rakam_systems_core/ai_utils/metrics.py +10 -0
- rakam_systems_core/ai_utils/s3.py +480 -0
- rakam_systems_core/ai_utils/tracing.py +5 -0
- rakam_systems_core-0.1.1rc7.dist-info/METADATA +162 -0
- rakam_systems_core-0.1.1rc7.dist-info/RECORD +34 -0
- rakam_systems_core-0.1.1rc7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Loader for automatic tool discovery and registration from configuration.
|
|
3
|
+
Supports loading tools from YAML/JSON config files and Python modules.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
7
|
+
import importlib
|
|
8
|
+
import inspect
|
|
9
|
+
import yaml
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from .tool_registry import ToolRegistry
|
|
13
|
+
from .tool import ToolComponent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolLoadError(Exception):
|
|
17
|
+
"""Base exception for tool loading errors."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolLoader:
|
|
22
|
+
"""
|
|
23
|
+
Load and register tools from configuration files.
|
|
24
|
+
|
|
25
|
+
Supports:
|
|
26
|
+
- YAML and JSON configuration files
|
|
27
|
+
- Auto-discovery of tool functions from Python modules
|
|
28
|
+
- Dynamic import and registration
|
|
29
|
+
- Validation of tool definitions
|
|
30
|
+
|
|
31
|
+
Configuration Format (YAML):
|
|
32
|
+
tools:
|
|
33
|
+
- name: calculate
|
|
34
|
+
type: direct
|
|
35
|
+
module: my_tools.math_tools
|
|
36
|
+
function: calculate
|
|
37
|
+
description: Add two numbers
|
|
38
|
+
category: math
|
|
39
|
+
tags: [arithmetic, basic]
|
|
40
|
+
schema:
|
|
41
|
+
type: object
|
|
42
|
+
properties:
|
|
43
|
+
x:
|
|
44
|
+
type: integer
|
|
45
|
+
description: First number
|
|
46
|
+
y:
|
|
47
|
+
type: integer
|
|
48
|
+
description: Second number
|
|
49
|
+
required: [x, y]
|
|
50
|
+
|
|
51
|
+
- name: web_search
|
|
52
|
+
type: mcp
|
|
53
|
+
mcp_server: search_server
|
|
54
|
+
mcp_tool_name: search
|
|
55
|
+
description: Search the web
|
|
56
|
+
category: search
|
|
57
|
+
tags: [web, external]
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> loader = ToolLoader(registry)
|
|
61
|
+
>>> loader.load_from_yaml("tools.yaml")
|
|
62
|
+
>>> print(f"Loaded {len(registry)} tools")
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, registry: ToolRegistry):
|
|
66
|
+
"""
|
|
67
|
+
Initialize the ToolLoader.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
registry: ToolRegistry instance to register tools into
|
|
71
|
+
"""
|
|
72
|
+
self.registry = registry
|
|
73
|
+
|
|
74
|
+
def load_from_yaml(self, file_path: str) -> int:
|
|
75
|
+
"""
|
|
76
|
+
Load tools from a YAML configuration file.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
file_path: Path to YAML configuration file
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Number of tools loaded
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ToolLoadError: If file cannot be loaded or parsed
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
with open(file_path, 'r') as f:
|
|
89
|
+
config = yaml.safe_load(f)
|
|
90
|
+
return self.load_from_config(config)
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
raise ToolLoadError(f"Configuration file not found: {file_path}")
|
|
93
|
+
except yaml.YAMLError as e:
|
|
94
|
+
raise ToolLoadError(f"Failed to parse YAML file: {str(e)}") from e
|
|
95
|
+
|
|
96
|
+
def load_from_json(self, file_path: str) -> int:
|
|
97
|
+
"""
|
|
98
|
+
Load tools from a JSON configuration file.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
file_path: Path to JSON configuration file
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Number of tools loaded
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ToolLoadError: If file cannot be loaded or parsed
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
with open(file_path, 'r') as f:
|
|
111
|
+
config = json.load(f)
|
|
112
|
+
return self.load_from_config(config)
|
|
113
|
+
except FileNotFoundError:
|
|
114
|
+
raise ToolLoadError(f"Configuration file not found: {file_path}")
|
|
115
|
+
except json.JSONDecodeError as e:
|
|
116
|
+
raise ToolLoadError(f"Failed to parse JSON file: {str(e)}") from e
|
|
117
|
+
|
|
118
|
+
def load_from_config(self, config: Dict[str, Any]) -> int:
|
|
119
|
+
"""
|
|
120
|
+
Load tools from a configuration dictionary.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
config: Configuration dictionary
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Number of tools loaded
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ToolLoadError: If configuration is invalid
|
|
130
|
+
"""
|
|
131
|
+
if not isinstance(config, dict):
|
|
132
|
+
raise ToolLoadError("Configuration must be a dictionary")
|
|
133
|
+
|
|
134
|
+
tools_config = config.get('tools', [])
|
|
135
|
+
if not isinstance(tools_config, list):
|
|
136
|
+
raise ToolLoadError("'tools' must be a list")
|
|
137
|
+
|
|
138
|
+
loaded_count = 0
|
|
139
|
+
errors = []
|
|
140
|
+
|
|
141
|
+
for tool_config in tools_config:
|
|
142
|
+
try:
|
|
143
|
+
self._load_tool(tool_config)
|
|
144
|
+
loaded_count += 1
|
|
145
|
+
except Exception as e:
|
|
146
|
+
tool_name = tool_config.get('name', 'unknown')
|
|
147
|
+
errors.append(f"Failed to load tool '{tool_name}': {str(e)}")
|
|
148
|
+
|
|
149
|
+
if errors:
|
|
150
|
+
error_msg = "\n".join(errors)
|
|
151
|
+
raise ToolLoadError(f"Errors loading tools:\n{error_msg}")
|
|
152
|
+
|
|
153
|
+
return loaded_count
|
|
154
|
+
|
|
155
|
+
def _load_tool(self, tool_config: Dict[str, Any]) -> None:
|
|
156
|
+
"""Load a single tool from configuration."""
|
|
157
|
+
# Validate required fields
|
|
158
|
+
if 'name' not in tool_config:
|
|
159
|
+
raise ToolLoadError("Tool configuration must have 'name' field")
|
|
160
|
+
|
|
161
|
+
name = tool_config['name']
|
|
162
|
+
tool_type = tool_config.get('type', 'direct')
|
|
163
|
+
description = tool_config.get('description', f"Tool: {name}")
|
|
164
|
+
category = tool_config.get('category')
|
|
165
|
+
tags = tool_config.get('tags', [])
|
|
166
|
+
|
|
167
|
+
if tool_type == 'direct':
|
|
168
|
+
self._load_direct_tool(name, tool_config, description, category, tags)
|
|
169
|
+
elif tool_type == 'mcp':
|
|
170
|
+
self._load_mcp_tool(name, tool_config, description, category, tags)
|
|
171
|
+
else:
|
|
172
|
+
raise ToolLoadError(f"Unknown tool type: {tool_type}")
|
|
173
|
+
|
|
174
|
+
def _load_direct_tool(
|
|
175
|
+
self,
|
|
176
|
+
name: str,
|
|
177
|
+
config: Dict[str, Any],
|
|
178
|
+
description: str,
|
|
179
|
+
category: Optional[str],
|
|
180
|
+
tags: List[str]
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Load a direct tool from configuration."""
|
|
183
|
+
# Get module and function name
|
|
184
|
+
module_name = config.get('module')
|
|
185
|
+
function_name = config.get('function')
|
|
186
|
+
|
|
187
|
+
if not module_name or not function_name:
|
|
188
|
+
raise ToolLoadError(
|
|
189
|
+
f"Direct tool '{name}' must specify 'module' and 'function'"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Import the function
|
|
193
|
+
try:
|
|
194
|
+
module = importlib.import_module(module_name)
|
|
195
|
+
function = getattr(module, function_name)
|
|
196
|
+
except ImportError as e:
|
|
197
|
+
raise ToolLoadError(
|
|
198
|
+
f"Failed to import module '{module_name}': {str(e)}"
|
|
199
|
+
) from e
|
|
200
|
+
except AttributeError as e:
|
|
201
|
+
raise ToolLoadError(
|
|
202
|
+
f"Function '{function_name}' not found in module '{module_name}'"
|
|
203
|
+
) from e
|
|
204
|
+
|
|
205
|
+
if not callable(function):
|
|
206
|
+
raise ToolLoadError(
|
|
207
|
+
f"'{module_name}.{function_name}' is not callable"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Get or generate JSON schema
|
|
211
|
+
json_schema = config.get('schema')
|
|
212
|
+
if json_schema is None:
|
|
213
|
+
# Try to auto-generate schema from function signature
|
|
214
|
+
json_schema = self._generate_schema_from_function(function)
|
|
215
|
+
|
|
216
|
+
# Check if function takes context
|
|
217
|
+
takes_ctx = config.get('takes_ctx', False)
|
|
218
|
+
|
|
219
|
+
# Register the tool
|
|
220
|
+
self.registry.register_direct_tool(
|
|
221
|
+
name=name,
|
|
222
|
+
function=function,
|
|
223
|
+
description=description,
|
|
224
|
+
json_schema=json_schema,
|
|
225
|
+
takes_ctx=takes_ctx,
|
|
226
|
+
category=category,
|
|
227
|
+
tags=tags,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _load_mcp_tool(
|
|
231
|
+
self,
|
|
232
|
+
name: str,
|
|
233
|
+
config: Dict[str, Any],
|
|
234
|
+
description: str,
|
|
235
|
+
category: Optional[str],
|
|
236
|
+
tags: List[str]
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Load an MCP tool from configuration."""
|
|
239
|
+
mcp_server = config.get('mcp_server')
|
|
240
|
+
mcp_tool_name = config.get('mcp_tool_name', name)
|
|
241
|
+
|
|
242
|
+
if not mcp_server:
|
|
243
|
+
raise ToolLoadError(f"MCP tool '{name}' must specify 'mcp_server'")
|
|
244
|
+
|
|
245
|
+
# Register the MCP tool
|
|
246
|
+
self.registry.register_mcp_tool(
|
|
247
|
+
name=name,
|
|
248
|
+
mcp_server=mcp_server,
|
|
249
|
+
mcp_tool_name=mcp_tool_name,
|
|
250
|
+
description=description,
|
|
251
|
+
category=category,
|
|
252
|
+
tags=tags,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _generate_schema_from_function(self, function: Callable) -> Dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Auto-generate a JSON schema from function signature.
|
|
258
|
+
|
|
259
|
+
Note: This is a basic implementation. For production use,
|
|
260
|
+
consider using pydantic or similar for better type inference.
|
|
261
|
+
"""
|
|
262
|
+
sig = inspect.signature(function)
|
|
263
|
+
properties = {}
|
|
264
|
+
required = []
|
|
265
|
+
|
|
266
|
+
for param_name, param in sig.parameters.items():
|
|
267
|
+
# Skip self, cls, and context parameters
|
|
268
|
+
if param_name in ('self', 'cls', 'ctx', 'context'):
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
param_schema = {'type': 'string'} # Default type
|
|
272
|
+
|
|
273
|
+
# Try to infer type from annotation
|
|
274
|
+
if param.annotation != inspect.Parameter.empty:
|
|
275
|
+
param_schema['type'] = self._python_type_to_json_type(param.annotation)
|
|
276
|
+
|
|
277
|
+
properties[param_name] = param_schema
|
|
278
|
+
|
|
279
|
+
# Check if parameter is required
|
|
280
|
+
if param.default == inspect.Parameter.empty:
|
|
281
|
+
required.append(param_name)
|
|
282
|
+
|
|
283
|
+
schema = {
|
|
284
|
+
'type': 'object',
|
|
285
|
+
'properties': properties,
|
|
286
|
+
'additionalProperties': False,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if required:
|
|
290
|
+
schema['required'] = required
|
|
291
|
+
|
|
292
|
+
return schema
|
|
293
|
+
|
|
294
|
+
def _python_type_to_json_type(self, python_type: Any) -> str:
|
|
295
|
+
"""Convert Python type annotation to JSON schema type."""
|
|
296
|
+
type_mapping = {
|
|
297
|
+
int: 'integer',
|
|
298
|
+
float: 'number',
|
|
299
|
+
str: 'string',
|
|
300
|
+
bool: 'boolean',
|
|
301
|
+
list: 'array',
|
|
302
|
+
dict: 'object',
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Handle Optional types
|
|
306
|
+
if hasattr(python_type, '__origin__'):
|
|
307
|
+
origin = python_type.__origin__
|
|
308
|
+
if origin is list:
|
|
309
|
+
return 'array'
|
|
310
|
+
elif origin is dict:
|
|
311
|
+
return 'object'
|
|
312
|
+
|
|
313
|
+
# Direct type mapping
|
|
314
|
+
return type_mapping.get(python_type, 'string')
|
|
315
|
+
|
|
316
|
+
def load_from_directory(
|
|
317
|
+
self,
|
|
318
|
+
directory: str,
|
|
319
|
+
pattern: str = "*.yaml",
|
|
320
|
+
recursive: bool = False
|
|
321
|
+
) -> int:
|
|
322
|
+
"""
|
|
323
|
+
Load tools from all configuration files in a directory.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
directory: Path to directory containing configuration files
|
|
327
|
+
pattern: Glob pattern for configuration files (default: "*.yaml")
|
|
328
|
+
recursive: Whether to search recursively (default: False)
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Total number of tools loaded
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
ToolLoadError: If directory not found or files cannot be loaded
|
|
335
|
+
"""
|
|
336
|
+
path = Path(directory)
|
|
337
|
+
if not path.exists():
|
|
338
|
+
raise ToolLoadError(f"Directory not found: {directory}")
|
|
339
|
+
|
|
340
|
+
if not path.is_dir():
|
|
341
|
+
raise ToolLoadError(f"Not a directory: {directory}")
|
|
342
|
+
|
|
343
|
+
# Find all matching files
|
|
344
|
+
if recursive:
|
|
345
|
+
files = list(path.rglob(pattern))
|
|
346
|
+
else:
|
|
347
|
+
files = list(path.glob(pattern))
|
|
348
|
+
|
|
349
|
+
if not files:
|
|
350
|
+
raise ToolLoadError(
|
|
351
|
+
f"No configuration files found matching '{pattern}' in {directory}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
total_loaded = 0
|
|
355
|
+
errors = []
|
|
356
|
+
|
|
357
|
+
for file_path in files:
|
|
358
|
+
try:
|
|
359
|
+
if file_path.suffix in ['.yaml', '.yml']:
|
|
360
|
+
count = self.load_from_yaml(str(file_path))
|
|
361
|
+
elif file_path.suffix == '.json':
|
|
362
|
+
count = self.load_from_json(str(file_path))
|
|
363
|
+
else:
|
|
364
|
+
continue
|
|
365
|
+
total_loaded += count
|
|
366
|
+
except Exception as e:
|
|
367
|
+
errors.append(f"Error loading {file_path}: {str(e)}")
|
|
368
|
+
|
|
369
|
+
if errors:
|
|
370
|
+
error_msg = "\n".join(errors)
|
|
371
|
+
raise ToolLoadError(f"Errors loading tools:\n{error_msg}")
|
|
372
|
+
|
|
373
|
+
return total_loaded
|
|
374
|
+
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Registry for managing and discovering tools across the system.
|
|
3
|
+
Provides a centralized registry for both direct tool invocation and MCP-based tools.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Dict, List, Optional, Any, Callable, Union
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
import inspect
|
|
9
|
+
from .tool import ToolComponent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolMode:
|
|
13
|
+
"""Constants for tool invocation modes."""
|
|
14
|
+
DIRECT = "direct"
|
|
15
|
+
MCP = "mcp"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ToolMetadata:
|
|
19
|
+
"""Metadata for a registered tool."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
name: str,
|
|
24
|
+
description: str,
|
|
25
|
+
mode: str,
|
|
26
|
+
tool_instance: Optional[ToolComponent] = None,
|
|
27
|
+
mcp_server: Optional[str] = None,
|
|
28
|
+
mcp_tool_name: Optional[str] = None,
|
|
29
|
+
category: Optional[str] = None,
|
|
30
|
+
tags: Optional[List[str]] = None,
|
|
31
|
+
):
|
|
32
|
+
self.name = name
|
|
33
|
+
self.description = description
|
|
34
|
+
self.mode = mode
|
|
35
|
+
self.tool_instance = tool_instance
|
|
36
|
+
self.mcp_server = mcp_server
|
|
37
|
+
self.mcp_tool_name = mcp_tool_name
|
|
38
|
+
self.category = category or "general"
|
|
39
|
+
self.tags = tags or []
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
42
|
+
"""Convert metadata to dictionary format."""
|
|
43
|
+
return {
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"description": self.description,
|
|
46
|
+
"mode": self.mode,
|
|
47
|
+
"mcp_server": self.mcp_server,
|
|
48
|
+
"mcp_tool_name": self.mcp_tool_name,
|
|
49
|
+
"category": self.category,
|
|
50
|
+
"tags": self.tags,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ToolRegistry:
|
|
55
|
+
"""
|
|
56
|
+
Central registry for managing tools in the system.
|
|
57
|
+
|
|
58
|
+
Supports:
|
|
59
|
+
- Direct tool registration (callable functions or Tool instances)
|
|
60
|
+
- MCP-based tool registration (tools exposed via MCP servers)
|
|
61
|
+
- Tool discovery and lookup by name, category, or tags
|
|
62
|
+
- Automatic tool conversion for different agent frameworks
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> registry = ToolRegistry()
|
|
66
|
+
>>>
|
|
67
|
+
>>> # Register a direct tool
|
|
68
|
+
>>> def calculate(x: int, y: int) -> int:
|
|
69
|
+
... return x + y
|
|
70
|
+
>>> registry.register_direct_tool(
|
|
71
|
+
... name="calculate",
|
|
72
|
+
... function=calculate,
|
|
73
|
+
... description="Add two numbers",
|
|
74
|
+
... json_schema={...}
|
|
75
|
+
... )
|
|
76
|
+
>>>
|
|
77
|
+
>>> # Register an MCP tool
|
|
78
|
+
>>> registry.register_mcp_tool(
|
|
79
|
+
... name="search",
|
|
80
|
+
... mcp_server="search_server",
|
|
81
|
+
... mcp_tool_name="web_search",
|
|
82
|
+
... description="Search the web"
|
|
83
|
+
... )
|
|
84
|
+
>>>
|
|
85
|
+
>>> # Get all tools
|
|
86
|
+
>>> tools = registry.get_all_tools()
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self._tools: Dict[str, ToolMetadata] = {}
|
|
91
|
+
self._categories: Dict[str, List[str]] = {}
|
|
92
|
+
self._tags: Dict[str, List[str]] = {}
|
|
93
|
+
|
|
94
|
+
def register_direct_tool(
|
|
95
|
+
self,
|
|
96
|
+
name: str,
|
|
97
|
+
function: Callable[..., Any],
|
|
98
|
+
description: str,
|
|
99
|
+
json_schema: Dict[str, Any],
|
|
100
|
+
takes_ctx: bool = False,
|
|
101
|
+
category: Optional[str] = None,
|
|
102
|
+
tags: Optional[List[str]] = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Register a tool for direct invocation.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name: Unique name for the tool
|
|
109
|
+
function: The callable function
|
|
110
|
+
description: Human-readable description
|
|
111
|
+
json_schema: JSON schema for the tool parameters
|
|
112
|
+
takes_ctx: Whether the tool takes context as first argument
|
|
113
|
+
category: Optional category for organizing tools
|
|
114
|
+
tags: Optional tags for filtering tools
|
|
115
|
+
"""
|
|
116
|
+
# Create a ToolComponent from the function
|
|
117
|
+
tool = ToolComponent.from_function(
|
|
118
|
+
function=function,
|
|
119
|
+
name=name,
|
|
120
|
+
description=description,
|
|
121
|
+
json_schema=json_schema,
|
|
122
|
+
takes_ctx=takes_ctx,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
metadata = ToolMetadata(
|
|
126
|
+
name=name,
|
|
127
|
+
description=description,
|
|
128
|
+
mode=ToolMode.DIRECT,
|
|
129
|
+
tool_instance=tool,
|
|
130
|
+
category=category,
|
|
131
|
+
tags=tags,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self._register_metadata(metadata)
|
|
135
|
+
|
|
136
|
+
def register_tool_instance(
|
|
137
|
+
self,
|
|
138
|
+
tool: ToolComponent,
|
|
139
|
+
category: Optional[str] = None,
|
|
140
|
+
tags: Optional[List[str]] = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Register an existing ToolComponent instance.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
tool: ToolComponent instance
|
|
147
|
+
category: Optional category for organizing tools
|
|
148
|
+
tags: Optional tags for filtering tools
|
|
149
|
+
"""
|
|
150
|
+
if not isinstance(tool, ToolComponent):
|
|
151
|
+
raise ValueError(f"Unsupported tool type: {type(tool)}. Expected ToolComponent.")
|
|
152
|
+
|
|
153
|
+
metadata = ToolMetadata(
|
|
154
|
+
name=tool.name,
|
|
155
|
+
description=tool.description,
|
|
156
|
+
mode=ToolMode.DIRECT,
|
|
157
|
+
tool_instance=tool,
|
|
158
|
+
category=category,
|
|
159
|
+
tags=tags,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._register_metadata(metadata)
|
|
163
|
+
|
|
164
|
+
def register_mcp_tool(
|
|
165
|
+
self,
|
|
166
|
+
name: str,
|
|
167
|
+
mcp_server: str,
|
|
168
|
+
mcp_tool_name: str,
|
|
169
|
+
description: str,
|
|
170
|
+
category: Optional[str] = None,
|
|
171
|
+
tags: Optional[List[str]] = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Register a tool that will be invoked via MCP server.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name: Unique name for the tool in this registry
|
|
178
|
+
mcp_server: Name of the MCP server hosting the tool
|
|
179
|
+
mcp_tool_name: Name of the tool on the MCP server
|
|
180
|
+
description: Human-readable description
|
|
181
|
+
category: Optional category for organizing tools
|
|
182
|
+
tags: Optional tags for filtering tools
|
|
183
|
+
"""
|
|
184
|
+
metadata = ToolMetadata(
|
|
185
|
+
name=name,
|
|
186
|
+
description=description,
|
|
187
|
+
mode=ToolMode.MCP,
|
|
188
|
+
mcp_server=mcp_server,
|
|
189
|
+
mcp_tool_name=mcp_tool_name,
|
|
190
|
+
category=category,
|
|
191
|
+
tags=tags,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self._register_metadata(metadata)
|
|
195
|
+
|
|
196
|
+
def _register_metadata(self, metadata: ToolMetadata) -> None:
|
|
197
|
+
"""Internal method to register tool metadata and update indices."""
|
|
198
|
+
if metadata.name in self._tools:
|
|
199
|
+
raise ValueError(f"Tool '{metadata.name}' is already registered")
|
|
200
|
+
|
|
201
|
+
self._tools[metadata.name] = metadata
|
|
202
|
+
|
|
203
|
+
# Update category index
|
|
204
|
+
if metadata.category not in self._categories:
|
|
205
|
+
self._categories[metadata.category] = []
|
|
206
|
+
self._categories[metadata.category].append(metadata.name)
|
|
207
|
+
|
|
208
|
+
# Update tag index
|
|
209
|
+
for tag in metadata.tags:
|
|
210
|
+
if tag not in self._tags:
|
|
211
|
+
self._tags[tag] = []
|
|
212
|
+
self._tags[tag].append(metadata.name)
|
|
213
|
+
|
|
214
|
+
def get_tool(self, name: str) -> Optional[ToolMetadata]:
|
|
215
|
+
"""Get tool metadata by name."""
|
|
216
|
+
return self._tools.get(name)
|
|
217
|
+
|
|
218
|
+
def get_all_tools(self) -> List[ToolMetadata]:
|
|
219
|
+
"""Get all registered tools."""
|
|
220
|
+
return list(self._tools.values())
|
|
221
|
+
|
|
222
|
+
def get_tools_by_category(self, category: str) -> List[ToolMetadata]:
|
|
223
|
+
"""Get all tools in a specific category."""
|
|
224
|
+
tool_names = self._categories.get(category, [])
|
|
225
|
+
return [self._tools[name] for name in tool_names]
|
|
226
|
+
|
|
227
|
+
def get_tools_by_tag(self, tag: str) -> List[ToolMetadata]:
|
|
228
|
+
"""Get all tools with a specific tag."""
|
|
229
|
+
tool_names = self._tags.get(tag, [])
|
|
230
|
+
return [self._tools[name] for name in tool_names]
|
|
231
|
+
|
|
232
|
+
def get_tools_by_mode(self, mode: str) -> List[ToolMetadata]:
|
|
233
|
+
"""Get all tools for a specific invocation mode."""
|
|
234
|
+
return [tool for tool in self._tools.values() if tool.mode == mode]
|
|
235
|
+
|
|
236
|
+
def list_categories(self) -> List[str]:
|
|
237
|
+
"""List all registered categories."""
|
|
238
|
+
return list(self._categories.keys())
|
|
239
|
+
|
|
240
|
+
def list_tags(self) -> List[str]:
|
|
241
|
+
"""List all registered tags."""
|
|
242
|
+
return list(self._tags.keys())
|
|
243
|
+
|
|
244
|
+
def unregister_tool(self, name: str) -> bool:
|
|
245
|
+
"""
|
|
246
|
+
Unregister a tool by name.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if tool was unregistered, False if not found
|
|
250
|
+
"""
|
|
251
|
+
if name not in self._tools:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
metadata = self._tools[name]
|
|
255
|
+
|
|
256
|
+
# Remove from indices
|
|
257
|
+
if metadata.category in self._categories:
|
|
258
|
+
self._categories[metadata.category].remove(name)
|
|
259
|
+
if not self._categories[metadata.category]:
|
|
260
|
+
del self._categories[metadata.category]
|
|
261
|
+
|
|
262
|
+
for tag in metadata.tags:
|
|
263
|
+
if tag in self._tags:
|
|
264
|
+
self._tags[tag].remove(name)
|
|
265
|
+
if not self._tags[tag]:
|
|
266
|
+
del self._tags[tag]
|
|
267
|
+
|
|
268
|
+
del self._tools[name]
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
def clear(self) -> None:
|
|
272
|
+
"""Clear all registered tools."""
|
|
273
|
+
self._tools.clear()
|
|
274
|
+
self._categories.clear()
|
|
275
|
+
self._tags.clear()
|
|
276
|
+
|
|
277
|
+
def __len__(self) -> int:
|
|
278
|
+
"""Return number of registered tools."""
|
|
279
|
+
return len(self._tools)
|
|
280
|
+
|
|
281
|
+
def __contains__(self, name: str) -> bool:
|
|
282
|
+
"""Check if a tool is registered."""
|
|
283
|
+
return name in self._tools
|
|
284
|
+
|
|
285
|
+
def __repr__(self) -> str:
|
|
286
|
+
return f"ToolRegistry(tools={len(self._tools)}, categories={len(self._categories)}, tags={len(self._tags)})"
|
|
287
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from ..base import BaseComponent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VectorStore(BaseComponent, ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def add(self, vectors: List[List[float]], metadatas: List[Dict[str, Any]]) -> Any:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def query(self, vector: List[float], top_k: int = 5) -> List[Dict[str, Any]]:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
def run(self, *args, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Convenience default so subclasses don't *have* to implement run().
|
|
19
|
+
If called with a 'vector' (or first positional arg), proxies to query().
|
|
20
|
+
"""
|
|
21
|
+
vector = kwargs.get("vector")
|
|
22
|
+
if vector is None and args:
|
|
23
|
+
vector = args[0]
|
|
24
|
+
top_k = kwargs.get("top_k", 5)
|
|
25
|
+
if vector is None:
|
|
26
|
+
raise NotImplementedError(
|
|
27
|
+
"VectorStore.run expects a 'vector' argument or an override."
|
|
28
|
+
)
|
|
29
|
+
return self.query(vector, top_k=top_k)
|
|
30
|
+
|
|
31
|
+
def count(self) -> Optional[int]:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# compatibility alias
|
|
36
|
+
VectorStoreComponent = VectorStore
|
|
37
|
+
__all__ = ["VectorStore", "VectorStoreComponent"]
|