mcp-code-indexer 3.1.3__py3-none-any.whl → 3.1.5__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.
- mcp_code_indexer/__init__.py +8 -6
- mcp_code_indexer/ask_handler.py +105 -75
- mcp_code_indexer/claude_api_handler.py +125 -82
- mcp_code_indexer/cleanup_manager.py +107 -81
- mcp_code_indexer/database/connection_health.py +212 -161
- mcp_code_indexer/database/database.py +529 -415
- mcp_code_indexer/database/exceptions.py +167 -118
- mcp_code_indexer/database/models.py +54 -19
- mcp_code_indexer/database/retry_executor.py +139 -103
- mcp_code_indexer/deepask_handler.py +178 -140
- mcp_code_indexer/error_handler.py +88 -76
- mcp_code_indexer/file_scanner.py +163 -141
- mcp_code_indexer/git_hook_handler.py +352 -261
- mcp_code_indexer/logging_config.py +76 -94
- mcp_code_indexer/main.py +406 -320
- mcp_code_indexer/middleware/error_middleware.py +106 -71
- mcp_code_indexer/query_preprocessor.py +40 -40
- mcp_code_indexer/server/mcp_server.py +785 -469
- mcp_code_indexer/token_counter.py +54 -47
- {mcp_code_indexer-3.1.3.dist-info → mcp_code_indexer-3.1.5.dist-info}/METADATA +3 -3
- mcp_code_indexer-3.1.5.dist-info/RECORD +37 -0
- mcp_code_indexer-3.1.3.dist-info/RECORD +0 -37
- {mcp_code_indexer-3.1.3.dist-info → mcp_code_indexer-3.1.5.dist-info}/WHEEL +0 -0
- {mcp_code_indexer-3.1.3.dist-info → mcp_code_indexer-3.1.5.dist-info}/entry_points.txt +0 -0
- {mcp_code_indexer-3.1.3.dist-info → mcp_code_indexer-3.1.5.dist-info}/licenses/LICENSE +0 -0
- {mcp_code_indexer-3.1.3.dist-info → mcp_code_indexer-3.1.5.dist-info}/top_level.txt +0 -0
@@ -13,71 +13,82 @@ from typing import Any, Callable, Dict, List
|
|
13
13
|
import aiosqlite
|
14
14
|
from mcp import types
|
15
15
|
|
16
|
-
from mcp_code_indexer.error_handler import ErrorHandler
|
17
|
-
from mcp_code_indexer.logging_config import
|
16
|
+
from mcp_code_indexer.error_handler import ErrorHandler
|
17
|
+
from mcp_code_indexer.logging_config import (
|
18
|
+
get_logger,
|
19
|
+
log_tool_usage,
|
20
|
+
log_performance_metrics,
|
21
|
+
)
|
18
22
|
|
19
23
|
logger = get_logger(__name__)
|
20
24
|
|
21
25
|
|
22
26
|
class ToolMiddleware:
|
23
27
|
"""Middleware for MCP tool error handling and logging."""
|
24
|
-
|
28
|
+
|
25
29
|
def __init__(self, error_handler: ErrorHandler):
|
26
30
|
"""Initialize middleware with error handler."""
|
27
31
|
self.error_handler = error_handler
|
28
|
-
|
32
|
+
|
29
33
|
def wrap_tool_handler(self, tool_name: str):
|
30
34
|
"""
|
31
35
|
Decorator to wrap tool handlers with error handling and logging.
|
32
|
-
|
36
|
+
|
33
37
|
Args:
|
34
38
|
tool_name: Name of the MCP tool
|
35
|
-
|
39
|
+
|
36
40
|
Returns:
|
37
41
|
Decorator function
|
38
42
|
"""
|
43
|
+
|
39
44
|
def decorator(func: Callable) -> Callable:
|
40
45
|
@functools.wraps(func)
|
41
46
|
async def wrapper(arguments: Dict[str, Any]) -> List[types.TextContent]:
|
42
47
|
start_time = time.time()
|
43
48
|
success = False
|
44
49
|
result_size = 0
|
45
|
-
|
50
|
+
|
46
51
|
try:
|
47
52
|
# Log tool invocation
|
48
|
-
logger.info(
|
49
|
-
"
|
50
|
-
|
51
|
-
|
52
|
-
"
|
53
|
+
logger.info(
|
54
|
+
f"Tool {tool_name} called",
|
55
|
+
extra={
|
56
|
+
"structured_data": {
|
57
|
+
"tool_invocation": {
|
58
|
+
"tool_name": tool_name,
|
59
|
+
"arguments_count": len(arguments),
|
60
|
+
}
|
53
61
|
}
|
54
|
-
}
|
55
|
-
|
56
|
-
|
62
|
+
},
|
63
|
+
)
|
64
|
+
|
57
65
|
# Call the actual tool handler
|
58
66
|
result = await func(arguments)
|
59
|
-
|
67
|
+
|
60
68
|
# Calculate result size
|
61
69
|
if isinstance(result, list):
|
62
|
-
result_size = sum(
|
63
|
-
|
70
|
+
result_size = sum(
|
71
|
+
len(item.text) if hasattr(item, "text") else 0
|
72
|
+
for item in result
|
73
|
+
)
|
74
|
+
|
64
75
|
success = True
|
65
76
|
duration = time.time() - start_time
|
66
|
-
|
77
|
+
|
67
78
|
# Log performance metrics
|
68
79
|
log_performance_metrics(
|
69
80
|
logger,
|
70
81
|
f"tool_{tool_name}",
|
71
82
|
duration,
|
72
83
|
result_size=result_size,
|
73
|
-
arguments_count=len(arguments)
|
84
|
+
arguments_count=len(arguments),
|
74
85
|
)
|
75
|
-
|
86
|
+
|
76
87
|
return result
|
77
|
-
|
88
|
+
|
78
89
|
except Exception as e:
|
79
90
|
duration = time.time() - start_time
|
80
|
-
|
91
|
+
|
81
92
|
# Enhanced SQLite error handling
|
82
93
|
if self._is_database_locking_error(e):
|
83
94
|
logger.warning(
|
@@ -88,26 +99,26 @@ class ToolMiddleware:
|
|
88
99
|
"tool_name": tool_name,
|
89
100
|
"error_type": type(e).__name__,
|
90
101
|
"error_message": str(e),
|
91
|
-
"duration": duration
|
102
|
+
"duration": duration,
|
92
103
|
}
|
93
104
|
}
|
94
|
-
}
|
105
|
+
},
|
95
106
|
)
|
96
|
-
|
107
|
+
|
97
108
|
# Log the error
|
98
109
|
self.error_handler.log_error(
|
99
110
|
e,
|
100
111
|
context={"arguments_count": len(arguments)},
|
101
|
-
tool_name=tool_name
|
112
|
+
tool_name=tool_name,
|
102
113
|
)
|
103
|
-
|
114
|
+
|
104
115
|
# Create error response
|
105
116
|
error_response = self.error_handler.create_mcp_error_response(
|
106
117
|
e, tool_name, arguments
|
107
118
|
)
|
108
|
-
|
119
|
+
|
109
120
|
return [error_response]
|
110
|
-
|
121
|
+
|
111
122
|
finally:
|
112
123
|
# Always log tool usage
|
113
124
|
log_tool_usage(
|
@@ -116,58 +127,73 @@ class ToolMiddleware:
|
|
116
127
|
arguments,
|
117
128
|
success,
|
118
129
|
time.time() - start_time,
|
119
|
-
result_size if success else None
|
130
|
+
result_size if success else None,
|
120
131
|
)
|
121
|
-
|
132
|
+
|
122
133
|
return wrapper
|
134
|
+
|
123
135
|
return decorator
|
124
|
-
|
125
|
-
def validate_tool_arguments(
|
136
|
+
|
137
|
+
def validate_tool_arguments(
|
138
|
+
self, required_fields: List[str], optional_fields: List[str] = None
|
139
|
+
):
|
126
140
|
"""
|
127
141
|
Decorator to validate tool arguments.
|
128
|
-
|
142
|
+
|
129
143
|
Args:
|
130
144
|
required_fields: List of required argument names
|
131
145
|
optional_fields: List of optional argument names
|
132
|
-
|
146
|
+
|
133
147
|
Returns:
|
134
148
|
Decorator function
|
135
149
|
"""
|
150
|
+
|
136
151
|
def decorator(func: Callable) -> Callable:
|
137
152
|
@functools.wraps(func)
|
138
153
|
async def wrapper(arguments: Dict[str, Any]) -> Any:
|
139
154
|
from ..error_handler import ValidationError
|
140
|
-
|
155
|
+
|
141
156
|
# Check required fields
|
142
|
-
missing_fields = [
|
157
|
+
missing_fields = [
|
158
|
+
field for field in required_fields if field not in arguments
|
159
|
+
]
|
143
160
|
if missing_fields:
|
144
161
|
raise ValidationError(
|
145
162
|
f"Missing required fields: {', '.join(missing_fields)}",
|
146
|
-
details={
|
163
|
+
details={
|
164
|
+
"missing_fields": missing_fields,
|
165
|
+
"provided_fields": list(arguments.keys()),
|
166
|
+
},
|
147
167
|
)
|
148
|
-
|
168
|
+
|
149
169
|
# Check for unexpected fields if optional_fields is provided
|
150
170
|
if optional_fields is not None:
|
151
171
|
all_fields = set(required_fields + optional_fields)
|
152
|
-
unexpected_fields = [
|
172
|
+
unexpected_fields = [
|
173
|
+
field for field in arguments.keys() if field not in all_fields
|
174
|
+
]
|
153
175
|
if unexpected_fields:
|
154
176
|
raise ValidationError(
|
155
177
|
f"Unexpected fields: {', '.join(unexpected_fields)}",
|
156
|
-
details={
|
178
|
+
details={
|
179
|
+
"unexpected_fields": unexpected_fields,
|
180
|
+
"allowed_fields": list(all_fields),
|
181
|
+
},
|
157
182
|
)
|
158
|
-
|
183
|
+
|
159
184
|
return await func(arguments)
|
160
|
-
|
185
|
+
|
161
186
|
return wrapper
|
187
|
+
|
162
188
|
return decorator
|
163
|
-
|
189
|
+
|
164
190
|
def _is_database_locking_error(self, error: Exception) -> bool:
|
165
191
|
"""
|
166
192
|
Check if an error is related to database locking.
|
167
|
-
|
193
|
+
|
168
194
|
Args:
|
169
195
|
error: Exception to check
|
170
|
-
|
196
|
+
|
171
197
|
Returns:
|
172
198
|
True if this is a database locking error
|
173
199
|
"""
|
@@ -179,74 +205,71 @@ class ToolMiddleware:
|
|
179
205
|
"database is busy",
|
180
206
|
"sqlite_busy",
|
181
207
|
"sqlite_locked",
|
182
|
-
"cannot start a transaction within a transaction"
|
208
|
+
"cannot start a transaction within a transaction",
|
183
209
|
]
|
184
210
|
return any(keyword in error_message for keyword in locking_keywords)
|
185
|
-
|
211
|
+
|
186
212
|
return False
|
187
213
|
|
188
214
|
|
189
215
|
class AsyncTaskManager:
|
190
216
|
"""Manages async tasks with proper error handling."""
|
191
|
-
|
217
|
+
|
192
218
|
def __init__(self, error_handler: ErrorHandler):
|
193
219
|
"""Initialize task manager."""
|
194
220
|
self.error_handler = error_handler
|
195
221
|
self._tasks: List[asyncio.Task] = []
|
196
|
-
|
222
|
+
|
197
223
|
def create_task(self, coro, name: str = None) -> asyncio.Task:
|
198
224
|
"""
|
199
225
|
Create a managed async task.
|
200
|
-
|
226
|
+
|
201
227
|
Args:
|
202
228
|
coro: Coroutine to run
|
203
229
|
name: Optional task name for logging
|
204
|
-
|
230
|
+
|
205
231
|
Returns:
|
206
232
|
Created task
|
207
233
|
"""
|
208
234
|
task = asyncio.create_task(coro, name=name)
|
209
235
|
self._tasks.append(task)
|
210
|
-
|
236
|
+
|
211
237
|
# Add done callback for error handling
|
212
238
|
task.add_done_callback(
|
213
239
|
lambda t: asyncio.create_task(
|
214
240
|
self._handle_task_completion(t, name or "unnamed_task")
|
215
241
|
)
|
216
242
|
)
|
217
|
-
|
243
|
+
|
218
244
|
return task
|
219
|
-
|
245
|
+
|
220
246
|
async def _handle_task_completion(self, task: asyncio.Task, task_name: str) -> None:
|
221
247
|
"""Handle task completion and errors."""
|
222
248
|
try:
|
223
249
|
if task.done() and not task.cancelled():
|
224
250
|
exception = task.exception()
|
225
251
|
if exception:
|
226
|
-
await self.error_handler.handle_async_task_error(
|
227
|
-
task, task_name
|
228
|
-
)
|
252
|
+
await self.error_handler.handle_async_task_error(task, task_name)
|
229
253
|
except Exception as e:
|
230
254
|
logger.error(f"Error handling task completion for {task_name}: {e}")
|
231
255
|
finally:
|
232
256
|
# Remove completed task from tracking
|
233
257
|
if task in self._tasks:
|
234
258
|
self._tasks.remove(task)
|
235
|
-
|
259
|
+
|
236
260
|
async def wait_for_all(self, timeout: float = None) -> None:
|
237
261
|
"""
|
238
262
|
Wait for all managed tasks to complete.
|
239
|
-
|
263
|
+
|
240
264
|
Args:
|
241
265
|
timeout: Maximum time to wait in seconds
|
242
266
|
"""
|
243
267
|
if not self._tasks:
|
244
268
|
return
|
245
|
-
|
269
|
+
|
246
270
|
try:
|
247
271
|
await asyncio.wait_for(
|
248
|
-
asyncio.gather(*self._tasks, return_exceptions=True),
|
249
|
-
timeout=timeout
|
272
|
+
asyncio.gather(*self._tasks, return_exceptions=True), timeout=timeout
|
250
273
|
)
|
251
274
|
except asyncio.TimeoutError:
|
252
275
|
logger.warning(f"Timeout waiting for {len(self._tasks)} tasks")
|
@@ -256,14 +279,14 @@ class AsyncTaskManager:
|
|
256
279
|
task.cancel()
|
257
280
|
except Exception as e:
|
258
281
|
logger.error(f"Error waiting for tasks: {e}")
|
259
|
-
|
282
|
+
|
260
283
|
def cancel_all(self) -> None:
|
261
284
|
"""Cancel all managed tasks."""
|
262
285
|
for task in self._tasks:
|
263
286
|
if not task.done():
|
264
287
|
task.cancel()
|
265
288
|
self._tasks.clear()
|
266
|
-
|
289
|
+
|
267
290
|
@property
|
268
291
|
def active_task_count(self) -> int:
|
269
292
|
"""Get count of active tasks."""
|
@@ -273,10 +296,10 @@ class AsyncTaskManager:
|
|
273
296
|
def create_tool_middleware(error_handler: ErrorHandler) -> ToolMiddleware:
|
274
297
|
"""
|
275
298
|
Create tool middleware instance.
|
276
|
-
|
299
|
+
|
277
300
|
Args:
|
278
301
|
error_handler: Error handler instance
|
279
|
-
|
302
|
+
|
280
303
|
Returns:
|
281
304
|
Configured ToolMiddleware
|
282
305
|
"""
|
@@ -285,43 +308,55 @@ def create_tool_middleware(error_handler: ErrorHandler) -> ToolMiddleware:
|
|
285
308
|
|
286
309
|
# Convenience decorators for common patterns
|
287
310
|
|
311
|
+
|
288
312
|
def require_fields(*required_fields):
|
289
313
|
"""Decorator that requires specific fields in arguments."""
|
314
|
+
|
290
315
|
def decorator(func):
|
291
316
|
@functools.wraps(func)
|
292
317
|
async def wrapper(self, arguments: Dict[str, Any]):
|
293
318
|
from ..error_handler import ValidationError
|
294
|
-
|
319
|
+
|
295
320
|
missing = [field for field in required_fields if field not in arguments]
|
296
321
|
if missing:
|
297
322
|
raise ValidationError(f"Missing required fields: {', '.join(missing)}")
|
298
|
-
|
323
|
+
|
299
324
|
return await func(self, arguments)
|
325
|
+
|
300
326
|
return wrapper
|
327
|
+
|
301
328
|
return decorator
|
302
329
|
|
303
330
|
|
304
331
|
def handle_file_operations(func):
|
305
332
|
"""Decorator for file operation error handling."""
|
333
|
+
|
306
334
|
@functools.wraps(func)
|
307
335
|
async def wrapper(*args, **kwargs):
|
308
336
|
try:
|
309
337
|
return await func(*args, **kwargs)
|
310
338
|
except (FileNotFoundError, PermissionError, OSError) as e:
|
311
339
|
from ..error_handler import FileSystemError
|
340
|
+
|
312
341
|
raise FileSystemError(f"File operation failed: {e}") from e
|
342
|
+
|
313
343
|
return wrapper
|
314
344
|
|
315
345
|
|
316
346
|
def handle_database_operations(func):
|
317
347
|
"""Decorator for database operation error handling."""
|
348
|
+
|
318
349
|
@functools.wraps(func)
|
319
350
|
async def wrapper(*args, **kwargs):
|
320
351
|
try:
|
321
352
|
return await func(*args, **kwargs)
|
322
353
|
except Exception as e:
|
323
|
-
if any(
|
354
|
+
if any(
|
355
|
+
keyword in str(e).lower() for keyword in ["database", "sqlite", "sql"]
|
356
|
+
):
|
324
357
|
from ..error_handler import DatabaseError
|
358
|
+
|
325
359
|
raise DatabaseError(f"Database operation failed: {e}") from e
|
326
360
|
raise
|
361
|
+
|
327
362
|
return wrapper
|
@@ -6,8 +6,10 @@ to enable multi-word search with case insensitive matching, whole word enforceme
|
|
6
6
|
and proper handling of FTS5 operators as literal search terms.
|
7
7
|
|
8
8
|
Key features:
|
9
|
-
- Multi-word queries: "grpc proto" becomes "grpc" AND "proto" for
|
10
|
-
-
|
9
|
+
- Multi-word queries: "grpc proto" becomes "grpc" AND "proto" for
|
10
|
+
order-agnostic matching
|
11
|
+
- FTS5 operator escaping: "AND OR" becomes '"AND" AND "OR"' to treat
|
12
|
+
operators as literals
|
11
13
|
- Whole word matching: prevents partial matches by relying on proper tokenization
|
12
14
|
- Case insensitive: leverages FTS5 default behavior
|
13
15
|
- Special character handling: preserves special characters in quoted terms
|
@@ -23,30 +25,28 @@ logger = logging.getLogger(__name__)
|
|
23
25
|
class QueryPreprocessor:
|
24
26
|
"""
|
25
27
|
Preprocesses user queries for optimal FTS5 search performance.
|
26
|
-
|
28
|
+
|
27
29
|
Handles multi-word queries, operator escaping, and special character preservation
|
28
30
|
while maintaining BM25 ranking performance.
|
29
31
|
"""
|
30
|
-
|
32
|
+
|
31
33
|
# FTS5 operators that need to be escaped when used as literal search terms
|
32
|
-
FTS5_OPERATORS: Set[str] = {
|
33
|
-
|
34
|
-
}
|
35
|
-
|
34
|
+
FTS5_OPERATORS: Set[str] = {"AND", "OR", "NOT", "NEAR"}
|
35
|
+
|
36
36
|
def __init__(self):
|
37
37
|
"""Initialize the query preprocessor."""
|
38
38
|
pass
|
39
|
-
|
39
|
+
|
40
40
|
def preprocess_query(self, query: str) -> str:
|
41
41
|
"""
|
42
42
|
Preprocess a user query for FTS5 search.
|
43
|
-
|
43
|
+
|
44
44
|
Args:
|
45
45
|
query: Raw user query string
|
46
|
-
|
46
|
+
|
47
47
|
Returns:
|
48
48
|
Preprocessed query string optimized for FTS5
|
49
|
-
|
49
|
+
|
50
50
|
Examples:
|
51
51
|
>>> preprocessor = QueryPreprocessor()
|
52
52
|
>>> preprocessor.preprocess_query("grpc proto")
|
@@ -58,74 +58,74 @@ class QueryPreprocessor:
|
|
58
58
|
"""
|
59
59
|
if not query or not query.strip():
|
60
60
|
return ""
|
61
|
-
|
61
|
+
|
62
62
|
# Normalize whitespace
|
63
63
|
query = query.strip()
|
64
|
-
|
64
|
+
|
65
65
|
# Split into terms while preserving quoted phrases
|
66
66
|
terms = self._split_terms(query)
|
67
|
-
|
67
|
+
|
68
68
|
if not terms:
|
69
69
|
return ""
|
70
|
-
|
70
|
+
|
71
71
|
# Process each term: escape operators and add quotes
|
72
72
|
processed_terms = []
|
73
73
|
for term in terms:
|
74
74
|
processed_term = self._process_term(term)
|
75
75
|
if processed_term: # Skip empty terms
|
76
76
|
processed_terms.append(processed_term)
|
77
|
-
|
77
|
+
|
78
78
|
if not processed_terms:
|
79
79
|
return ""
|
80
|
-
|
80
|
+
|
81
81
|
# Join with AND for multi-word matching
|
82
82
|
result = " AND ".join(processed_terms)
|
83
|
-
|
83
|
+
|
84
84
|
logger.debug(f"Preprocessed query: '{query}' -> '{result}'")
|
85
85
|
return result
|
86
|
-
|
86
|
+
|
87
87
|
def _split_terms(self, query: str) -> List[str]:
|
88
88
|
"""
|
89
89
|
Split query into terms while preserving quoted phrases.
|
90
|
-
|
90
|
+
|
91
91
|
Args:
|
92
92
|
query: Input query string
|
93
|
-
|
93
|
+
|
94
94
|
Returns:
|
95
95
|
List of terms and quoted phrases
|
96
|
-
|
96
|
+
|
97
97
|
Examples:
|
98
98
|
'grpc proto' -> ['grpc', 'proto']
|
99
99
|
'config "file system"' -> ['config', '"file system"']
|
100
100
|
'error AND handling' -> ['error', 'AND', 'handling']
|
101
101
|
"""
|
102
102
|
terms = []
|
103
|
-
|
103
|
+
|
104
104
|
# Regex to match quoted phrases or individual words
|
105
105
|
# This pattern captures:
|
106
106
|
# 1. Double-quoted strings (including the quotes)
|
107
107
|
# 2. Single words (sequences of non-whitespace characters)
|
108
108
|
pattern = r'"[^"]*"|\S+'
|
109
|
-
|
109
|
+
|
110
110
|
matches = re.findall(pattern, query)
|
111
|
-
|
111
|
+
|
112
112
|
for match in matches:
|
113
113
|
# Skip empty matches
|
114
114
|
if match.strip():
|
115
115
|
terms.append(match)
|
116
|
-
|
116
|
+
|
117
117
|
return terms
|
118
|
-
|
118
|
+
|
119
119
|
def _process_term(self, term: str) -> str:
|
120
120
|
"""
|
121
121
|
Process a single term: escape operators and ensure proper quoting.
|
122
|
-
|
122
|
+
|
123
123
|
Args:
|
124
124
|
term: Single term or quoted phrase
|
125
|
-
|
125
|
+
|
126
126
|
Returns:
|
127
127
|
Processed term ready for FTS5
|
128
|
-
|
128
|
+
|
129
129
|
Examples:
|
130
130
|
'grpc' -> '"grpc"'
|
131
131
|
'AND' -> '"AND"'
|
@@ -134,31 +134,31 @@ class QueryPreprocessor:
|
|
134
134
|
"""
|
135
135
|
if not term:
|
136
136
|
return ""
|
137
|
-
|
137
|
+
|
138
138
|
# If already quoted, return as-is (user intentional phrase)
|
139
139
|
if term.startswith('"') and term.endswith('"') and len(term) >= 2:
|
140
140
|
return term
|
141
|
-
|
141
|
+
|
142
142
|
# Check if term is an FTS5 operator (case-insensitive)
|
143
143
|
if term.upper() in self.FTS5_OPERATORS:
|
144
144
|
# Escape operator by quoting
|
145
145
|
escaped_term = f'"{term}"'
|
146
146
|
logger.debug(f"Escaped FTS5 operator: '{term}' -> '{escaped_term}'")
|
147
147
|
return escaped_term
|
148
|
-
|
148
|
+
|
149
149
|
# Quote all terms to ensure whole-word matching and handle special characters
|
150
150
|
return f'"{term}"'
|
151
|
-
|
151
|
+
|
152
152
|
def _escape_quotes_in_term(self, term: str) -> str:
|
153
153
|
"""
|
154
154
|
Escape internal quotes in a term for FTS5 compatibility.
|
155
|
-
|
155
|
+
|
156
156
|
Args:
|
157
157
|
term: Term that may contain quotes
|
158
|
-
|
158
|
+
|
159
159
|
Returns:
|
160
160
|
Term with escaped quotes
|
161
|
-
|
161
|
+
|
162
162
|
Examples:
|
163
163
|
'say "hello"' -> 'say ""hello""'
|
164
164
|
"test's file" -> "test's file"
|
@@ -170,10 +170,10 @@ class QueryPreprocessor:
|
|
170
170
|
def preprocess_search_query(query: str) -> str:
|
171
171
|
"""
|
172
172
|
Convenience function for preprocessing search queries.
|
173
|
-
|
173
|
+
|
174
174
|
Args:
|
175
175
|
query: Raw user query
|
176
|
-
|
176
|
+
|
177
177
|
Returns:
|
178
178
|
Preprocessed query ready for FTS5
|
179
179
|
"""
|