agent-coderag 1.3.3__tar.gz → 1.3.4__tar.gz

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 (98) hide show
  1. {agent_coderag-1.3.3/agent_coderag.egg-info → agent_coderag-1.3.4}/PKG-INFO +13 -1
  2. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/README.md +12 -0
  3. {agent_coderag-1.3.3 → agent_coderag-1.3.4/agent_coderag.egg-info}/PKG-INFO +13 -1
  4. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/SOURCES.txt +15 -0
  5. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/api/client.py +33 -10
  6. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/api/models.py +7 -1
  7. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/constants.py +4 -0
  8. agent_coderag-1.3.4/code_rag/core/interfaces.py +113 -0
  9. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/manager.py +101 -33
  10. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/entry/cli.py +125 -47
  11. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/intelligence/distiller.py +30 -1
  12. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/intelligence/embedder.py +55 -11
  13. agent_coderag-1.3.4/code_rag/intelligence/factory.py +33 -0
  14. agent_coderag-1.3.4/code_rag/intelligence/openai_embedder.py +86 -0
  15. agent_coderag-1.3.4/code_rag/services/config.py +118 -0
  16. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/factory.py +9 -7
  17. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/sync.py +21 -7
  18. agent_coderag-1.3.4/code_rag/storage/duckdb_impl.py +490 -0
  19. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/pyproject.toml +1 -1
  20. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_api_client.py +6 -33
  21. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_api_models.py +19 -2
  22. agent_coderag-1.3.4/tests/test_api_rebuild_wipe.py +171 -0
  23. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_cli.py +41 -29
  24. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_cli_detailed.py +158 -7
  25. agent_coderag-1.3.4/tests/test_cli_embeddings.py +221 -0
  26. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_cli_json_parity.py +11 -6
  27. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_code_rag_simple.py +5 -34
  28. agent_coderag-1.3.4/tests/test_distiller_config_embedding.py +192 -0
  29. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_embedder_detailed.py +6 -2
  30. agent_coderag-1.3.4/tests/test_embedder_factory.py +69 -0
  31. agent_coderag-1.3.4/tests/test_embedder_interface.py +74 -0
  32. agent_coderag-1.3.4/tests/test_embedder_stubs.py +26 -0
  33. agent_coderag-1.3.4/tests/test_factory_async.py +95 -0
  34. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_interfaces.py +33 -3
  35. agent_coderag-1.3.4/tests/test_local_onnx_embedder.py +236 -0
  36. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_manager_detailed.py +8 -2
  37. agent_coderag-1.3.4/tests/test_manager_embeddings.py +277 -0
  38. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_manager_extra.py +15 -2
  39. agent_coderag-1.3.4/tests/test_openai_embedder.py +182 -0
  40. agent_coderag-1.3.4/tests/test_readme_offline_embeddings.py +11 -0
  41. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_storage_detailed.py +41 -7
  42. agent_coderag-1.3.4/tests/test_storage_embeddings.py +446 -0
  43. agent_coderag-1.3.4/tests/test_sync_worker_errors.py +250 -0
  44. agent_coderag-1.3.3/code_rag/core/interfaces.py +0 -63
  45. agent_coderag-1.3.3/code_rag/services/config.py +0 -25
  46. agent_coderag-1.3.3/code_rag/storage/duckdb_impl.py +0 -311
  47. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/LICENSE +0 -0
  48. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/NOTICE +0 -0
  49. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/dependency_links.txt +0 -0
  50. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/entry_points.txt +0 -0
  51. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/requires.txt +0 -0
  52. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/top_level.txt +0 -0
  53. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/__init__.py +0 -0
  54. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/api/__init__.py +0 -0
  55. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/__init__.py +0 -0
  56. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/exceptions.py +0 -0
  57. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/models.py +0 -0
  58. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/utils.py +0 -0
  59. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/__init__.py +0 -0
  60. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/dependency.py +0 -0
  61. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/java_discovery.py +0 -0
  62. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/manager.py +0 -0
  63. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/__init__.py +0 -0
  64. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/base.py +0 -0
  65. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/csharp.py +0 -0
  66. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/go.py +0 -0
  67. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/java.py +0 -0
  68. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/javascript.py +0 -0
  69. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/python.py +0 -0
  70. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/rust.py +0 -0
  71. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/entry/__init__.py +0 -0
  72. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/intelligence/__init__.py +0 -0
  73. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/__init__.py +0 -0
  74. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/languages.py +0 -0
  75. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/multi_parser.py +0 -0
  76. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/tree_sitter.py +0 -0
  77. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/__init__.py +0 -0
  78. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/discovery_api.py +0 -0
  79. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/search.py +0 -0
  80. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/setup.py +0 -0
  81. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/storage/__init__.py +0 -0
  82. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/setup.cfg +0 -0
  83. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_csharp_discovery_detailed.py +0 -0
  84. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_dependency_discovery.py +0 -0
  85. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_discovery_manager_detailed.py +0 -0
  86. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_discovery_providers_extra.py +0 -0
  87. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_discovery_python.py +0 -0
  88. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_distiller.py +0 -0
  89. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_distiller_extra.py +0 -0
  90. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_embedder.py +0 -0
  91. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_js_discovery_detailed.py +0 -0
  92. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_languages.py +0 -0
  93. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_models.py +0 -0
  94. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_multi_parser.py +0 -0
  95. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_readme_library_usage.py +0 -0
  96. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_rust_discovery_detailed.py +0 -0
  97. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_tree_sitter_parser.py +0 -0
  98. {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_utils_detailed.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-coderag
3
- Version: 1.3.3
3
+ Version: 1.3.4
4
4
  Summary: Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index.
5
5
  Author-email: Igor Boloban <naranor@gmail.com>
6
6
  License: MIT
@@ -119,6 +119,18 @@ If you don't configure an LLM provider, agent-coderag works in **100% Offline Mo
119
119
  - **Search:** Remains fast and accurate.
120
120
  - **Distillation:** Instead of AI-generated summaries, the system uses code signatures and entity names as fallback metadata. No data ever leaves your machine.
121
121
 
122
+ Remote embeddings are **not** 100% Offline Mode. If you set `embedding_base` + `embedding_model`, `sync` / `search` / `rebuild` need the network. Embedder choice is process-global (`config.json`); after a remote model or dimension change, run `agent-coderag rebuild` (or delete that `--db`) for **each** project index.
123
+
124
+ ```bash
125
+ agent-coderag config \
126
+ --embedding-url "http://localhost:8081/v1" \
127
+ --embedding-model "text-embedding-3-small" \
128
+ --embedding-key "your-api-key" \
129
+ --embedding-provider "openai"
130
+
131
+ agent-coderag config --clear-embedding
132
+ ```
133
+
122
134
  ### First Sync & Search
123
135
  ```bash
124
136
  # Index your entire project (respects .gitignore automatically)
@@ -73,6 +73,18 @@ If you don't configure an LLM provider, agent-coderag works in **100% Offline Mo
73
73
  - **Search:** Remains fast and accurate.
74
74
  - **Distillation:** Instead of AI-generated summaries, the system uses code signatures and entity names as fallback metadata. No data ever leaves your machine.
75
75
 
76
+ Remote embeddings are **not** 100% Offline Mode. If you set `embedding_base` + `embedding_model`, `sync` / `search` / `rebuild` need the network. Embedder choice is process-global (`config.json`); after a remote model or dimension change, run `agent-coderag rebuild` (or delete that `--db`) for **each** project index.
77
+
78
+ ```bash
79
+ agent-coderag config \
80
+ --embedding-url "http://localhost:8081/v1" \
81
+ --embedding-model "text-embedding-3-small" \
82
+ --embedding-key "your-api-key" \
83
+ --embedding-provider "openai"
84
+
85
+ agent-coderag config --clear-embedding
86
+ ```
87
+
76
88
  ### First Sync & Search
77
89
  ```bash
78
90
  # Index your entire project (respects .gitignore automatically)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-coderag
3
- Version: 1.3.3
3
+ Version: 1.3.4
4
4
  Summary: Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index.
5
5
  Author-email: Igor Boloban <naranor@gmail.com>
6
6
  License: MIT
@@ -119,6 +119,18 @@ If you don't configure an LLM provider, agent-coderag works in **100% Offline Mo
119
119
  - **Search:** Remains fast and accurate.
120
120
  - **Distillation:** Instead of AI-generated summaries, the system uses code signatures and entity names as fallback metadata. No data ever leaves your machine.
121
121
 
122
+ Remote embeddings are **not** 100% Offline Mode. If you set `embedding_base` + `embedding_model`, `sync` / `search` / `rebuild` need the network. Embedder choice is process-global (`config.json`); after a remote model or dimension change, run `agent-coderag rebuild` (or delete that `--db`) for **each** project index.
123
+
124
+ ```bash
125
+ agent-coderag config \
126
+ --embedding-url "http://localhost:8081/v1" \
127
+ --embedding-model "text-embedding-3-small" \
128
+ --embedding-key "your-api-key" \
129
+ --embedding-provider "openai"
130
+
131
+ agent-coderag config --clear-embedding
132
+ ```
133
+
122
134
  ### First Sync & Search
123
135
  ```bash
124
136
  # Index your entire project (respects .gitignore automatically)
@@ -36,6 +36,8 @@ code_rag/entry/cli.py
36
36
  code_rag/intelligence/__init__.py
37
37
  code_rag/intelligence/distiller.py
38
38
  code_rag/intelligence/embedder.py
39
+ code_rag/intelligence/factory.py
40
+ code_rag/intelligence/openai_embedder.py
39
41
  code_rag/parsers/__init__.py
40
42
  code_rag/parsers/languages.py
41
43
  code_rag/parsers/multi_parser.py
@@ -51,8 +53,10 @@ code_rag/storage/__init__.py
51
53
  code_rag/storage/duckdb_impl.py
52
54
  tests/test_api_client.py
53
55
  tests/test_api_models.py
56
+ tests/test_api_rebuild_wipe.py
54
57
  tests/test_cli.py
55
58
  tests/test_cli_detailed.py
59
+ tests/test_cli_embeddings.py
56
60
  tests/test_cli_json_parity.py
57
61
  tests/test_code_rag_simple.py
58
62
  tests/test_csharp_discovery_detailed.py
@@ -61,18 +65,29 @@ tests/test_discovery_manager_detailed.py
61
65
  tests/test_discovery_providers_extra.py
62
66
  tests/test_discovery_python.py
63
67
  tests/test_distiller.py
68
+ tests/test_distiller_config_embedding.py
64
69
  tests/test_distiller_extra.py
65
70
  tests/test_embedder.py
66
71
  tests/test_embedder_detailed.py
72
+ tests/test_embedder_factory.py
73
+ tests/test_embedder_interface.py
74
+ tests/test_embedder_stubs.py
75
+ tests/test_factory_async.py
67
76
  tests/test_interfaces.py
68
77
  tests/test_js_discovery_detailed.py
69
78
  tests/test_languages.py
79
+ tests/test_local_onnx_embedder.py
70
80
  tests/test_manager_detailed.py
81
+ tests/test_manager_embeddings.py
71
82
  tests/test_manager_extra.py
72
83
  tests/test_models.py
73
84
  tests/test_multi_parser.py
85
+ tests/test_openai_embedder.py
74
86
  tests/test_readme_library_usage.py
87
+ tests/test_readme_offline_embeddings.py
75
88
  tests/test_rust_discovery_detailed.py
76
89
  tests/test_storage_detailed.py
90
+ tests/test_storage_embeddings.py
91
+ tests/test_sync_worker_errors.py
77
92
  tests/test_tree_sitter_parser.py
78
93
  tests/test_utils_detailed.py
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
  from typing import Optional, Union
3
3
 
4
4
  from code_rag.api.models import ApiReport, SetupResult, SyncResult
5
+ from code_rag.core.manager import CodeRAGManager
5
6
  from code_rag.core.models import KnowledgeUnit
6
7
  from code_rag.intelligence.distiller import DistillerConfig
7
8
  from code_rag.services.config import load_or_update_config
@@ -27,7 +28,7 @@ class CodeRAG:
27
28
  self._onnx = onnx
28
29
  self._root = Path(root) if root is not None else Path.cwd()
29
30
  self._allow_build_execution = allow_build_execution
30
- self._manager = None
31
+ self._manager: Optional[CodeRAGManager] = None
31
32
 
32
33
  async def __aenter__(self) -> "CodeRAG":
33
34
  return self
@@ -40,22 +41,42 @@ class CodeRAG:
40
41
  await self._manager.close()
41
42
  self._manager = None
42
43
 
43
- def _ensure_manager(self):
44
+ async def _ensure_manager(self, wipe: bool = False):
45
+ if wipe and self._manager is not None:
46
+ await self.close()
44
47
  if self._manager is None:
45
- self._manager = create_manager(
46
- self._db, self._onnx, allow_build_execution=self._allow_build_execution
48
+ self._manager = await create_manager(
49
+ self._db,
50
+ self._onnx,
51
+ allow_build_execution=self._allow_build_execution,
52
+ wipe=wipe,
47
53
  )
48
54
  return self._manager
49
55
 
50
- async def config(
56
+ async def config( # pylint: disable=too-many-arguments
51
57
  self,
52
58
  *,
53
59
  url: Optional[str] = None,
54
60
  key: Optional[str] = None,
55
61
  model: Optional[str] = None,
56
62
  provider: Optional[str] = None,
63
+ embedding_url: Optional[str] = None,
64
+ embedding_key: Optional[str] = None,
65
+ embedding_model: Optional[str] = None,
66
+ embedding_provider: Optional[str] = None,
67
+ clear_embedding: bool = False,
57
68
  ) -> DistillerConfig:
58
- return load_or_update_config(url=url, key=key, model=model, provider=provider)
69
+ return load_or_update_config(
70
+ url=url,
71
+ key=key,
72
+ model=model,
73
+ provider=provider,
74
+ embedding_url=embedding_url,
75
+ embedding_key=embedding_key,
76
+ embedding_model=embedding_model,
77
+ embedding_provider=embedding_provider,
78
+ clear_embedding=clear_embedding,
79
+ )
59
80
 
60
81
  async def setup(self, *, force: bool = False) -> SetupResult:
61
82
  return await run_setup(force=force)
@@ -70,7 +91,7 @@ class CodeRAG:
70
91
  if path is None and not index_all:
71
92
  return SyncResult(status="success", indexed_files=0)
72
93
  return await run_sync(
73
- self._ensure_manager(),
94
+ await self._ensure_manager(),
74
95
  root=self._root,
75
96
  path=path,
76
97
  index_all=index_all,
@@ -78,10 +99,12 @@ class CodeRAG:
78
99
  )
79
100
 
80
101
  async def search(self, query: str, *, limit: int = 5) -> list[KnowledgeUnit]:
81
- return await run_search(self._ensure_manager(), query, limit=limit)
102
+ return await run_search(await self._ensure_manager(), query, limit=limit)
82
103
 
83
104
  async def api(self, library: str, *, lang: Optional[str] = None) -> ApiReport:
84
- return await run_api(self._ensure_manager(), library, lang=lang)
105
+ return await run_api(await self._ensure_manager(), library, lang=lang)
85
106
 
86
107
  async def rebuild(self) -> SyncResult:
87
- return await run_rebuild(self._ensure_manager(), root=self._root)
108
+ await self.close()
109
+ manager = await self._ensure_manager(wipe=True)
110
+ return await run_rebuild(manager, root=self._root)
@@ -1,9 +1,15 @@
1
- from pydantic import BaseModel, ConfigDict
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+
4
+ class SyncFileError(BaseModel):
5
+ file: str
6
+ message: str
2
7
 
3
8
 
4
9
  class SyncResult(BaseModel):
5
10
  status: str
6
11
  indexed_files: int
12
+ errors: list[SyncFileError] = Field(default_factory=list)
7
13
 
8
14
 
9
15
  class ApiReport(BaseModel):
@@ -4,6 +4,10 @@ Central location for project-wide constants.
4
4
 
5
5
  # Embedding Model Constants
6
6
  EMBEDDING_DIM = 384
7
+ EMBEDDING_BATCH_SIZE = 32
8
+ EMBEDDING_REQUEST_TIMEOUT = 30
9
+ EMBEDDING_PROBE_TEXT = "probe"
10
+ LOCAL_EMBEDDING_MODEL_ID = "local:mini-lm"
7
11
  MAX_TOKEN_LENGTH = 512
8
12
  PAD_ID = 0
9
13
  PAD_TOKEN = "[PAD]" # nosec B105
@@ -0,0 +1,113 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Optional
3
+ from .models import KnowledgeUnit, Relation
4
+
5
+
6
+ class IParser(ABC):
7
+ """Interface for extracting structure from code."""
8
+
9
+ @abstractmethod
10
+ async def distill_file(self, file_path: str) -> List[KnowledgeUnit]:
11
+ pass # pragma: no cover
12
+
13
+
14
+ class IEmbedder(ABC):
15
+ """Port for local ONNX and remote OpenAI-compatible embedders."""
16
+
17
+ @property
18
+ @abstractmethod
19
+ def dimension(self) -> int:
20
+ """Bound embedding width. No IO. Raise if unbound.""" # pragma: no cover
21
+
22
+ @property
23
+ @abstractmethod
24
+ def model_id(self) -> str:
25
+ """Stable id written to index_meta (local:mini-lm or remote model).""" # pragma: no cover
26
+
27
+ @abstractmethod
28
+ def bind_dimension(self, dim: int) -> None:
29
+ """Record the index dimension before search/upsert.""" # pragma: no cover
30
+
31
+ @abstractmethod
32
+ async def aembed(self, texts: list[str]) -> list[list[float]]:
33
+ """Return len(texts) L2-normalized rows, 1:1 with input order.""" # pragma: no cover
34
+
35
+ @abstractmethod
36
+ async def close(self) -> None:
37
+ """Release ONNX session and/or HTTP resources.""" # pragma: no cover
38
+
39
+
40
+ class IStorage(ABC):
41
+ """Interface for storing the index."""
42
+
43
+ @abstractmethod
44
+ async def upsert_unit(
45
+ self, unit: KnowledgeUnit, vector: Optional[List[float]] = None
46
+ ):
47
+ pass # pragma: no cover
48
+
49
+ @abstractmethod
50
+ async def has_embedding(self, unit_id: str) -> bool:
51
+ pass # pragma: no cover
52
+
53
+ @abstractmethod
54
+ async def list_units(self) -> List[KnowledgeUnit]:
55
+ pass # pragma: no cover
56
+
57
+ @abstractmethod
58
+ async def mark_embedding_model_synced(self) -> None:
59
+ pass # pragma: no cover
60
+
61
+ @property
62
+ @abstractmethod
63
+ def embedding_model_dirty(self) -> bool:
64
+ pass # pragma: no cover
65
+
66
+ @property
67
+ @abstractmethod
68
+ def embedder(self) -> IEmbedder:
69
+ """Bound embedder used for vector search and upsert.""" # pragma: no cover
70
+
71
+ @abstractmethod
72
+ async def get_unit(self, unit_id: str) -> Optional[KnowledgeUnit]:
73
+ pass # pragma: no cover
74
+
75
+ @abstractmethod
76
+ async def search_units(self, query: str, limit: int = 5) -> List[KnowledgeUnit]:
77
+ pass # pragma: no cover
78
+
79
+ @abstractmethod
80
+ async def upsert_relation(self, relation: Relation):
81
+ pass # pragma: no cover
82
+
83
+ @abstractmethod
84
+ async def get_relations(
85
+ self, unit_id: str, direction: str = "out"
86
+ ) -> List[Relation]:
87
+ pass # pragma: no cover
88
+
89
+ @abstractmethod
90
+ async def set_dependency_path(self, lib_name: str, path: str) -> None:
91
+ """Caches the absolute path to a library's JAR/binary."""
92
+
93
+ @abstractmethod
94
+ async def get_dependency_path(self, lib_name: str) -> Optional[str]:
95
+ """Retrieves the cached path for a library."""
96
+
97
+ @abstractmethod
98
+ async def delete_stale_units(
99
+ self, file_path: str, current_unit_ids: List[str]
100
+ ) -> None:
101
+ """Removes units that are no longer present in the given file."""
102
+
103
+ @abstractmethod
104
+ async def close(self) -> None:
105
+ """Closes the storage connection and releases resources."""
106
+
107
+
108
+ class IIntelligence(ABC):
109
+ """Interface for LLM-based analysis (distillation, embeddings)."""
110
+
111
+ @abstractmethod
112
+ async def summarize(self, code: str, unit_name: str) -> str:
113
+ pass # pragma: no cover
@@ -1,19 +1,34 @@
1
1
  import logging
2
2
  import asyncio
3
+ import inspect
3
4
  import os
4
5
  import shutil
5
6
  import subprocess # nosec
7
+ import sys
6
8
  from pathlib import Path
7
9
  from typing import List, Optional
8
10
  from .interfaces import IStorage, IParser, IIntelligence
9
11
  from .models import KnowledgeUnit
10
12
  from .utils import validate_path
11
- from .constants import MAX_CONCURRENT_TASKS
13
+ from .constants import MAX_CONCURRENT_TASKS, EMBEDDING_BATCH_SIZE
14
+ from .exceptions import StorageError, IntelligenceError
12
15
  from ..discovery.manager import DiscoveryManager
13
16
 
14
17
  logger = logging.getLogger(__name__)
15
18
 
16
19
 
20
+ def _report_worker_failure(path: str, exc: BaseException) -> tuple[str, str]:
21
+ logger.error("Worker failed to sync %s: %s", path, exc)
22
+ print(f"Worker failed to sync {path}: {exc}", file=sys.stderr, flush=True)
23
+ return (path, str(exc))
24
+
25
+
26
+ def unit_embedding_text(unit: KnowledgeUnit) -> str:
27
+ return unit.summary or (
28
+ f"{unit.kind.value} {unit.name} {unit.signature or ''} {unit.docstring or ''}"
29
+ )
30
+
31
+
17
32
  class CodeRAGManager:
18
33
  """
19
34
  Orchestrates the RAG workflow: parsing, distillation, and storage.
@@ -192,20 +207,46 @@ allprojects {
192
207
  if init_script.exists():
193
208
  init_script.unlink()
194
209
 
210
+ async def _reject_dirty_incremental(self) -> None:
211
+ if getattr(self.storage, "embedding_model_dirty", False):
212
+ raise StorageError("Embedding model changed; run rebuild or sync --all")
213
+
214
+ async def _ensure_embeddings(self) -> None:
215
+ ensure = getattr(self.storage, "ensure_embeddings_bound", None)
216
+ if callable(ensure):
217
+ result = ensure()
218
+ if inspect.isawaitable(result):
219
+ await result
220
+
221
+ async def _reembed_all_units(self) -> None:
222
+ units = await self.storage.list_units()
223
+ await self._embed_and_upsert(units)
224
+ await self.storage.mark_embedding_model_synced()
225
+
226
+ async def _embed_and_upsert(self, units: list[KnowledgeUnit]) -> None:
227
+ if not units:
228
+ return
229
+ await self._ensure_embeddings()
230
+ embedder = self.storage.embedder
231
+ for start in range(0, len(units), EMBEDDING_BATCH_SIZE):
232
+ chunk = units[start : start + EMBEDDING_BATCH_SIZE]
233
+ texts = [unit_embedding_text(unit) for unit in chunk]
234
+ vectors = await embedder.aembed(texts)
235
+ if len(vectors) != len(chunk):
236
+ raise IntelligenceError("Embedding count mismatch")
237
+ for unit, vector in zip(chunk, vectors):
238
+ await self.storage.upsert_unit(unit, vector=vector)
239
+
195
240
  async def sync_file(self, file_path: str, force_distill: bool = False) -> None:
196
241
  """
197
242
  Processes a single file and syncs it with the storage.
198
243
  """
199
- # 1. Parse AST to get units
244
+ await self._reject_dirty_incremental()
200
245
  current_units = await self.parser.distill_file(file_path)
201
-
246
+ pending: list[KnowledgeUnit] = []
202
247
  for unit in current_units:
203
- # v5.40: Delta-distillation logic
204
248
  raw_code = unit.metadata.pop("raw_code", "")
205
-
206
- # 2. Get existing unit to check hash
207
249
  existing_unit = await self.storage.get_unit(unit.id)
208
-
209
250
  should_distill = force_distill
210
251
  if not existing_unit:
211
252
  should_distill = True
@@ -216,29 +257,36 @@ allprojects {
216
257
  elif not existing_unit.summary:
217
258
  should_distill = True
218
259
  logger.info("Summary missing for %s", unit.name)
219
-
220
260
  if should_distill:
221
261
  async with self.semaphore:
222
262
  logger.info(
223
263
  "Distilling summary for %s in %s...", unit.name, unit.path
224
264
  )
225
265
  try:
226
- summary = await self.intelligence.summarize(raw_code, unit.name)
227
- unit.summary = summary
228
- except Exception as e:
229
- logger.error("Failed to distill %s: %s", unit.name, e)
230
- # Keep old summary if available, otherwise stay None
266
+ unit.summary = await self.intelligence.summarize(
267
+ raw_code, unit.name
268
+ )
269
+ except Exception as exc:
270
+ logger.error("Failed to distill %s: %s", unit.name, exc)
231
271
  unit.summary = existing_unit.summary if existing_unit else None
232
272
  else:
233
- # Reuse existing summary if code hasn't changed
234
273
  unit.summary = existing_unit.summary if existing_unit else None
235
274
 
236
- # 3. Save to storage (includes embedding generation)
237
- await self.storage.upsert_unit(unit)
238
-
239
- # 4. Garbage Collection: Remove units that were in this file but are no longer there
240
- current_unit_ids = [u.id for u in current_units]
241
- await self.storage.delete_stale_units(file_path, current_unit_ids)
275
+ has_vec = await self.storage.has_embedding(unit.id)
276
+ skip = (
277
+ existing_unit is not None
278
+ and existing_unit.code_hash == unit.code_hash
279
+ and existing_unit.summary == unit.summary
280
+ and has_vec
281
+ )
282
+ if skip:
283
+ await self.storage.upsert_unit(unit)
284
+ else:
285
+ pending.append(unit)
286
+ await self._embed_and_upsert(pending)
287
+ await self.storage.delete_stale_units(
288
+ file_path, [unit.id for unit in current_units]
289
+ )
242
290
 
243
291
  async def search(self, query: str, limit: int = 5) -> List[KnowledgeUnit]:
244
292
  """
@@ -246,38 +294,58 @@ allprojects {
246
294
  """
247
295
  return await self.storage.search_units(query, limit=limit)
248
296
 
249
- async def sync_project(self, paths: List[str], force_distill: bool = False) -> None:
297
+ async def sync_project(
298
+ self,
299
+ paths: List[str],
300
+ force_distill: bool = False,
301
+ *,
302
+ index_all: bool = False,
303
+ ) -> list[tuple[str, str]]:
250
304
  """
251
305
  Concurrent synchronization of multiple files using a worker pool.
306
+
307
+ Returns (path, error) pairs for files that failed. The queue is drained
308
+ even if some workers fail; callers must not treat an empty return as
309
+ "raised" — check the list instead of catching after completion.
252
310
  """
311
+ if getattr(self.storage, "embedding_model_dirty", False):
312
+ if not index_all:
313
+ raise StorageError("Embedding model changed; run rebuild or sync --all")
314
+ await self._reembed_all_units()
253
315
  if not paths:
254
- return
316
+ return []
255
317
 
256
318
  queue: asyncio.Queue[str] = asyncio.Queue()
257
- for p in paths:
258
- await queue.put(p)
319
+ for path in paths:
320
+ await queue.put(path)
259
321
 
260
- async def worker() -> None:
322
+ async def worker() -> list[tuple[str, str]]:
323
+ failures: list[tuple[str, str]] = []
261
324
  while not queue.empty():
262
325
  path = await queue.get()
263
326
  try:
264
- await self.sync_file(path, force_distill=force_distill)
265
- except Exception as e:
266
- logger.error("Worker failed to sync %s: %s", path, e)
327
+ await self.sync_file(
328
+ path,
329
+ force_distill=force_distill,
330
+ )
331
+ except Exception as exc:
332
+ failures.append(_report_worker_failure(path, exc))
267
333
  finally:
268
334
  queue.task_done()
335
+ return failures
269
336
 
270
- # Run limited number of workers
271
337
  worker_count = min(len(paths), self.max_concurrency)
272
338
  tasks = [asyncio.create_task(worker()) for _ in range(worker_count)]
273
-
274
- await asyncio.gather(*tasks)
339
+ batches = await asyncio.gather(*tasks)
275
340
  logger.info("Project sync complete.")
341
+ return [item for batch in batches for item in batch]
276
342
 
277
343
  async def close(self) -> None:
278
344
  """Releases manager resources."""
279
345
  await self.storage.close()
280
- # If intelligence has close method (Embedder does)
281
346
  if hasattr(self.intelligence, "close"):
282
- self.intelligence.close()
347
+ closer = self.intelligence.close
348
+ result = closer()
349
+ if asyncio.iscoroutine(result):
350
+ await result
283
351
  logger.info("CodeRAG manager closed.")