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.

Files changed (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {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
- # infinity_embedded not available, functionality will be limited
15
- INFINITY_AVAILABLE = False
16
- infinity_embedded = None # type: ignore
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)