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.
- {agent_coderag-1.3.3/agent_coderag.egg-info → agent_coderag-1.3.4}/PKG-INFO +13 -1
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/README.md +12 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4/agent_coderag.egg-info}/PKG-INFO +13 -1
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/SOURCES.txt +15 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/api/client.py +33 -10
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/api/models.py +7 -1
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/constants.py +4 -0
- agent_coderag-1.3.4/code_rag/core/interfaces.py +113 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/manager.py +101 -33
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/entry/cli.py +125 -47
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/intelligence/distiller.py +30 -1
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/intelligence/embedder.py +55 -11
- agent_coderag-1.3.4/code_rag/intelligence/factory.py +33 -0
- agent_coderag-1.3.4/code_rag/intelligence/openai_embedder.py +86 -0
- agent_coderag-1.3.4/code_rag/services/config.py +118 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/factory.py +9 -7
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/sync.py +21 -7
- agent_coderag-1.3.4/code_rag/storage/duckdb_impl.py +490 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/pyproject.toml +1 -1
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_api_client.py +6 -33
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_api_models.py +19 -2
- agent_coderag-1.3.4/tests/test_api_rebuild_wipe.py +171 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_cli.py +41 -29
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_cli_detailed.py +158 -7
- agent_coderag-1.3.4/tests/test_cli_embeddings.py +221 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_cli_json_parity.py +11 -6
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_code_rag_simple.py +5 -34
- agent_coderag-1.3.4/tests/test_distiller_config_embedding.py +192 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_embedder_detailed.py +6 -2
- agent_coderag-1.3.4/tests/test_embedder_factory.py +69 -0
- agent_coderag-1.3.4/tests/test_embedder_interface.py +74 -0
- agent_coderag-1.3.4/tests/test_embedder_stubs.py +26 -0
- agent_coderag-1.3.4/tests/test_factory_async.py +95 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_interfaces.py +33 -3
- agent_coderag-1.3.4/tests/test_local_onnx_embedder.py +236 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_manager_detailed.py +8 -2
- agent_coderag-1.3.4/tests/test_manager_embeddings.py +277 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_manager_extra.py +15 -2
- agent_coderag-1.3.4/tests/test_openai_embedder.py +182 -0
- agent_coderag-1.3.4/tests/test_readme_offline_embeddings.py +11 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_storage_detailed.py +41 -7
- agent_coderag-1.3.4/tests/test_storage_embeddings.py +446 -0
- agent_coderag-1.3.4/tests/test_sync_worker_errors.py +250 -0
- agent_coderag-1.3.3/code_rag/core/interfaces.py +0 -63
- agent_coderag-1.3.3/code_rag/services/config.py +0 -25
- agent_coderag-1.3.3/code_rag/storage/duckdb_impl.py +0 -311
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/LICENSE +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/NOTICE +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/dependency_links.txt +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/entry_points.txt +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/requires.txt +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/agent_coderag.egg-info/top_level.txt +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/api/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/exceptions.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/models.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/core/utils.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/dependency.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/java_discovery.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/manager.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/base.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/csharp.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/go.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/java.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/javascript.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/python.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/discovery/providers/rust.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/entry/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/intelligence/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/languages.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/multi_parser.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/parsers/tree_sitter.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/discovery_api.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/search.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/services/setup.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/code_rag/storage/__init__.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/setup.cfg +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_csharp_discovery_detailed.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_dependency_discovery.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_discovery_manager_detailed.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_discovery_providers_extra.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_discovery_python.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_distiller.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_distiller_extra.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_embedder.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_js_discovery_detailed.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_languages.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_models.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_multi_parser.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_readme_library_usage.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_rust_discovery_detailed.py +0 -0
- {agent_coderag-1.3.3 → agent_coderag-1.3.4}/tests/test_tree_sitter_parser.py +0 -0
- {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
|
+
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
|
+
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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(
|
|
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
|
|
258
|
-
await queue.put(
|
|
319
|
+
for path in paths:
|
|
320
|
+
await queue.put(path)
|
|
259
321
|
|
|
260
|
-
async def worker() ->
|
|
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(
|
|
265
|
-
|
|
266
|
-
|
|
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.")
|