docsif 0.1.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.
- docsif-0.1.0.dist-info/METADATA +393 -0
- docsif-0.1.0.dist-info/RECORD +86 -0
- docsif-0.1.0.dist-info/WHEEL +4 -0
- docsif-0.1.0.dist-info/entry_points.txt +3 -0
- docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
- sif/__init__.py +24 -0
- sif/_version.py +3 -0
- sif/cli/__init__.py +11 -0
- sif/cli/commands/__init__.py +18 -0
- sif/cli/commands/bench.py +148 -0
- sif/cli/commands/collection.py +360 -0
- sif/cli/commands/context.py +154 -0
- sif/cli/commands/get.py +200 -0
- sif/cli/commands/index.py +294 -0
- sif/cli/commands/ls.py +67 -0
- sif/cli/commands/mcp.py +93 -0
- sif/cli/commands/pull.py +111 -0
- sif/cli/commands/search.py +574 -0
- sif/cli/config.py +206 -0
- sif/cli/formatters.py +269 -0
- sif/cli/main.py +205 -0
- sif/config/__init__.py +30 -0
- sif/config/constants.py +52 -0
- sif/config/settings.py +217 -0
- sif/core/__init__.py +21 -0
- sif/core/collection.py +161 -0
- sif/core/context.py +167 -0
- sif/core/document.py +125 -0
- sif/core/models.py +298 -0
- sif/database/__init__.py +23 -0
- sif/database/connection.py +139 -0
- sif/database/database.py +136 -0
- sif/database/migrations.py +187 -0
- sif/database/repositories.py +500 -0
- sif/database/repository.py +171 -0
- sif/database/schema.py +362 -0
- sif/embedding/__init__.py +22 -0
- sif/embedding/cache.py +196 -0
- sif/embedding/embedder.py +328 -0
- sif/embedding/factory.py +92 -0
- sif/embedding/manager.py +200 -0
- sif/embedding/model.py +126 -0
- sif/indexing/__init__.py +21 -0
- sif/indexing/chunker.py +226 -0
- sif/indexing/indexer.py +226 -0
- sif/indexing/parser.py +192 -0
- sif/indexing/scanner.py +148 -0
- sif/indexing/watcher.py +156 -0
- sif/mcp/README.md +258 -0
- sif/mcp/__init__.py +9 -0
- sif/mcp/claude_desktop_config.json +16 -0
- sif/mcp/cli.py +92 -0
- sif/mcp/protocol.py +336 -0
- sif/mcp/server.py +433 -0
- sif/mcp/server_http.py +54 -0
- sif/mcp/test_mcp.py +108 -0
- sif/mcp/tools.py +497 -0
- sif/mcp/transport_http.py +424 -0
- sif/mcp/transport_stdio.py +292 -0
- sif/mcp_server/__init__.py +26 -0
- sif/mcp_server/handlers.py +68 -0
- sif/mcp_server/server.py +58 -0
- sif/mcp_server/tools.py +216 -0
- sif/mcp_server/transport.py +221 -0
- sif/models/__init__.py +70 -0
- sif/models/collection.py +51 -0
- sif/models/context.py +45 -0
- sif/models/document.py +76 -0
- sif/models/download.py +208 -0
- sif/models/embedding.py +71 -0
- sif/models/search.py +92 -0
- sif/search/__init__.py +22 -0
- sif/search/benchmark.py +110 -0
- sif/search/bm25.py +216 -0
- sif/search/expansion.py +157 -0
- sif/search/hybrid.py +323 -0
- sif/search/rerank.py +390 -0
- sif/search/rrf.py +186 -0
- sif/search/snippets.py +130 -0
- sif/search/strategy.py +97 -0
- sif/search/vector.py +177 -0
- sif/utils/__init__.py +26 -0
- sif/utils/logging.py +143 -0
- sif/utils/paths.py +82 -0
- sif/utils/progress.py +83 -0
- sif/utils/text.py +128 -0
sif/mcp/tools.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Tool Implementations
|
|
3
|
+
|
|
4
|
+
This module implements all MCP tools for SIF:
|
|
5
|
+
- query: Hybrid search (FTS + Vector + Reranking)
|
|
6
|
+
- lex_search: BM25 full-text search
|
|
7
|
+
- vec_search: Vector semantic search
|
|
8
|
+
- get: Get document by path or doc_id
|
|
9
|
+
- multi_get: Batch get documents by pattern
|
|
10
|
+
- status: Get index status
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import fnmatch
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any, Optional, Protocol
|
|
17
|
+
|
|
18
|
+
from .protocol import (
|
|
19
|
+
CollectionInfo,
|
|
20
|
+
Document,
|
|
21
|
+
GetInput,
|
|
22
|
+
GetOutput,
|
|
23
|
+
LexSearchInput,
|
|
24
|
+
LexSearchOutput,
|
|
25
|
+
MCPTool,
|
|
26
|
+
MultiGetInput,
|
|
27
|
+
MultiGetOutput,
|
|
28
|
+
QueryInput,
|
|
29
|
+
QueryOutput,
|
|
30
|
+
SearchResult,
|
|
31
|
+
StatusOutput,
|
|
32
|
+
ToolInputSchema,
|
|
33
|
+
VecSearchInput,
|
|
34
|
+
VecSearchOutput,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# Search Backend Protocol
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SearchBackend(Protocol):
|
|
47
|
+
"""Protocol for search backend implementations."""
|
|
48
|
+
|
|
49
|
+
async def hybrid_search(
|
|
50
|
+
self,
|
|
51
|
+
query: str,
|
|
52
|
+
collections: Optional[list[str]] = None,
|
|
53
|
+
limit: int = 10,
|
|
54
|
+
min_score: float = 0.0,
|
|
55
|
+
) -> list[SearchResult]:
|
|
56
|
+
"""Perform hybrid search (FTS + Vector + Reranking)."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
async def lex_search(
|
|
60
|
+
self, query: str, collections: Optional[list[str]] = None, limit: int = 10
|
|
61
|
+
) -> list[SearchResult]:
|
|
62
|
+
"""Perform BM25 full-text search."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
async def vec_search(
|
|
66
|
+
self, query: str, collections: Optional[list[str]] = None, limit: int = 10
|
|
67
|
+
) -> list[SearchResult]:
|
|
68
|
+
"""Perform vector semantic search."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
async def get_document(
|
|
72
|
+
self, path_or_docid: str, from_line: Optional[int] = None, max_lines: Optional[int] = None
|
|
73
|
+
) -> Optional[Document]:
|
|
74
|
+
"""Get a document by path or doc_id."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
async def get_documents_by_pattern(
|
|
78
|
+
self, pattern: str, max_bytes: Optional[int] = None
|
|
79
|
+
) -> tuple[list[Document], list[str]]:
|
|
80
|
+
"""Get documents matching a pattern."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
async def get_status(self) -> tuple[list[CollectionInfo], int]:
|
|
84
|
+
"""Get index status."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# Mock Search Backend (for testing/development)
|
|
90
|
+
# ============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MockSearchBackend:
|
|
94
|
+
"""Mock search backend for testing and development."""
|
|
95
|
+
|
|
96
|
+
def __init__(self):
|
|
97
|
+
self._documents: dict[str, Document] = {}
|
|
98
|
+
self._collections: dict[str, CollectionInfo] = {}
|
|
99
|
+
self._init_mock_data()
|
|
100
|
+
|
|
101
|
+
def _init_mock_data(self):
|
|
102
|
+
"""Initialize mock data."""
|
|
103
|
+
# Add some mock documents
|
|
104
|
+
for i in range(10):
|
|
105
|
+
doc = Document(
|
|
106
|
+
doc_id=f"doc_{i}",
|
|
107
|
+
path=f"/docs/document_{i}.md",
|
|
108
|
+
title=f"Document {i}",
|
|
109
|
+
content=(
|
|
110
|
+
f"This is the content of document {i}. "
|
|
111
|
+
f"It contains information about various topics."
|
|
112
|
+
),
|
|
113
|
+
metadata={"category": "general", "index": i},
|
|
114
|
+
)
|
|
115
|
+
self._documents[doc.doc_id] = doc
|
|
116
|
+
self._documents[doc.path] = doc
|
|
117
|
+
|
|
118
|
+
# Add mock collections
|
|
119
|
+
self._collections["default"] = CollectionInfo(
|
|
120
|
+
name="default",
|
|
121
|
+
document_count=10,
|
|
122
|
+
last_updated=datetime.now().isoformat(),
|
|
123
|
+
size_bytes=102400,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def hybrid_search(
|
|
127
|
+
self,
|
|
128
|
+
query: str,
|
|
129
|
+
collections: Optional[list[str]] = None,
|
|
130
|
+
limit: int = 10,
|
|
131
|
+
min_score: float = 0.0,
|
|
132
|
+
) -> list[SearchResult]:
|
|
133
|
+
"""Mock hybrid search."""
|
|
134
|
+
logger.info(f"Hybrid search: query='{query}', limit={limit}")
|
|
135
|
+
results = []
|
|
136
|
+
query_lower = query.lower()
|
|
137
|
+
|
|
138
|
+
for doc in list(self._documents.values())[:limit]:
|
|
139
|
+
score = 0.5
|
|
140
|
+
if query_lower in doc.content.lower():
|
|
141
|
+
score = 0.9
|
|
142
|
+
if score >= min_score:
|
|
143
|
+
results.append(
|
|
144
|
+
SearchResult(
|
|
145
|
+
doc_id=doc.doc_id,
|
|
146
|
+
path=doc.path,
|
|
147
|
+
title=doc.title,
|
|
148
|
+
content=doc.content[:200] + "..."
|
|
149
|
+
if len(doc.content) > 200
|
|
150
|
+
else doc.content,
|
|
151
|
+
score=score,
|
|
152
|
+
highlights=[f"Match found in {doc.title}"],
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return sorted(results, key=lambda x: x.score, reverse=True)[:limit]
|
|
157
|
+
|
|
158
|
+
async def lex_search(
|
|
159
|
+
self, query: str, collections: Optional[list[str]] = None, limit: int = 10
|
|
160
|
+
) -> list[SearchResult]:
|
|
161
|
+
"""Mock BM25 search."""
|
|
162
|
+
logger.info(f"BM25 search: query='{query}', limit={limit}")
|
|
163
|
+
results = []
|
|
164
|
+
query_lower = query.lower()
|
|
165
|
+
|
|
166
|
+
for doc in list(self._documents.values())[:limit]:
|
|
167
|
+
score = 0.3
|
|
168
|
+
if query_lower in doc.content.lower():
|
|
169
|
+
score = 0.8
|
|
170
|
+
results.append(
|
|
171
|
+
SearchResult(
|
|
172
|
+
doc_id=doc.doc_id,
|
|
173
|
+
path=doc.path,
|
|
174
|
+
title=doc.title,
|
|
175
|
+
content=doc.content[:200] + "..." if len(doc.content) > 200 else doc.content,
|
|
176
|
+
score=score,
|
|
177
|
+
highlights=[f"BM25 match in {doc.title}"],
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return sorted(results, key=lambda x: x.score, reverse=True)[:limit]
|
|
182
|
+
|
|
183
|
+
async def vec_search(
|
|
184
|
+
self, query: str, collections: Optional[list[str]] = None, limit: int = 10
|
|
185
|
+
) -> list[SearchResult]:
|
|
186
|
+
"""Mock vector search."""
|
|
187
|
+
logger.info(f"Vector search: query='{query}', limit={limit}")
|
|
188
|
+
results = []
|
|
189
|
+
|
|
190
|
+
for doc in list(self._documents.values())[:limit]:
|
|
191
|
+
results.append(
|
|
192
|
+
SearchResult(
|
|
193
|
+
doc_id=doc.doc_id,
|
|
194
|
+
path=doc.path,
|
|
195
|
+
title=doc.title,
|
|
196
|
+
content=doc.content[:200] + "..." if len(doc.content) > 200 else doc.content,
|
|
197
|
+
score=0.7, # Mock semantic similarity
|
|
198
|
+
highlights=[f"Semantic match in {doc.title}"],
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return sorted(results, key=lambda x: x.score, reverse=True)[:limit]
|
|
203
|
+
|
|
204
|
+
async def get_document(
|
|
205
|
+
self, path_or_docid: str, from_line: Optional[int] = None, max_lines: Optional[int] = None
|
|
206
|
+
) -> Optional[Document]:
|
|
207
|
+
"""Get a document by path or doc_id."""
|
|
208
|
+
logger.info(f"Get document: path_or_docid='{path_or_docid}'")
|
|
209
|
+
doc = self._documents.get(path_or_docid)
|
|
210
|
+
|
|
211
|
+
if doc and (from_line is not None or max_lines is not None):
|
|
212
|
+
# Simulate line-based extraction
|
|
213
|
+
lines = doc.content.split("\n")
|
|
214
|
+
start = from_line or 0
|
|
215
|
+
end = start + max_lines if max_lines else len(lines)
|
|
216
|
+
content = "\n".join(lines[start:end])
|
|
217
|
+
return Document(
|
|
218
|
+
doc_id=doc.doc_id,
|
|
219
|
+
path=doc.path,
|
|
220
|
+
title=doc.title,
|
|
221
|
+
content=content,
|
|
222
|
+
metadata=doc.metadata,
|
|
223
|
+
line_start=start,
|
|
224
|
+
line_end=end,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return doc
|
|
228
|
+
|
|
229
|
+
async def get_documents_by_pattern(
|
|
230
|
+
self, pattern: str, max_bytes: Optional[int] = None
|
|
231
|
+
) -> tuple[list[Document], list[str]]:
|
|
232
|
+
"""Get documents matching a pattern."""
|
|
233
|
+
logger.info(f"Multi-get: pattern='{pattern}', max_bytes={max_bytes}")
|
|
234
|
+
documents = []
|
|
235
|
+
errors = []
|
|
236
|
+
total_bytes = 0
|
|
237
|
+
|
|
238
|
+
for _doc_id, doc in self._documents.items():
|
|
239
|
+
if fnmatch.fnmatch(doc.path, pattern) or fnmatch.fnmatch(doc.doc_id, pattern):
|
|
240
|
+
doc_bytes = len(doc.content.encode("utf-8"))
|
|
241
|
+
|
|
242
|
+
if max_bytes and total_bytes + doc_bytes > max_bytes:
|
|
243
|
+
errors.append(f"Max bytes limit reached, skipping {doc.path}")
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
documents.append(doc)
|
|
247
|
+
total_bytes += doc_bytes
|
|
248
|
+
|
|
249
|
+
return documents, errors
|
|
250
|
+
|
|
251
|
+
async def get_status(self) -> tuple[list[CollectionInfo], int]:
|
|
252
|
+
"""Get index status."""
|
|
253
|
+
logger.info("Get status")
|
|
254
|
+
total_docs = len(set(d.doc_id for d in self._documents.values()))
|
|
255
|
+
return list(self._collections.values()), total_docs
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ============================================================================
|
|
259
|
+
# Tool Registry
|
|
260
|
+
# ============================================================================
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ToolRegistry:
|
|
264
|
+
"""Registry for MCP tools."""
|
|
265
|
+
|
|
266
|
+
def __init__(self, backend: Optional[SearchBackend] = None) -> None:
|
|
267
|
+
self.backend = backend or MockSearchBackend()
|
|
268
|
+
self._tools: dict[str, MCPTool] = {}
|
|
269
|
+
self._handlers: dict[str, callable] = {}
|
|
270
|
+
self._register_tools()
|
|
271
|
+
|
|
272
|
+
def _register_tools(self):
|
|
273
|
+
"""Register all MCP tools."""
|
|
274
|
+
# query - Hybrid search
|
|
275
|
+
self._tools["query"] = MCPTool(
|
|
276
|
+
name="query",
|
|
277
|
+
description=(
|
|
278
|
+
"Hybrid search combining full-text search (BM25), "
|
|
279
|
+
"vector semantic search, and reranking for the most relevant results."
|
|
280
|
+
),
|
|
281
|
+
inputSchema=ToolInputSchema(
|
|
282
|
+
type="object",
|
|
283
|
+
properties={
|
|
284
|
+
"query": {"type": "string", "description": "The search query string"},
|
|
285
|
+
"collections": {
|
|
286
|
+
"type": "array",
|
|
287
|
+
"items": {"type": "string"},
|
|
288
|
+
"description": "Optional list of collection names to search",
|
|
289
|
+
},
|
|
290
|
+
"limit": {
|
|
291
|
+
"type": "integer",
|
|
292
|
+
"description": "Maximum number of results to return (default: 10)",
|
|
293
|
+
"default": 10,
|
|
294
|
+
},
|
|
295
|
+
"min_score": {
|
|
296
|
+
"type": "number",
|
|
297
|
+
"description": "Minimum relevance score threshold (default: 0.0)",
|
|
298
|
+
"default": 0.0,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
required=["query"],
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
self._handlers["query"] = self._handle_query
|
|
305
|
+
|
|
306
|
+
# lex_search - BM25 search
|
|
307
|
+
self._tools["lex_search"] = MCPTool(
|
|
308
|
+
name="lex_search",
|
|
309
|
+
description="BM25 full-text search for keyword-based document retrieval.",
|
|
310
|
+
inputSchema=ToolInputSchema(
|
|
311
|
+
type="object",
|
|
312
|
+
properties={
|
|
313
|
+
"query": {"type": "string", "description": "The search query string"},
|
|
314
|
+
"collections": {
|
|
315
|
+
"type": "array",
|
|
316
|
+
"items": {"type": "string"},
|
|
317
|
+
"description": "Optional list of collection names to search",
|
|
318
|
+
},
|
|
319
|
+
"limit": {
|
|
320
|
+
"type": "integer",
|
|
321
|
+
"description": "Maximum number of results to return (default: 10)",
|
|
322
|
+
"default": 10,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
required=["query"],
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
self._handlers["lex_search"] = self._handle_lex_search
|
|
329
|
+
|
|
330
|
+
# vec_search - Vector search
|
|
331
|
+
self._tools["vec_search"] = MCPTool(
|
|
332
|
+
name="vec_search",
|
|
333
|
+
description="Vector semantic search for finding documents with similar meaning.",
|
|
334
|
+
inputSchema=ToolInputSchema(
|
|
335
|
+
type="object",
|
|
336
|
+
properties={
|
|
337
|
+
"query": {"type": "string", "description": "The search query string"},
|
|
338
|
+
"collections": {
|
|
339
|
+
"type": "array",
|
|
340
|
+
"items": {"type": "string"},
|
|
341
|
+
"description": "Optional list of collection names to search",
|
|
342
|
+
},
|
|
343
|
+
"limit": {
|
|
344
|
+
"type": "integer",
|
|
345
|
+
"description": "Maximum number of results to return (default: 10)",
|
|
346
|
+
"default": 10,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
required=["query"],
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
self._handlers["vec_search"] = self._handle_vec_search
|
|
353
|
+
|
|
354
|
+
# get - Get document
|
|
355
|
+
self._tools["get"] = MCPTool(
|
|
356
|
+
name="get",
|
|
357
|
+
description="Get a document by its path or document ID, with optional line range.",
|
|
358
|
+
inputSchema=ToolInputSchema(
|
|
359
|
+
type="object",
|
|
360
|
+
properties={
|
|
361
|
+
"path_or_docid": {
|
|
362
|
+
"type": "string",
|
|
363
|
+
"description": "Document path or document ID",
|
|
364
|
+
},
|
|
365
|
+
"from_line": {
|
|
366
|
+
"type": "integer",
|
|
367
|
+
"description": "Optional starting line number (0-indexed)",
|
|
368
|
+
},
|
|
369
|
+
"max_lines": {
|
|
370
|
+
"type": "integer",
|
|
371
|
+
"description": "Optional maximum number of lines to return",
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
required=["path_or_docid"],
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
self._handlers["get"] = self._handle_get
|
|
378
|
+
|
|
379
|
+
# multi_get - Batch get documents
|
|
380
|
+
self._tools["multi_get"] = MCPTool(
|
|
381
|
+
name="multi_get",
|
|
382
|
+
description="Batch get documents matching a glob pattern.",
|
|
383
|
+
inputSchema=ToolInputSchema(
|
|
384
|
+
type="object",
|
|
385
|
+
properties={
|
|
386
|
+
"pattern": {
|
|
387
|
+
"type": "string",
|
|
388
|
+
"description": "Glob pattern to match document paths or IDs (e.g., '/docs/*.md', 'doc_*')",
|
|
389
|
+
},
|
|
390
|
+
"max_bytes": {
|
|
391
|
+
"type": "integer",
|
|
392
|
+
"description": "Optional maximum total bytes to return",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
required=["pattern"],
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
self._handlers["multi_get"] = self._handle_multi_get
|
|
399
|
+
|
|
400
|
+
# status - Get index status
|
|
401
|
+
self._tools["status"] = MCPTool(
|
|
402
|
+
name="status",
|
|
403
|
+
description="Get the current status of all document collections.",
|
|
404
|
+
inputSchema=ToolInputSchema(type="object", properties={}, required=[]),
|
|
405
|
+
)
|
|
406
|
+
self._handlers["status"] = self._handle_status
|
|
407
|
+
|
|
408
|
+
def list_tools(self) -> list[MCPTool]:
|
|
409
|
+
"""List all registered tools."""
|
|
410
|
+
return list(self._tools.values())
|
|
411
|
+
|
|
412
|
+
def get_tool(self, name: str) -> Optional[MCPTool]:
|
|
413
|
+
"""Get a tool by name."""
|
|
414
|
+
return self._tools.get(name)
|
|
415
|
+
|
|
416
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
417
|
+
"""Call a tool by name with arguments."""
|
|
418
|
+
handler = self._handlers.get(name)
|
|
419
|
+
if not handler:
|
|
420
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
421
|
+
|
|
422
|
+
return await handler(arguments)
|
|
423
|
+
|
|
424
|
+
# -------------------------------------------------------------------------
|
|
425
|
+
# Tool Handlers
|
|
426
|
+
# -------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
async def _handle_query(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
429
|
+
"""Handle query tool."""
|
|
430
|
+
input_data = QueryInput(**arguments)
|
|
431
|
+
results = await self.backend.hybrid_search(
|
|
432
|
+
query=input_data.query,
|
|
433
|
+
collections=input_data.collections,
|
|
434
|
+
limit=input_data.limit,
|
|
435
|
+
min_score=input_data.min_score,
|
|
436
|
+
)
|
|
437
|
+
return QueryOutput(results=results).model_dump()
|
|
438
|
+
|
|
439
|
+
async def _handle_lex_search(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
440
|
+
"""Handle lex_search tool."""
|
|
441
|
+
input_data = LexSearchInput(**arguments)
|
|
442
|
+
results = await self.backend.lex_search(
|
|
443
|
+
query=input_data.query, collections=input_data.collections, limit=input_data.limit
|
|
444
|
+
)
|
|
445
|
+
return LexSearchOutput(results=results).model_dump()
|
|
446
|
+
|
|
447
|
+
async def _handle_vec_search(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
448
|
+
"""Handle vec_search tool."""
|
|
449
|
+
input_data = VecSearchInput(**arguments)
|
|
450
|
+
results = await self.backend.vec_search(
|
|
451
|
+
query=input_data.query, collections=input_data.collections, limit=input_data.limit
|
|
452
|
+
)
|
|
453
|
+
return VecSearchOutput(results=results).model_dump()
|
|
454
|
+
|
|
455
|
+
async def _handle_get(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
456
|
+
"""Handle get tool."""
|
|
457
|
+
input_data = GetInput(**arguments)
|
|
458
|
+
document = await self.backend.get_document(
|
|
459
|
+
path_or_docid=input_data.path_or_docid,
|
|
460
|
+
from_line=input_data.from_line,
|
|
461
|
+
max_lines=input_data.max_lines,
|
|
462
|
+
)
|
|
463
|
+
return GetOutput(document=document).model_dump()
|
|
464
|
+
|
|
465
|
+
async def _handle_multi_get(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
466
|
+
"""Handle multi_get tool."""
|
|
467
|
+
input_data = MultiGetInput(**arguments)
|
|
468
|
+
documents, errors = await self.backend.get_documents_by_pattern(
|
|
469
|
+
pattern=input_data.pattern, max_bytes=input_data.max_bytes
|
|
470
|
+
)
|
|
471
|
+
return MultiGetOutput(documents=documents, errors=errors).model_dump()
|
|
472
|
+
|
|
473
|
+
async def _handle_status(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
474
|
+
"""Handle status tool."""
|
|
475
|
+
collections, total = await self.backend.get_status()
|
|
476
|
+
return StatusOutput(collections=collections, total_documents=total).model_dump()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# ============================================================================
|
|
480
|
+
# Global Tool Registry Instance
|
|
481
|
+
# ============================================================================
|
|
482
|
+
|
|
483
|
+
_tool_registry: Optional[ToolRegistry] = None
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def get_tool_registry(backend: Optional[SearchBackend] = None) -> ToolRegistry:
|
|
487
|
+
"""Get or create the global tool registry."""
|
|
488
|
+
global _tool_registry
|
|
489
|
+
if _tool_registry is None:
|
|
490
|
+
_tool_registry = ToolRegistry(backend)
|
|
491
|
+
return _tool_registry
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def set_tool_registry(registry: ToolRegistry):
|
|
495
|
+
"""Set the global tool registry."""
|
|
496
|
+
global _tool_registry
|
|
497
|
+
_tool_registry = registry
|