mita-code 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.
Files changed (67) hide show
  1. mita/__init__.py +5 -0
  2. mita/__main__.py +5 -0
  3. mita/agent/__init__.py +1 -0
  4. mita/agent/context.py +43 -0
  5. mita/agent/conversation.py +101 -0
  6. mita/agent/loop.py +594 -0
  7. mita/agent/system_prompt.py +75 -0
  8. mita/cli.py +940 -0
  9. mita/config/__init__.py +6 -0
  10. mita/config/defaults.py +41 -0
  11. mita/config/loader.py +53 -0
  12. mita/config/schema.py +131 -0
  13. mita/hooks/__init__.py +1 -0
  14. mita/hooks/manager.py +94 -0
  15. mita/hooks/runner.py +145 -0
  16. mita/index/__init__.py +1 -0
  17. mita/index/embeddings.py +49 -0
  18. mita/index/manager.py +170 -0
  19. mita/index/parser.py +331 -0
  20. mita/index/retriever.py +53 -0
  21. mita/index/store.py +143 -0
  22. mita/llm/__init__.py +1 -0
  23. mita/llm/client.py +86 -0
  24. mita/llm/instructor.py +80 -0
  25. mita/llm/streaming.py +58 -0
  26. mita/memory/__init__.py +6 -0
  27. mita/memory/discovery.py +61 -0
  28. mita/memory/loader.py +76 -0
  29. mita/memory/manager.py +117 -0
  30. mita/models/__init__.py +13 -0
  31. mita/models/hardware.py +289 -0
  32. mita/models/manager.py +268 -0
  33. mita/models/ollama_client.py +104 -0
  34. mita/models/recommender.py +88 -0
  35. mita/models/registry.py +167 -0
  36. mita/models/server.py +262 -0
  37. mita/plugins/__init__.py +1 -0
  38. mita/plugins/client.py +152 -0
  39. mita/plugins/manager.py +210 -0
  40. mita/py.typed +0 -0
  41. mita/skills/__init__.py +1 -0
  42. mita/skills/executor.py +84 -0
  43. mita/skills/loader.py +117 -0
  44. mita/skills/manager.py +129 -0
  45. mita/tools/__init__.py +1 -0
  46. mita/tools/builtins/__init__.py +28 -0
  47. mita/tools/builtins/file_edit.py +71 -0
  48. mita/tools/builtins/file_read.py +74 -0
  49. mita/tools/builtins/file_write.py +42 -0
  50. mita/tools/builtins/git.py +112 -0
  51. mita/tools/builtins/glob_tool.py +67 -0
  52. mita/tools/builtins/grep_tool.py +93 -0
  53. mita/tools/builtins/shell.py +83 -0
  54. mita/tools/executor.py +80 -0
  55. mita/tools/registry.py +69 -0
  56. mita/tools/safety.py +91 -0
  57. mita/tools/schema.py +87 -0
  58. mita/ui/__init__.py +1 -0
  59. mita/ui/display.py +139 -0
  60. mita/ui/repl.py +88 -0
  61. mita/ui/spinner.py +48 -0
  62. mita/ui/theme.py +23 -0
  63. mita_code-0.1.0.dist-info/METADATA +227 -0
  64. mita_code-0.1.0.dist-info/RECORD +67 -0
  65. mita_code-0.1.0.dist-info/WHEEL +4 -0
  66. mita_code-0.1.0.dist-info/entry_points.txt +3 -0
  67. mita_code-0.1.0.dist-info/licenses/LICENSE +201 -0
mita/index/manager.py ADDED
@@ -0,0 +1,170 @@
1
+ """CLI command handlers for codebase indexing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
10
+ from rich.syntax import Syntax
11
+ from rich.table import Table
12
+
13
+ from mita.config.loader import load_config
14
+ from mita.index.embeddings import EmbeddingClient
15
+ from mita.index.parser import parse_codebase
16
+ from mita.index.retriever import Retriever
17
+ from mita.index.store import IndexStore
18
+
19
+ console = Console()
20
+
21
+
22
+ def _get_index_dir() -> Path:
23
+ return Path.cwd() / ".mita" / "index"
24
+
25
+
26
+ async def build_index(force: bool = False, pull_model_fn: object | None = None) -> None:
27
+ """Build the codebase index.
28
+
29
+ Args:
30
+ force: Rebuild even if index already exists.
31
+ pull_model_fn: Async callback to pull the embedding model if missing.
32
+ Signature: async (MitaConfig) -> bool. Called from the CLI layer
33
+ so it can import from mita.models without violating dependency rules.
34
+ """
35
+ config = load_config()
36
+ index_dir = _get_index_dir()
37
+ store = IndexStore(index_dir)
38
+
39
+ # Check if index already exists
40
+ if store.exists() and not force:
41
+ console.print("[yellow]Index already exists. Use --force to rebuild.[/yellow]")
42
+ return
43
+
44
+ # Check embedding model availability
45
+ embedder = EmbeddingClient(config)
46
+ if not await embedder.is_model_available():
47
+ if pull_model_fn is not None:
48
+ if not await pull_model_fn(config): # type: ignore[operator]
49
+ return
50
+ else:
51
+ console.print("[red]Embedding model not available.[/red]")
52
+ return
53
+
54
+ start_time = time.monotonic()
55
+
56
+ # Parse codebase
57
+ root = Path.cwd()
58
+ with Progress(
59
+ SpinnerColumn(),
60
+ TextColumn("[progress.description]{task.description}"),
61
+ console=console,
62
+ ) as progress:
63
+ progress.add_task("Parsing codebase...", total=None)
64
+ chunks = parse_codebase(root, config.index)
65
+
66
+ if not chunks:
67
+ console.print("[yellow]No code chunks found to index.[/yellow]")
68
+ return
69
+
70
+ console.print(f"Found {len(chunks)} chunks from codebase.")
71
+
72
+ # Generate embeddings
73
+ texts = [c.content for c in chunks]
74
+ with Progress(
75
+ SpinnerColumn(),
76
+ TextColumn("[progress.description]{task.description}"),
77
+ BarColumn(),
78
+ TextColumn("{task.completed}/{task.total}"),
79
+ console=console,
80
+ ) as progress:
81
+ task = progress.add_task("Generating embeddings...", total=len(texts))
82
+ embeddings = []
83
+ batch_size = 32
84
+ for i in range(0, len(texts), batch_size):
85
+ batch = texts[i : i + batch_size]
86
+ batch_embeddings = await embedder.embed_texts(batch)
87
+ embeddings.extend(batch_embeddings)
88
+ progress.update(task, completed=min(i + batch_size, len(texts)))
89
+
90
+ # Attach embeddings to chunks
91
+ for chunk, embedding in zip(chunks, embeddings):
92
+ chunk.embedding = embedding
93
+
94
+ # Store in LanceDB
95
+ with Progress(
96
+ SpinnerColumn(),
97
+ TextColumn("[progress.description]{task.description}"),
98
+ console=console,
99
+ ) as progress:
100
+ progress.add_task("Writing index...", total=None)
101
+ await store.create_or_replace(chunks)
102
+
103
+ elapsed = time.monotonic() - start_time
104
+ file_count = len({c.file_path for c in chunks})
105
+ console.print(
106
+ f"[green]Indexed {len(chunks)} chunks from {file_count} files in {elapsed:.1f}s.[/green]"
107
+ )
108
+
109
+
110
+ async def show_index_status() -> None:
111
+ """Show index statistics."""
112
+ config = load_config()
113
+ store = IndexStore(_get_index_dir())
114
+ stats = await store.status()
115
+
116
+ if not stats["exists"]:
117
+ console.print("[yellow]No index found. Run 'mita index build' first.[/yellow]")
118
+ return
119
+
120
+ table = Table(title="Index Status")
121
+ table.add_column("Metric", style="bold")
122
+ table.add_column("Value")
123
+ table.add_row("Chunks", str(stats["chunks"]))
124
+ table.add_row("Files", str(stats["files"]))
125
+ table.add_row("Location", str(_get_index_dir()))
126
+ table.add_row("Embedding model", config.model.embedding)
127
+ console.print(table)
128
+
129
+
130
+ async def search_index(query: str, top_k: int = 10) -> None:
131
+ """Search the index and display results."""
132
+ config = load_config()
133
+ retriever = Retriever(config, index_dir=_get_index_dir())
134
+
135
+ if not retriever.is_available():
136
+ console.print("[yellow]No index found. Run 'mita index build' first.[/yellow]")
137
+ return
138
+
139
+ results = await retriever.retrieve(query, top_k=top_k)
140
+
141
+ if not results:
142
+ console.print("[yellow]No results found.[/yellow]")
143
+ return
144
+
145
+ console.print(f"[bold]Found {len(results)} results:[/bold]\n")
146
+ for i, r in enumerate(results, 1):
147
+ c = r.chunk
148
+ header = f"{i}. {c.file_path}:{c.start_line}-{c.end_line}"
149
+ if c.symbol:
150
+ header += f" ({c.symbol})"
151
+ header += f" [dim]score: {r.score:.3f}[/dim]"
152
+ console.print(header)
153
+ console.print(Syntax(c.content, c.language, line_numbers=True, start_line=c.start_line))
154
+ console.print()
155
+
156
+
157
+ async def clear_index() -> None:
158
+ """Clear the index."""
159
+ store = IndexStore(_get_index_dir())
160
+
161
+ if not store.exists():
162
+ console.print("[yellow]No index found.[/yellow]")
163
+ return
164
+
165
+ confirm = console.input("Delete the index? [y/N] ").strip().lower()
166
+ if confirm in ("y", "yes"):
167
+ await store.clear()
168
+ console.print("[green]Index cleared.[/green]")
169
+ else:
170
+ console.print("Cancelled.")
mita/index/parser.py ADDED
@@ -0,0 +1,331 @@
1
+ """Tree-sitter code parsing and chunking for codebase indexing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fnmatch import fnmatch
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mita.config.schema import IndexSettings
10
+ from mita.index.store import CodeChunk
11
+
12
+ # Extension → tree-sitter language name
13
+ EXTENSION_MAP: dict[str, str] = {
14
+ ".py": "python",
15
+ ".js": "javascript",
16
+ ".jsx": "javascript",
17
+ ".ts": "typescript",
18
+ ".tsx": "tsx",
19
+ ".go": "go",
20
+ ".rs": "rust",
21
+ ".java": "java",
22
+ ".c": "c",
23
+ ".h": "c",
24
+ ".cpp": "cpp",
25
+ ".hpp": "cpp",
26
+ ".cc": "cpp",
27
+ ".rb": "ruby",
28
+ ".php": "php",
29
+ ".swift": "swift",
30
+ ".kt": "kotlin",
31
+ ".scala": "scala",
32
+ ".lua": "lua",
33
+ ".sh": "bash",
34
+ ".bash": "bash",
35
+ ".zsh": "bash",
36
+ ".css": "css",
37
+ ".html": "html",
38
+ ".json": "json",
39
+ ".yaml": "yaml",
40
+ ".yml": "yaml",
41
+ ".toml": "toml",
42
+ }
43
+
44
+ # Node types that represent semantic code units per language
45
+ CHUNK_NODE_TYPES: dict[str, set[str]] = {
46
+ "python": {"function_definition", "class_definition", "decorated_definition"},
47
+ "javascript": {
48
+ "function_declaration",
49
+ "class_declaration",
50
+ "method_definition",
51
+ "export_statement",
52
+ },
53
+ "typescript": {
54
+ "function_declaration",
55
+ "class_declaration",
56
+ "method_definition",
57
+ "export_statement",
58
+ "interface_declaration",
59
+ "type_alias_declaration",
60
+ },
61
+ "tsx": {
62
+ "function_declaration",
63
+ "class_declaration",
64
+ "method_definition",
65
+ "export_statement",
66
+ "interface_declaration",
67
+ "type_alias_declaration",
68
+ },
69
+ "go": {"function_declaration", "method_declaration", "type_declaration"},
70
+ "rust": {"function_item", "impl_item", "struct_item", "enum_item", "trait_item"},
71
+ "java": {"class_declaration", "method_declaration", "interface_declaration"},
72
+ "c": {"function_definition", "struct_specifier"},
73
+ "cpp": {"function_definition", "class_specifier", "struct_specifier"},
74
+ "ruby": {"method", "class", "module"},
75
+ }
76
+
77
+
78
+ def parse_codebase(root: Path, config: IndexSettings) -> list[CodeChunk]:
79
+ """Parse all supported files under root into code chunks."""
80
+ chunks: list[CodeChunk] = []
81
+ for file_path in _discover_files(root, config.exclude_patterns):
82
+ file_chunks = parse_file(file_path, root, config)
83
+ chunks.extend(file_chunks)
84
+ return chunks
85
+
86
+
87
+ def parse_file(file_path: Path, root: Path, config: IndexSettings) -> list[CodeChunk]:
88
+ """Parse a single file into code chunks."""
89
+ if _is_binary(file_path):
90
+ return []
91
+
92
+ try:
93
+ content = file_path.read_text(encoding="utf-8", errors="replace")
94
+ except OSError:
95
+ return []
96
+
97
+ if not content.strip():
98
+ return []
99
+
100
+ rel_path = str(file_path.relative_to(root))
101
+ language = get_language_for_file(file_path)
102
+
103
+ if language and language in CHUNK_NODE_TYPES:
104
+ chunks = _parse_with_treesitter(content, rel_path, language, config)
105
+ if chunks:
106
+ return chunks
107
+
108
+ # Fallback: line-based chunking
109
+ return _chunk_by_lines(content, rel_path, language or "text", config)
110
+
111
+
112
+ def get_language_for_file(path: Path) -> str | None:
113
+ """Map a file extension to a tree-sitter language name."""
114
+ return EXTENSION_MAP.get(path.suffix.lower())
115
+
116
+
117
+ def _discover_files(root: Path, exclude_patterns: list[str]) -> list[Path]:
118
+ """Walk the project and yield non-excluded files."""
119
+ files = []
120
+ for path in sorted(root.rglob("*")):
121
+ if not path.is_file():
122
+ continue
123
+ rel = str(path.relative_to(root))
124
+ if _matches_exclude(rel, exclude_patterns):
125
+ continue
126
+ files.append(path)
127
+ return files
128
+
129
+
130
+ def _matches_exclude(rel_path: str, patterns: list[str]) -> bool:
131
+ """Check if a relative path matches any exclude pattern."""
132
+ for pattern in patterns:
133
+ if fnmatch(rel_path, pattern):
134
+ return True
135
+ # Also check each path component for directory patterns
136
+ parts = rel_path.split("/")
137
+ for i in range(len(parts)):
138
+ partial = "/".join(parts[: i + 1])
139
+ if fnmatch(partial, pattern.rstrip("/")):
140
+ return True
141
+ if fnmatch(partial + "/", pattern):
142
+ return True
143
+ return False
144
+
145
+
146
+ def _is_binary(path: Path) -> bool:
147
+ """Heuristic: file is binary if first 8KB contains null bytes."""
148
+ try:
149
+ with open(path, "rb") as f:
150
+ chunk = f.read(8192)
151
+ return b"\x00" in chunk
152
+ except OSError:
153
+ return True
154
+
155
+
156
+ def _parse_with_treesitter(
157
+ content: str,
158
+ rel_path: str,
159
+ language: str,
160
+ config: IndexSettings,
161
+ ) -> list[CodeChunk]:
162
+ """Parse a file using tree-sitter and extract semantic chunks."""
163
+ try:
164
+ from tree_sitter_language_pack import get_parser
165
+ except ImportError:
166
+ return []
167
+
168
+ try:
169
+ parser = get_parser(language) # type: ignore[arg-type]
170
+ except Exception: # noqa: BLE001
171
+ return []
172
+
173
+ tree = parser.parse(content.encode("utf-8"))
174
+ node_types = CHUNK_NODE_TYPES.get(language, set())
175
+ lines = content.split("\n")
176
+ chunks: list[CodeChunk] = []
177
+
178
+ for node in _walk_top_level(tree.root_node):
179
+ if node.type not in node_types:
180
+ continue
181
+
182
+ start_line = node.start_point[0] + 1 # 1-indexed
183
+ end_line = node.end_point[0] + 1
184
+ node_content = "\n".join(lines[start_line - 1 : end_line])
185
+ symbol = _extract_symbol(node, language)
186
+
187
+ # Split large nodes
188
+ char_budget = config.chunk_size * 4 # ~4 chars per token
189
+ if len(node_content) > char_budget:
190
+ sub_chunks = _split_large_content(
191
+ node_content, start_line, rel_path, language, symbol, config
192
+ )
193
+ chunks.extend(sub_chunks)
194
+ else:
195
+ chunks.append(
196
+ CodeChunk(
197
+ file_path=rel_path,
198
+ start_line=start_line,
199
+ end_line=end_line,
200
+ content=node_content,
201
+ language=language,
202
+ symbol=symbol,
203
+ )
204
+ )
205
+
206
+ return chunks
207
+
208
+
209
+ def _walk_top_level(node: Any) -> list[Any]:
210
+ """Get top-level children of the root node (non-recursive)."""
211
+ children = getattr(node, "children", [])
212
+ return list(children)
213
+
214
+
215
+ def _extract_symbol(node: Any, language: str) -> str | None:
216
+ """Extract the name of a function/class node."""
217
+ # Most languages store the name in a child node of type "identifier" or "name"
218
+ for child in getattr(node, "children", []):
219
+ child_type = getattr(child, "type", "")
220
+ if child_type in ("identifier", "name", "property_identifier"):
221
+ return getattr(child, "text", b"").decode("utf-8", errors="replace")
222
+ return None
223
+
224
+
225
+ def _split_large_content(
226
+ content: str,
227
+ base_start_line: int,
228
+ rel_path: str,
229
+ language: str,
230
+ symbol: str | None,
231
+ config: IndexSettings,
232
+ ) -> list[CodeChunk]:
233
+ """Split a large chunk into overlapping sub-chunks."""
234
+ lines = content.split("\n")
235
+ char_budget = config.chunk_size * 4
236
+ overlap_chars = config.chunk_overlap * 4
237
+ chunks: list[CodeChunk] = []
238
+
239
+ start = 0
240
+ while start < len(lines):
241
+ # Accumulate lines until we hit the budget
242
+ end = start
243
+ total_chars = 0
244
+ while end < len(lines) and total_chars < char_budget:
245
+ total_chars += len(lines[end]) + 1
246
+ end += 1
247
+
248
+ chunk_content = "\n".join(lines[start:end])
249
+ chunks.append(
250
+ CodeChunk(
251
+ file_path=rel_path,
252
+ start_line=base_start_line + start,
253
+ end_line=base_start_line + end - 1,
254
+ content=chunk_content,
255
+ language=language,
256
+ symbol=symbol,
257
+ )
258
+ )
259
+
260
+ # If we consumed all lines, we're done
261
+ if end >= len(lines):
262
+ break
263
+
264
+ # Advance with overlap
265
+ overlap_lines = 0
266
+ overlap_total = 0
267
+ for i in range(end - 1, start, -1):
268
+ overlap_total += len(lines[i]) + 1
269
+ overlap_lines += 1
270
+ if overlap_total >= overlap_chars:
271
+ break
272
+
273
+ new_start = end - overlap_lines
274
+ if new_start <= start:
275
+ break
276
+ start = new_start
277
+
278
+ return chunks
279
+
280
+
281
+ def _chunk_by_lines(
282
+ content: str,
283
+ rel_path: str,
284
+ language: str,
285
+ config: IndexSettings,
286
+ ) -> list[CodeChunk]:
287
+ """Fallback: chunk file by line windows."""
288
+ lines = content.split("\n")
289
+ char_budget = config.chunk_size * 4
290
+ overlap_chars = config.chunk_overlap * 4
291
+ chunks: list[CodeChunk] = []
292
+
293
+ start = 0
294
+ while start < len(lines):
295
+ end = start
296
+ total_chars = 0
297
+ while end < len(lines) and total_chars < char_budget:
298
+ total_chars += len(lines[end]) + 1
299
+ end += 1
300
+
301
+ chunk_content = "\n".join(lines[start:end])
302
+ if chunk_content.strip():
303
+ chunks.append(
304
+ CodeChunk(
305
+ file_path=rel_path,
306
+ start_line=start + 1,
307
+ end_line=end,
308
+ content=chunk_content,
309
+ language=language,
310
+ )
311
+ )
312
+
313
+ # If we consumed all lines, we're done
314
+ if end >= len(lines):
315
+ break
316
+
317
+ # Advance with overlap
318
+ overlap_lines = 0
319
+ overlap_total = 0
320
+ for i in range(end - 1, start, -1):
321
+ overlap_total += len(lines[i]) + 1
322
+ overlap_lines += 1
323
+ if overlap_total >= overlap_chars:
324
+ break
325
+
326
+ new_start = end - overlap_lines
327
+ if new_start <= start:
328
+ break
329
+ start = new_start
330
+
331
+ return chunks
@@ -0,0 +1,53 @@
1
+ """RAG retrieval pipeline: vector search over indexed code chunks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mita.config.schema import MitaConfig
8
+ from mita.index.embeddings import EmbeddingClient
9
+ from mita.index.store import IndexStore, SearchResult
10
+
11
+
12
+ def _default_index_dir() -> Path:
13
+ """Determine the index directory for the current project."""
14
+ return Path.cwd() / ".mita" / "index"
15
+
16
+
17
+ class Retriever:
18
+ """High-level retrieval combining embeddings + vector search."""
19
+
20
+ def __init__(self, config: MitaConfig, index_dir: Path | None = None) -> None:
21
+ self._config = config
22
+ self._index_dir = index_dir or _default_index_dir()
23
+ self._store = IndexStore(self._index_dir)
24
+ self._embedder = EmbeddingClient(config)
25
+
26
+ async def retrieve(self, query: str, top_k: int | None = None) -> list[SearchResult]:
27
+ """Retrieve relevant code chunks for a query."""
28
+ k = top_k or self._config.index.top_k
29
+ query_embedding = await self._embedder.embed_single(query)
30
+ return await self._store.search(query_embedding, top_k=k)
31
+
32
+ async def retrieve_formatted(self, query: str, top_k: int | None = None) -> str:
33
+ """Retrieve and format results as a context string for the LLM."""
34
+ results = await self.retrieve(query, top_k=top_k)
35
+ if not results:
36
+ return ""
37
+ return format_results(results)
38
+
39
+ def is_available(self) -> bool:
40
+ """Check if the index exists and is ready to query."""
41
+ return self._store.exists()
42
+
43
+
44
+ def format_results(results: list[SearchResult]) -> str:
45
+ """Format search results as a readable context block."""
46
+ parts: list[str] = []
47
+ for r in results:
48
+ c = r.chunk
49
+ header = f"## {c.file_path}:{c.start_line}-{c.end_line}"
50
+ if c.symbol:
51
+ header += f" ({c.symbol})"
52
+ parts.append(f"{header}\n```{c.language}\n{c.content}\n```")
53
+ return "\n\n".join(parts)
mita/index/store.py ADDED
@@ -0,0 +1,143 @@
1
+ """LanceDB vector store operations for code chunk storage and retrieval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import lancedb # type: ignore[import-untyped]
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class CodeChunk(BaseModel):
15
+ """A chunk of code extracted from a source file."""
16
+
17
+ file_path: str
18
+ start_line: int
19
+ end_line: int
20
+ content: str
21
+ language: str
22
+ symbol: str | None = None
23
+ embedding: list[float] | None = Field(default=None, exclude=True)
24
+
25
+
26
+ class SearchResult(BaseModel):
27
+ """A code chunk with its similarity score."""
28
+
29
+ chunk: CodeChunk
30
+ score: float
31
+
32
+
33
+ class IndexStore:
34
+ """LanceDB-backed vector store for code chunks."""
35
+
36
+ TABLE_NAME = "code_chunks"
37
+
38
+ def __init__(self, index_dir: Path) -> None:
39
+ self._index_dir = index_dir
40
+ self._db_path = index_dir / "lancedb"
41
+
42
+ def _connect(self) -> lancedb.DBConnection:
43
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
44
+ return lancedb.connect(str(self._db_path))
45
+
46
+ async def create_or_replace(self, chunks: list[CodeChunk]) -> None:
47
+ """Create or replace the index with the given chunks."""
48
+ if not chunks:
49
+ return
50
+
51
+ def _sync() -> None:
52
+ db = self._connect()
53
+ records = _chunks_to_records(chunks)
54
+ if self.TABLE_NAME in db.table_names():
55
+ db.drop_table(self.TABLE_NAME)
56
+ db.create_table(self.TABLE_NAME, data=records)
57
+
58
+ await asyncio.to_thread(_sync)
59
+
60
+ async def search(self, query_embedding: list[float], top_k: int = 10) -> list[SearchResult]:
61
+ """Vector similarity search."""
62
+
63
+ def _sync() -> list[SearchResult]:
64
+ db = self._connect()
65
+ if self.TABLE_NAME not in db.table_names():
66
+ return []
67
+ table = db.open_table(self.TABLE_NAME)
68
+ results = table.search(query_embedding).limit(top_k).to_pandas()
69
+
70
+ search_results: list[SearchResult] = []
71
+ for _, row in results.iterrows():
72
+ distance = row.get("_distance", 0.0)
73
+ score = 1.0 / (1.0 + distance)
74
+ chunk = CodeChunk(
75
+ file_path=row["file_path"],
76
+ start_line=int(row["start_line"]),
77
+ end_line=int(row["end_line"]),
78
+ content=row["content"],
79
+ language=row["language"],
80
+ symbol=row.get("symbol"),
81
+ )
82
+ search_results.append(SearchResult(chunk=chunk, score=score))
83
+ return search_results
84
+
85
+ return await asyncio.to_thread(_sync)
86
+
87
+ async def clear(self) -> None:
88
+ """Delete the entire index."""
89
+
90
+ def _sync() -> None:
91
+ if self._db_path.exists():
92
+ shutil.rmtree(self._db_path)
93
+
94
+ await asyncio.to_thread(_sync)
95
+
96
+ async def status(self) -> dict[str, Any]:
97
+ """Return index statistics."""
98
+
99
+ def _sync() -> dict[str, Any]:
100
+ if not self._db_path.exists():
101
+ return {"exists": False, "chunks": 0, "files": 0}
102
+ db = self._connect()
103
+ if self.TABLE_NAME not in db.table_names():
104
+ return {"exists": False, "chunks": 0, "files": 0}
105
+ table = db.open_table(self.TABLE_NAME)
106
+ df = table.to_pandas()
107
+ return {
108
+ "exists": True,
109
+ "chunks": len(df),
110
+ "files": df["file_path"].nunique() if len(df) > 0 else 0,
111
+ }
112
+
113
+ return await asyncio.to_thread(_sync)
114
+
115
+ def exists(self) -> bool:
116
+ """Check if the index exists on disk."""
117
+ if not self._db_path.exists():
118
+ return False
119
+ try:
120
+ db = self._connect()
121
+ return self.TABLE_NAME in db.table_names()
122
+ except Exception: # noqa: BLE001
123
+ return False
124
+
125
+
126
+ def _chunks_to_records(chunks: list[CodeChunk]) -> list[dict[str, Any]]:
127
+ """Convert CodeChunks (with embeddings) to LanceDB-compatible records."""
128
+ records = []
129
+ for chunk in chunks:
130
+ if chunk.embedding is None:
131
+ continue
132
+ records.append(
133
+ {
134
+ "file_path": chunk.file_path,
135
+ "start_line": chunk.start_line,
136
+ "end_line": chunk.end_line,
137
+ "content": chunk.content,
138
+ "language": chunk.language,
139
+ "symbol": chunk.symbol or "",
140
+ "vector": chunk.embedding,
141
+ }
142
+ )
143
+ return records
mita/llm/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """LLM client abstraction: LiteLLM + Instructor + streaming."""