mcp-vector-search 0.12.6__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_vector_search/__init__.py +10 -0
- mcp_vector_search/cli/__init__.py +1 -0
- mcp_vector_search/cli/commands/__init__.py +1 -0
- mcp_vector_search/cli/commands/auto_index.py +397 -0
- mcp_vector_search/cli/commands/config.py +393 -0
- mcp_vector_search/cli/commands/demo.py +358 -0
- mcp_vector_search/cli/commands/index.py +744 -0
- mcp_vector_search/cli/commands/init.py +645 -0
- mcp_vector_search/cli/commands/install.py +675 -0
- mcp_vector_search/cli/commands/install_old.py +696 -0
- mcp_vector_search/cli/commands/mcp.py +1182 -0
- mcp_vector_search/cli/commands/reset.py +393 -0
- mcp_vector_search/cli/commands/search.py +773 -0
- mcp_vector_search/cli/commands/status.py +549 -0
- mcp_vector_search/cli/commands/uninstall.py +485 -0
- mcp_vector_search/cli/commands/visualize.py +1467 -0
- mcp_vector_search/cli/commands/watch.py +287 -0
- mcp_vector_search/cli/didyoumean.py +500 -0
- mcp_vector_search/cli/export.py +320 -0
- mcp_vector_search/cli/history.py +295 -0
- mcp_vector_search/cli/interactive.py +342 -0
- mcp_vector_search/cli/main.py +461 -0
- mcp_vector_search/cli/output.py +412 -0
- mcp_vector_search/cli/suggestions.py +375 -0
- mcp_vector_search/config/__init__.py +1 -0
- mcp_vector_search/config/constants.py +24 -0
- mcp_vector_search/config/defaults.py +200 -0
- mcp_vector_search/config/settings.py +134 -0
- mcp_vector_search/core/__init__.py +1 -0
- mcp_vector_search/core/auto_indexer.py +298 -0
- mcp_vector_search/core/connection_pool.py +360 -0
- mcp_vector_search/core/database.py +1214 -0
- mcp_vector_search/core/directory_index.py +318 -0
- mcp_vector_search/core/embeddings.py +294 -0
- mcp_vector_search/core/exceptions.py +89 -0
- mcp_vector_search/core/factory.py +318 -0
- mcp_vector_search/core/git_hooks.py +345 -0
- mcp_vector_search/core/indexer.py +1002 -0
- mcp_vector_search/core/models.py +294 -0
- mcp_vector_search/core/project.py +333 -0
- mcp_vector_search/core/scheduler.py +330 -0
- mcp_vector_search/core/search.py +952 -0
- mcp_vector_search/core/watcher.py +322 -0
- mcp_vector_search/mcp/__init__.py +5 -0
- mcp_vector_search/mcp/__main__.py +25 -0
- mcp_vector_search/mcp/server.py +733 -0
- mcp_vector_search/parsers/__init__.py +8 -0
- mcp_vector_search/parsers/base.py +296 -0
- mcp_vector_search/parsers/dart.py +605 -0
- mcp_vector_search/parsers/html.py +413 -0
- mcp_vector_search/parsers/javascript.py +643 -0
- mcp_vector_search/parsers/php.py +694 -0
- mcp_vector_search/parsers/python.py +502 -0
- mcp_vector_search/parsers/registry.py +223 -0
- mcp_vector_search/parsers/ruby.py +678 -0
- mcp_vector_search/parsers/text.py +186 -0
- mcp_vector_search/parsers/utils.py +265 -0
- mcp_vector_search/py.typed +1 -0
- mcp_vector_search/utils/__init__.py +40 -0
- mcp_vector_search/utils/gitignore.py +250 -0
- mcp_vector_search/utils/monorepo.py +277 -0
- mcp_vector_search/utils/timing.py +334 -0
- mcp_vector_search/utils/version.py +47 -0
- mcp_vector_search-0.12.6.dist-info/METADATA +754 -0
- mcp_vector_search-0.12.6.dist-info/RECORD +68 -0
- mcp_vector_search-0.12.6.dist-info/WHEEL +4 -0
- mcp_vector_search-0.12.6.dist-info/entry_points.txt +2 -0
- mcp_vector_search-0.12.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Timing utilities for performance measurement and optimization."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import statistics
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TimingResult:
|
|
18
|
+
"""Result of a timing measurement."""
|
|
19
|
+
|
|
20
|
+
operation: str
|
|
21
|
+
duration: float # in seconds
|
|
22
|
+
timestamp: float
|
|
23
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def duration_ms(self) -> float:
|
|
27
|
+
"""Duration in milliseconds."""
|
|
28
|
+
return self.duration * 1000
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def duration_us(self) -> float:
|
|
32
|
+
"""Duration in microseconds."""
|
|
33
|
+
return self.duration * 1_000_000
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PerformanceProfiler:
|
|
37
|
+
"""Performance profiler for measuring and analyzing operation timings."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str = "default"):
|
|
40
|
+
self.name = name
|
|
41
|
+
self.results: list[TimingResult] = []
|
|
42
|
+
self._active_timers: dict[str, float] = {}
|
|
43
|
+
self._nested_level = 0
|
|
44
|
+
|
|
45
|
+
def start_timer(self, operation: str) -> None:
|
|
46
|
+
"""Start timing an operation."""
|
|
47
|
+
if operation in self._active_timers:
|
|
48
|
+
logger.warning(f"Timer '{operation}' already active, overwriting")
|
|
49
|
+
self._active_timers[operation] = time.perf_counter()
|
|
50
|
+
|
|
51
|
+
def stop_timer(
|
|
52
|
+
self, operation: str, metadata: dict[str, Any] | None = None
|
|
53
|
+
) -> TimingResult:
|
|
54
|
+
"""Stop timing an operation and record the result."""
|
|
55
|
+
if operation not in self._active_timers:
|
|
56
|
+
raise ValueError(f"Timer '{operation}' not found or not started")
|
|
57
|
+
|
|
58
|
+
start_time = self._active_timers.pop(operation)
|
|
59
|
+
duration = time.perf_counter() - start_time
|
|
60
|
+
|
|
61
|
+
result = TimingResult(
|
|
62
|
+
operation=operation,
|
|
63
|
+
duration=duration,
|
|
64
|
+
timestamp=time.time(),
|
|
65
|
+
metadata=metadata or {},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.results.append(result)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
@contextmanager
|
|
72
|
+
def time_operation(self, operation: str, metadata: dict[str, Any] | None = None):
|
|
73
|
+
"""Context manager for timing an operation."""
|
|
74
|
+
indent = " " * self._nested_level
|
|
75
|
+
logger.debug(f"{indent}⏱️ Starting: {operation}")
|
|
76
|
+
|
|
77
|
+
self._nested_level += 1
|
|
78
|
+
start_time = time.perf_counter()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
yield
|
|
82
|
+
finally:
|
|
83
|
+
duration = time.perf_counter() - start_time
|
|
84
|
+
self._nested_level -= 1
|
|
85
|
+
|
|
86
|
+
result = TimingResult(
|
|
87
|
+
operation=operation,
|
|
88
|
+
duration=duration,
|
|
89
|
+
timestamp=time.time(),
|
|
90
|
+
metadata=metadata or {},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.results.append(result)
|
|
94
|
+
|
|
95
|
+
indent = " " * self._nested_level
|
|
96
|
+
logger.debug(f"{indent}✅ Completed: {operation} ({duration * 1000:.2f}ms)")
|
|
97
|
+
|
|
98
|
+
@asynccontextmanager
|
|
99
|
+
async def time_async_operation(
|
|
100
|
+
self, operation: str, metadata: dict[str, Any] | None = None
|
|
101
|
+
):
|
|
102
|
+
"""Async context manager for timing an operation."""
|
|
103
|
+
indent = " " * self._nested_level
|
|
104
|
+
logger.debug(f"{indent}⏱️ Starting: {operation}")
|
|
105
|
+
|
|
106
|
+
self._nested_level += 1
|
|
107
|
+
start_time = time.perf_counter()
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
duration = time.perf_counter() - start_time
|
|
113
|
+
self._nested_level -= 1
|
|
114
|
+
|
|
115
|
+
result = TimingResult(
|
|
116
|
+
operation=operation,
|
|
117
|
+
duration=duration,
|
|
118
|
+
timestamp=time.time(),
|
|
119
|
+
metadata=metadata or {},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.results.append(result)
|
|
123
|
+
|
|
124
|
+
indent = " " * self._nested_level
|
|
125
|
+
logger.debug(f"{indent}✅ Completed: {operation} ({duration * 1000:.2f}ms)")
|
|
126
|
+
|
|
127
|
+
def get_stats(self, operation: str | None = None) -> dict[str, Any]:
|
|
128
|
+
"""Get timing statistics for operations."""
|
|
129
|
+
if operation:
|
|
130
|
+
durations = [r.duration for r in self.results if r.operation == operation]
|
|
131
|
+
else:
|
|
132
|
+
durations = [r.duration for r in self.results]
|
|
133
|
+
|
|
134
|
+
if not durations:
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"count": len(durations),
|
|
139
|
+
"total": sum(durations),
|
|
140
|
+
"mean": statistics.mean(durations),
|
|
141
|
+
"median": statistics.median(durations),
|
|
142
|
+
"min": min(durations),
|
|
143
|
+
"max": max(durations),
|
|
144
|
+
"std_dev": statistics.stdev(durations) if len(durations) > 1 else 0.0,
|
|
145
|
+
"p95": statistics.quantiles(durations, n=20)[18]
|
|
146
|
+
if len(durations) >= 20
|
|
147
|
+
else max(durations),
|
|
148
|
+
"p99": statistics.quantiles(durations, n=100)[98]
|
|
149
|
+
if len(durations) >= 100
|
|
150
|
+
else max(durations),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def get_operation_breakdown(self) -> dict[str, dict[str, Any]]:
|
|
154
|
+
"""Get breakdown of all operations."""
|
|
155
|
+
operations = {r.operation for r in self.results}
|
|
156
|
+
return {op: self.get_stats(op) for op in operations}
|
|
157
|
+
|
|
158
|
+
def print_report(self, show_individual: bool = False, min_duration_ms: float = 0.0):
|
|
159
|
+
"""Print a detailed performance report."""
|
|
160
|
+
if not self.results:
|
|
161
|
+
print("No timing results recorded.")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
print(f"\n{'=' * 60}")
|
|
165
|
+
print(f"PERFORMANCE REPORT: {self.name}")
|
|
166
|
+
print(f"{'=' * 60}")
|
|
167
|
+
|
|
168
|
+
# Overall stats
|
|
169
|
+
overall_stats = self.get_stats()
|
|
170
|
+
print("\nOVERALL STATISTICS:")
|
|
171
|
+
print(f" Total operations: {overall_stats['count']}")
|
|
172
|
+
print(f" Total time: {overall_stats['total'] * 1000:.2f}ms")
|
|
173
|
+
print(f" Average: {overall_stats['mean'] * 1000:.2f}ms")
|
|
174
|
+
print(f" Median: {overall_stats['median'] * 1000:.2f}ms")
|
|
175
|
+
print(f" Min: {overall_stats['min'] * 1000:.2f}ms")
|
|
176
|
+
print(f" Max: {overall_stats['max'] * 1000:.2f}ms")
|
|
177
|
+
|
|
178
|
+
# Per-operation breakdown
|
|
179
|
+
breakdown = self.get_operation_breakdown()
|
|
180
|
+
print("\nPER-OPERATION BREAKDOWN:")
|
|
181
|
+
|
|
182
|
+
for operation, stats in sorted(
|
|
183
|
+
breakdown.items(), key=lambda x: x[1]["total"], reverse=True
|
|
184
|
+
):
|
|
185
|
+
print(f"\n {operation}:")
|
|
186
|
+
print(f" Count: {stats['count']}")
|
|
187
|
+
print(
|
|
188
|
+
f" Total: {stats['total'] * 1000:.2f}ms ({stats['total'] / overall_stats['total'] * 100:.1f}%)"
|
|
189
|
+
)
|
|
190
|
+
print(f" Average: {stats['mean'] * 1000:.2f}ms")
|
|
191
|
+
print(
|
|
192
|
+
f" Min/Max: {stats['min'] * 1000:.2f}ms / {stats['max'] * 1000:.2f}ms"
|
|
193
|
+
)
|
|
194
|
+
if stats["count"] > 1:
|
|
195
|
+
print(f" StdDev: {stats['std_dev'] * 1000:.2f}ms")
|
|
196
|
+
|
|
197
|
+
# Individual results if requested
|
|
198
|
+
if show_individual:
|
|
199
|
+
print("\nINDIVIDUAL RESULTS:")
|
|
200
|
+
for result in self.results:
|
|
201
|
+
if result.duration_ms >= min_duration_ms:
|
|
202
|
+
print(f" {result.operation}: {result.duration_ms:.2f}ms")
|
|
203
|
+
if result.metadata:
|
|
204
|
+
print(f" Metadata: {result.metadata}")
|
|
205
|
+
|
|
206
|
+
def save_results(self, file_path: Path):
|
|
207
|
+
"""Save timing results to a JSON file."""
|
|
208
|
+
data = {
|
|
209
|
+
"profiler_name": self.name,
|
|
210
|
+
"timestamp": time.time(),
|
|
211
|
+
"results": [
|
|
212
|
+
{
|
|
213
|
+
"operation": r.operation,
|
|
214
|
+
"duration": r.duration,
|
|
215
|
+
"timestamp": r.timestamp,
|
|
216
|
+
"metadata": r.metadata,
|
|
217
|
+
}
|
|
218
|
+
for r in self.results
|
|
219
|
+
],
|
|
220
|
+
"stats": self.get_operation_breakdown(),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
with open(file_path, "w") as f:
|
|
224
|
+
json.dump(data, f, indent=2)
|
|
225
|
+
|
|
226
|
+
def clear(self):
|
|
227
|
+
"""Clear all timing results."""
|
|
228
|
+
self.results.clear()
|
|
229
|
+
self._active_timers.clear()
|
|
230
|
+
self._nested_level = 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Global profiler instance
|
|
234
|
+
_global_profiler = PerformanceProfiler("global")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def time_function(
|
|
238
|
+
operation_name: str | None = None, metadata: dict[str, Any] | None = None
|
|
239
|
+
):
|
|
240
|
+
"""Decorator for timing function execution."""
|
|
241
|
+
|
|
242
|
+
def decorator(func: Callable) -> Callable:
|
|
243
|
+
name = operation_name or f"{func.__module__}.{func.__name__}"
|
|
244
|
+
|
|
245
|
+
if asyncio.iscoroutinefunction(func):
|
|
246
|
+
|
|
247
|
+
async def async_wrapper(*args, **kwargs):
|
|
248
|
+
async with _global_profiler.time_async_operation(name, metadata):
|
|
249
|
+
return await func(*args, **kwargs)
|
|
250
|
+
|
|
251
|
+
return async_wrapper
|
|
252
|
+
else:
|
|
253
|
+
|
|
254
|
+
def sync_wrapper(*args, **kwargs):
|
|
255
|
+
with _global_profiler.time_operation(name, metadata):
|
|
256
|
+
return func(*args, **kwargs)
|
|
257
|
+
|
|
258
|
+
return sync_wrapper
|
|
259
|
+
|
|
260
|
+
return decorator
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@contextmanager
|
|
264
|
+
def time_block(operation: str, metadata: dict[str, Any] | None = None):
|
|
265
|
+
"""Context manager for timing a block of code using the global profiler."""
|
|
266
|
+
with _global_profiler.time_operation(operation, metadata):
|
|
267
|
+
yield
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@asynccontextmanager
|
|
271
|
+
async def time_async_block(operation: str, metadata: dict[str, Any] | None = None):
|
|
272
|
+
"""Async context manager for timing a block of code using the global profiler."""
|
|
273
|
+
async with _global_profiler.time_async_operation(operation, metadata):
|
|
274
|
+
yield
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_global_profiler() -> PerformanceProfiler:
|
|
278
|
+
"""Get the global profiler instance."""
|
|
279
|
+
return _global_profiler
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def print_global_report(**kwargs):
|
|
283
|
+
"""Print report from the global profiler."""
|
|
284
|
+
_global_profiler.print_report(**kwargs)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def clear_global_profiler():
|
|
288
|
+
"""Clear the global profiler."""
|
|
289
|
+
_global_profiler.clear()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class SearchProfiler(PerformanceProfiler):
|
|
293
|
+
"""Specialized profiler for search operations."""
|
|
294
|
+
|
|
295
|
+
def __init__(self):
|
|
296
|
+
super().__init__("search_profiler")
|
|
297
|
+
|
|
298
|
+
async def profile_search(
|
|
299
|
+
self, search_func: Callable, query: str, **search_kwargs
|
|
300
|
+
) -> tuple[Any, dict[str, float]]:
|
|
301
|
+
"""Profile a complete search operation with detailed breakdown."""
|
|
302
|
+
|
|
303
|
+
async with self.time_async_operation(
|
|
304
|
+
"total_search", {"query": query, "kwargs": search_kwargs}
|
|
305
|
+
):
|
|
306
|
+
# Time the actual search
|
|
307
|
+
async with self.time_async_operation("search_execution", {"query": query}):
|
|
308
|
+
result = await search_func(query, **search_kwargs)
|
|
309
|
+
|
|
310
|
+
# Time result processing if we can measure it
|
|
311
|
+
async with self.time_async_operation(
|
|
312
|
+
"result_processing",
|
|
313
|
+
{"result_count": len(result) if hasattr(result, "__len__") else 0},
|
|
314
|
+
):
|
|
315
|
+
# Simulate any post-processing that might happen
|
|
316
|
+
await asyncio.sleep(0) # Placeholder for actual processing
|
|
317
|
+
|
|
318
|
+
# Return results and timing breakdown
|
|
319
|
+
timing_breakdown = {
|
|
320
|
+
op: self.get_stats(op)["mean"] * 1000 # Convert to ms
|
|
321
|
+
for op in ["total_search", "search_execution", "result_processing"]
|
|
322
|
+
if self.get_stats(op)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return result, timing_breakdown
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Convenience function for quick search profiling
|
|
329
|
+
async def profile_search_operation(
|
|
330
|
+
search_func: Callable, query: str, **kwargs
|
|
331
|
+
) -> tuple[Any, dict[str, float]]:
|
|
332
|
+
"""Quick function to profile a search operation."""
|
|
333
|
+
profiler = SearchProfiler()
|
|
334
|
+
return await profiler.profile_search(search_func, query, **kwargs)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Version utilities for MCP Vector Search.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for accessing and formatting version information.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .. import __author__, __build__, __email__, __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_version_info() -> dict[str, Any]:
|
|
12
|
+
"""Get complete version information.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Dictionary containing version, build, and package metadata
|
|
16
|
+
"""
|
|
17
|
+
return {
|
|
18
|
+
"version": __version__,
|
|
19
|
+
"build": __build__,
|
|
20
|
+
"author": __author__,
|
|
21
|
+
"email": __email__,
|
|
22
|
+
"package": "mcp-vector-search",
|
|
23
|
+
"version_string": f"{__version__} (build {__build__})",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_version_string(include_build: bool = True) -> str:
|
|
28
|
+
"""Get formatted version string.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
include_build: Whether to include build number
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Formatted version string
|
|
35
|
+
"""
|
|
36
|
+
if include_build:
|
|
37
|
+
return f"{__version__} (build {__build__})"
|
|
38
|
+
return __version__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_user_agent() -> str:
|
|
42
|
+
"""Get user agent string for HTTP requests.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
User agent string including version
|
|
46
|
+
"""
|
|
47
|
+
return f"mcp-vector-search/{__version__}"
|