hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.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.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +4 -17
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +8 -17
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +2 -4
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +6 -7
- hanzo_mcp/tools/__init__.py +29 -32
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +23 -17
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +76 -75
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +7 -19
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +3 -5
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +33 -40
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +7 -19
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +27 -81
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +11 -30
- hanzo_mcp/tools/vector/mock_infinity.py +159 -0
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -723
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -153,16 +153,12 @@ class GitIngester:
|
|
|
153
153
|
)
|
|
154
154
|
return result.stdout.strip()
|
|
155
155
|
|
|
156
|
-
def _get_repository_files(
|
|
157
|
-
self, repo_path: Path, patterns: Optional[List[str]] = None
|
|
158
|
-
) -> List[Path]:
|
|
156
|
+
def _get_repository_files(self, repo_path: Path, patterns: Optional[List[str]] = None) -> List[Path]:
|
|
159
157
|
"""Get list of files in repository matching patterns."""
|
|
160
158
|
# Use git ls-files to respect .gitignore
|
|
161
159
|
cmd = ["git", "ls-files"]
|
|
162
160
|
|
|
163
|
-
result = subprocess.run(
|
|
164
|
-
cmd, cwd=repo_path, capture_output=True, text=True, check=True
|
|
165
|
-
)
|
|
161
|
+
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True, check=True)
|
|
166
162
|
|
|
167
163
|
files = []
|
|
168
164
|
for line in result.stdout.strip().split("\n"):
|
|
@@ -178,9 +174,7 @@ class GitIngester:
|
|
|
178
174
|
|
|
179
175
|
return files
|
|
180
176
|
|
|
181
|
-
def _get_commit_history(
|
|
182
|
-
self, repo_path: Path, branch: str = "HEAD", max_commits: int = 1000
|
|
183
|
-
) -> List[GitCommit]:
|
|
177
|
+
def _get_commit_history(self, repo_path: Path, branch: str = "HEAD", max_commits: int = 1000) -> List[GitCommit]:
|
|
184
178
|
"""Get commit history for the repository."""
|
|
185
179
|
# Get commit list with basic info
|
|
186
180
|
result = subprocess.run(
|
|
@@ -222,9 +216,7 @@ class GitIngester:
|
|
|
222
216
|
|
|
223
217
|
return commits
|
|
224
218
|
|
|
225
|
-
def _get_commit_files(
|
|
226
|
-
self, repo_path: Path, commit_hash: str
|
|
227
|
-
) -> List[Dict[str, str]]:
|
|
219
|
+
def _get_commit_files(self, repo_path: Path, commit_hash: str) -> List[Dict[str, str]]:
|
|
228
220
|
"""Get list of files changed in a commit."""
|
|
229
221
|
result = subprocess.run(
|
|
230
222
|
["git", "show", "--name-status", "--format=", commit_hash],
|
|
@@ -275,21 +267,15 @@ class GitIngester:
|
|
|
275
267
|
if history:
|
|
276
268
|
metadata["first_commit"] = history[-1]["hash"]
|
|
277
269
|
metadata["last_commit"] = history[0]["hash"]
|
|
278
|
-
metadata["last_modified"] = datetime.fromtimestamp(
|
|
279
|
-
history[0]["timestamp"]
|
|
280
|
-
).isoformat()
|
|
270
|
+
metadata["last_modified"] = datetime.fromtimestamp(history[0]["timestamp"]).isoformat()
|
|
281
271
|
|
|
282
272
|
# Add blame information if requested
|
|
283
273
|
if include_blame:
|
|
284
274
|
blame_data = self._get_file_blame(repo_path, relative_path)
|
|
285
|
-
metadata["unique_authors"] = len(
|
|
286
|
-
set(b["author"] for b in blame_data.values())
|
|
287
|
-
)
|
|
275
|
+
metadata["unique_authors"] = len(set(b["author"] for b in blame_data.values()))
|
|
288
276
|
|
|
289
277
|
# Index the file content
|
|
290
|
-
doc_ids = self.vector_store.add_file(
|
|
291
|
-
str(file_path), chunk_size=1000, chunk_overlap=200, metadata=metadata
|
|
292
|
-
)
|
|
278
|
+
doc_ids = self.vector_store.add_file(str(file_path), chunk_size=1000, chunk_overlap=200, metadata=metadata)
|
|
293
279
|
results["files_indexed"] += 1
|
|
294
280
|
|
|
295
281
|
# Perform AST analysis for supported languages
|
|
@@ -309,9 +295,7 @@ class GitIngester:
|
|
|
309
295
|
except Exception as e:
|
|
310
296
|
logger.warning(f"AST analysis failed for {file_path}: {e}")
|
|
311
297
|
|
|
312
|
-
def _get_file_history(
|
|
313
|
-
self, repo_path: Path, file_path: Path
|
|
314
|
-
) -> List[Dict[str, Any]]:
|
|
298
|
+
def _get_file_history(self, repo_path: Path, file_path: Path) -> List[Dict[str, Any]]:
|
|
315
299
|
"""Get commit history for a specific file."""
|
|
316
300
|
result = subprocess.run(
|
|
317
301
|
[
|
|
@@ -346,9 +330,7 @@ class GitIngester:
|
|
|
346
330
|
|
|
347
331
|
return history
|
|
348
332
|
|
|
349
|
-
def _get_file_blame(
|
|
350
|
-
self, repo_path: Path, file_path: Path
|
|
351
|
-
) -> Dict[int, Dict[str, Any]]:
|
|
333
|
+
def _get_file_blame(self, repo_path: Path, file_path: Path) -> Dict[int, Dict[str, Any]]:
|
|
352
334
|
"""Get blame information for a file."""
|
|
353
335
|
result = subprocess.run(
|
|
354
336
|
["git", "blame", "--line-porcelain", "--", str(file_path)],
|
|
@@ -448,9 +430,7 @@ Message: {commit.message}
|
|
|
448
430
|
text=True,
|
|
449
431
|
)
|
|
450
432
|
|
|
451
|
-
remote_url = (
|
|
452
|
-
remote_result.stdout.strip() if remote_result.returncode == 0 else None
|
|
453
|
-
)
|
|
433
|
+
remote_url = remote_result.stdout.strip() if remote_result.returncode == 0 else None
|
|
454
434
|
|
|
455
435
|
# Create repository summary document
|
|
456
436
|
repo_doc = f"""Repository: {repo_path.name}
|
|
@@ -151,13 +151,9 @@ Usage:
|
|
|
151
151
|
if not force:
|
|
152
152
|
stats = await vector_store.get_stats()
|
|
153
153
|
if stats and stats.get("document_count", 0) > 0:
|
|
154
|
-
await tool_ctx.info(
|
|
155
|
-
"Project already indexed, use --force to re-index"
|
|
156
|
-
)
|
|
154
|
+
await tool_ctx.info("Project already indexed, use --force to re-index")
|
|
157
155
|
if show_stats:
|
|
158
|
-
return self._format_stats(
|
|
159
|
-
stats, abs_path, time.time() - start_time
|
|
160
|
-
)
|
|
156
|
+
return self._format_stats(stats, abs_path, time.time() - start_time)
|
|
161
157
|
return "Project is already indexed. Use --force to re-index."
|
|
162
158
|
|
|
163
159
|
# Prepare file patterns
|
|
@@ -268,9 +264,7 @@ Usage:
|
|
|
268
264
|
except Exception as e:
|
|
269
265
|
errors.append(f"{file_path}: {str(e)}")
|
|
270
266
|
|
|
271
|
-
await tool_ctx.info(
|
|
272
|
-
f"Indexed {indexed_files} files ({total_size / 1024 / 1024:.1f} MB)"
|
|
273
|
-
)
|
|
267
|
+
await tool_ctx.info(f"Indexed {indexed_files} files ({total_size / 1024 / 1024:.1f} MB)")
|
|
274
268
|
|
|
275
269
|
# Index git history if requested
|
|
276
270
|
git_stats = {}
|
|
@@ -11,9 +11,10 @@ try:
|
|
|
11
11
|
|
|
12
12
|
INFINITY_AVAILABLE = True
|
|
13
13
|
except ImportError:
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
# Use mock implementation when infinity_embedded is not available
|
|
15
|
+
from . import mock_infinity as infinity_embedded
|
|
16
|
+
|
|
17
|
+
INFINITY_AVAILABLE = True # Mock is always available
|
|
17
18
|
|
|
18
19
|
from .ast_analyzer import Symbol, FileAST, ASTAnalyzer, create_symbol_embedding_text
|
|
19
20
|
|
|
@@ -78,9 +79,7 @@ class InfinityVectorStore:
|
|
|
78
79
|
dimension: Vector dimension (must match embedding model)
|
|
79
80
|
"""
|
|
80
81
|
if not INFINITY_AVAILABLE:
|
|
81
|
-
raise ImportError(
|
|
82
|
-
"infinity_embedded is required for vector store functionality"
|
|
83
|
-
)
|
|
82
|
+
raise ImportError("infinity_embedded is required for vector store functionality")
|
|
84
83
|
|
|
85
84
|
# Set up data path
|
|
86
85
|
if data_path:
|
|
@@ -171,17 +170,13 @@ class InfinityVectorStore:
|
|
|
171
170
|
"source_file": {"type": "varchar"},
|
|
172
171
|
"target_file": {"type": "varchar"},
|
|
173
172
|
"symbol_name": {"type": "varchar"},
|
|
174
|
-
"reference_type": {
|
|
175
|
-
"type": "varchar"
|
|
176
|
-
}, # import, call, inheritance, etc.
|
|
173
|
+
"reference_type": {"type": "varchar"}, # import, call, inheritance, etc.
|
|
177
174
|
"line_number": {"type": "integer"},
|
|
178
175
|
"metadata": {"type": "varchar"}, # JSON string
|
|
179
176
|
},
|
|
180
177
|
)
|
|
181
178
|
|
|
182
|
-
def _generate_doc_id(
|
|
183
|
-
self, content: str, file_path: str = "", chunk_index: int = 0
|
|
184
|
-
) -> str:
|
|
179
|
+
def _generate_doc_id(self, content: str, file_path: str = "", chunk_index: int = 0) -> str:
|
|
185
180
|
"""Generate a unique document ID."""
|
|
186
181
|
content_hash = hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
187
182
|
path_hash = hashlib.sha256(file_path.encode()).hexdigest()[:8]
|
|
@@ -533,11 +528,7 @@ class InfinityVectorStore:
|
|
|
533
528
|
FileAST object if file found, None otherwise
|
|
534
529
|
"""
|
|
535
530
|
try:
|
|
536
|
-
results = (
|
|
537
|
-
self.ast_table.output(["*"])
|
|
538
|
-
.filter(f"file_path = '{file_path}'")
|
|
539
|
-
.to_pl()
|
|
540
|
-
)
|
|
531
|
+
results = self.ast_table.output(["*"]).filter(f"file_path = '{file_path}'").to_pl()
|
|
541
532
|
|
|
542
533
|
if len(results) == 0:
|
|
543
534
|
return None
|
|
@@ -576,11 +567,7 @@ class InfinityVectorStore:
|
|
|
576
567
|
List of reference information
|
|
577
568
|
"""
|
|
578
569
|
try:
|
|
579
|
-
results = (
|
|
580
|
-
self.references_table.output(["*"])
|
|
581
|
-
.filter(f"target_file = '{file_path}'")
|
|
582
|
-
.to_pl()
|
|
583
|
-
)
|
|
570
|
+
results = self.references_table.output(["*"]).filter(f"target_file = '{file_path}'").to_pl()
|
|
584
571
|
|
|
585
572
|
references = []
|
|
586
573
|
for row in results.iter_rows(named=True):
|
|
@@ -695,11 +682,7 @@ class InfinityVectorStore:
|
|
|
695
682
|
"""
|
|
696
683
|
try:
|
|
697
684
|
# Get count first
|
|
698
|
-
results = (
|
|
699
|
-
self.documents_table.output(["id"])
|
|
700
|
-
.filter(f"file_path = '{file_path}'")
|
|
701
|
-
.to_pl()
|
|
702
|
-
)
|
|
685
|
+
results = self.documents_table.output(["id"]).filter(f"file_path = '{file_path}'").to_pl()
|
|
703
686
|
count = len(results)
|
|
704
687
|
|
|
705
688
|
# Delete all documents for this file
|
|
@@ -725,9 +708,7 @@ class InfinityVectorStore:
|
|
|
725
708
|
metadata = json.loads(row["metadata"])
|
|
726
709
|
files[file_path] = {
|
|
727
710
|
"file_path": file_path,
|
|
728
|
-
"file_name": metadata.get(
|
|
729
|
-
"file_name", Path(file_path).name
|
|
730
|
-
),
|
|
711
|
+
"file_name": metadata.get("file_name", Path(file_path).name),
|
|
731
712
|
"file_size": metadata.get("file_size", 0),
|
|
732
713
|
"total_chunks": metadata.get("total_chunks", 1),
|
|
733
714
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Mock implementation of infinity_embedded for testing on unsupported platforms."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MockTable:
|
|
9
|
+
"""Mock implementation of an Infinity table."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: str, schema: Dict[str, Any]):
|
|
12
|
+
self.name = name
|
|
13
|
+
self.schema = schema
|
|
14
|
+
self.data = []
|
|
15
|
+
self._id_counter = 0
|
|
16
|
+
|
|
17
|
+
def insert(self, records: List[Dict[str, Any]]):
|
|
18
|
+
"""Insert records into the table."""
|
|
19
|
+
for record in records:
|
|
20
|
+
# Add an internal ID if not present
|
|
21
|
+
if "id" not in record:
|
|
22
|
+
record["_internal_id"] = self._id_counter
|
|
23
|
+
self._id_counter += 1
|
|
24
|
+
self.data.append(record)
|
|
25
|
+
|
|
26
|
+
def delete(self, condition: str):
|
|
27
|
+
"""Delete records matching condition."""
|
|
28
|
+
# Simple implementation - just clear for now
|
|
29
|
+
self.data = [r for r in self.data if not self._eval_condition(r, condition)]
|
|
30
|
+
|
|
31
|
+
def output(self, columns: List[str]):
|
|
32
|
+
"""Start a query chain."""
|
|
33
|
+
return MockQuery(self, columns)
|
|
34
|
+
|
|
35
|
+
def _eval_condition(self, record: Dict[str, Any], condition: str) -> bool:
|
|
36
|
+
"""Evaluate a simple condition."""
|
|
37
|
+
# Very basic implementation
|
|
38
|
+
if "=" in condition:
|
|
39
|
+
field, value = condition.split("=", 1)
|
|
40
|
+
field = field.strip()
|
|
41
|
+
value = value.strip().strip("'\"")
|
|
42
|
+
return str(record.get(field, "")) == value
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MockQuery:
|
|
47
|
+
"""Mock query builder."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, table: MockTable, columns: List[str]):
|
|
50
|
+
self.table = table
|
|
51
|
+
self.columns = columns
|
|
52
|
+
self.filters = []
|
|
53
|
+
self.vector_search = None
|
|
54
|
+
self.limit_value = None
|
|
55
|
+
|
|
56
|
+
def filter(self, condition: str):
|
|
57
|
+
"""Add a filter condition."""
|
|
58
|
+
self.filters.append(condition)
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def match_dense(self, column: str, vector: List[float], dtype: str, metric: str, limit: int):
|
|
62
|
+
"""Add vector search."""
|
|
63
|
+
self.vector_search = {
|
|
64
|
+
"column": column,
|
|
65
|
+
"vector": vector,
|
|
66
|
+
"dtype": dtype,
|
|
67
|
+
"metric": metric,
|
|
68
|
+
"limit": limit,
|
|
69
|
+
}
|
|
70
|
+
self.limit_value = limit
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def to_pl(self):
|
|
74
|
+
"""Execute query and return polars-like result."""
|
|
75
|
+
results = self.table.data.copy()
|
|
76
|
+
|
|
77
|
+
# Apply filters
|
|
78
|
+
for condition in self.filters:
|
|
79
|
+
results = [r for r in results if self.table._eval_condition(r, condition)]
|
|
80
|
+
|
|
81
|
+
# Apply vector search (mock similarity)
|
|
82
|
+
if self.vector_search:
|
|
83
|
+
# Add mock scores
|
|
84
|
+
for r in results:
|
|
85
|
+
r["score"] = random.uniform(0.5, 1.0)
|
|
86
|
+
# Sort by score
|
|
87
|
+
results.sort(key=lambda x: x.get("score", 0), reverse=True)
|
|
88
|
+
# Limit results
|
|
89
|
+
if self.limit_value:
|
|
90
|
+
results = results[: self.limit_value]
|
|
91
|
+
|
|
92
|
+
# Return mock polars DataFrame
|
|
93
|
+
return MockDataFrame(results)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MockDataFrame:
|
|
97
|
+
"""Mock polars DataFrame."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, data: List[Dict[str, Any]]):
|
|
100
|
+
self.data = data
|
|
101
|
+
|
|
102
|
+
def __len__(self):
|
|
103
|
+
return len(self.data)
|
|
104
|
+
|
|
105
|
+
def iter_rows(self, named: bool = False):
|
|
106
|
+
"""Iterate over rows."""
|
|
107
|
+
if named:
|
|
108
|
+
return iter(self.data)
|
|
109
|
+
else:
|
|
110
|
+
# Return tuples
|
|
111
|
+
if not self.data:
|
|
112
|
+
return iter([])
|
|
113
|
+
keys = list(self.data[0].keys())
|
|
114
|
+
return iter([tuple(row.get(k) for k in keys) for row in self.data])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class MockDatabase:
|
|
118
|
+
"""Mock implementation of an Infinity database."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, name: str):
|
|
121
|
+
self.name = name
|
|
122
|
+
self.tables = {}
|
|
123
|
+
|
|
124
|
+
def create_table(self, name: str, schema: Dict[str, Any]) -> MockTable:
|
|
125
|
+
"""Create a new table."""
|
|
126
|
+
table = MockTable(name, schema)
|
|
127
|
+
self.tables[name] = table
|
|
128
|
+
return table
|
|
129
|
+
|
|
130
|
+
def get_table(self, name: str) -> MockTable:
|
|
131
|
+
"""Get an existing table."""
|
|
132
|
+
if name not in self.tables:
|
|
133
|
+
raise KeyError(f"Table {name} not found")
|
|
134
|
+
return self.tables[name]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class MockInfinity:
|
|
138
|
+
"""Mock implementation of Infinity connection."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, path: str):
|
|
141
|
+
self.path = Path(path)
|
|
142
|
+
self.databases = {}
|
|
143
|
+
# Ensure directory exists
|
|
144
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
def get_database(self, name: str) -> MockDatabase:
|
|
147
|
+
"""Get or create a database."""
|
|
148
|
+
if name not in self.databases:
|
|
149
|
+
self.databases[name] = MockDatabase(name)
|
|
150
|
+
return self.databases[name]
|
|
151
|
+
|
|
152
|
+
def disconnect(self):
|
|
153
|
+
"""Disconnect from Infinity."""
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def connect(path: str) -> MockInfinity:
|
|
158
|
+
"""Connect to Infinity (mock implementation)."""
|
|
159
|
+
return MockInfinity(path)
|