agent-coderag 1.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.
- agent_coderag-1.1.0.dist-info/METADATA +188 -0
- agent_coderag-1.1.0.dist-info/RECORD +23 -0
- agent_coderag-1.1.0.dist-info/WHEEL +5 -0
- agent_coderag-1.1.0.dist-info/entry_points.txt +2 -0
- agent_coderag-1.1.0.dist-info/licenses/LICENSE +21 -0
- agent_coderag-1.1.0.dist-info/licenses/NOTICE +17 -0
- agent_coderag-1.1.0.dist-info/top_level.txt +1 -0
- code_rag/__init__.py +0 -0
- code_rag/core/__init__.py +0 -0
- code_rag/core/interfaces.py +29 -0
- code_rag/core/manager.py +70 -0
- code_rag/core/models.py +33 -0
- code_rag/discovery/__init__.py +0 -0
- code_rag/discovery/dependency.py +40 -0
- code_rag/entry/__init__.py +0 -0
- code_rag/entry/cli.py +214 -0
- code_rag/intelligence/__init__.py +0 -0
- code_rag/intelligence/distiller.py +80 -0
- code_rag/intelligence/embedder.py +100 -0
- code_rag/parsers/__init__.py +0 -0
- code_rag/parsers/ast_index.py +81 -0
- code_rag/storage/__init__.py +0 -0
- code_rag/storage/duckdb_impl.py +111 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-coderag
|
|
3
|
+
Version: 1.1.0
|
|
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
|
+
Author-email: Igor Boloban <naranor@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/naranor/agent-coderag
|
|
8
|
+
Project-URL: Repository, https://github.com/naranor/agent-coderag
|
|
9
|
+
Project-URL: Issues, https://github.com/naranor/agent-coderag/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/naranor/agent-coderag/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: rag,ai-agents,semantic-search,code-analysis,context-compression,onnx,local-embeddings
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Requires-Dist: duckdb
|
|
26
|
+
Requires-Dist: numpy
|
|
27
|
+
Requires-Dist: litellm
|
|
28
|
+
Requires-Dist: onnxruntime
|
|
29
|
+
Requires-Dist: tokenizers
|
|
30
|
+
Requires-Dist: pydantic
|
|
31
|
+
Requires-Dist: httpx
|
|
32
|
+
Requires-Dist: aiofiles
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Agent-CodeRAG: Semantic Intelligence for AI Coding Agents
|
|
36
|
+
|
|
37
|
+
> **Fast. Local. Agent-First. Token-Efficient.**
|
|
38
|
+
|
|
39
|
+
[](https://www.python.org/downloads/)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
[](#-key-technologies)
|
|
42
|
+
[](CONTRIBUTING.md)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 📖 Table of Contents
|
|
47
|
+
- [🧠 The Problem: The API Knowledge Gap](#-the-problem-the-api-knowledge-gap)
|
|
48
|
+
- [🚀 The Solution: Real-Time Contextual Truth](#-the-solution-real-time-contextual-truth)
|
|
49
|
+
- [🛠 How it Works](#-how-it-works)
|
|
50
|
+
- [📡 API Discovery](#-api-discovery)
|
|
51
|
+
- [🏃 Quick Start](#-quick-start)
|
|
52
|
+
- [🤖 For AI Agents](#-for-ai-agents)
|
|
53
|
+
- [🔧 Development](#-development)
|
|
54
|
+
- [📄 License](#-license)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🧠 The Problem: The API Knowledge Gap
|
|
59
|
+
|
|
60
|
+
AI coding agents often hallucinate when calling library APIs because their training data is static. This leads to a **"Fail-Fix-Fail" cycle**:
|
|
61
|
+
|
|
62
|
+
1. **Broken Code**: Agents use deprecated parameters or non-existent methods from outdated versions.
|
|
63
|
+
2. **Token Waste**: You provide the error, the agent tries to fix it using more outdated data, consuming thousands of tokens in a loop.
|
|
64
|
+
3. **Environment Mismatch**: The agent knows the API for version 1.0, but your environment has 2.0.
|
|
65
|
+
|
|
66
|
+
### Real-world Example (The Pydantic Gap)
|
|
67
|
+
* **Agent's Knowledge**: Knows Pydantic v1 (`model.dict()`).
|
|
68
|
+
* **Your Environment**: Uses Pydantic v2 (`model.model_dump()`).
|
|
69
|
+
* **The Result**: The agent writes `dict()`, the code fails, and it wastes **5000+ tokens** trying to "fix" a problem it doesn't understand.
|
|
70
|
+
|
|
71
|
+
## 🚀 The Solution: Real-Time Contextual Truth
|
|
72
|
+
|
|
73
|
+
Agent-CodeRAG acts as a lightweight semantic bridge between your local environment and the LLM.
|
|
74
|
+
|
|
75
|
+
* **API Discovery**: Extracts *actual* signatures from your installed libraries.
|
|
76
|
+
* **Semantic Retrieval**: Provides the LLM with the exact **Intent** of your code units, indexed locally via ONNX.
|
|
77
|
+
* **Token Efficiency**: Instead of sending whole files, Agent-CodeRAG distills code into compact semantic summaries, **saving up to 80% of context window tokens**.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🛠 How it Works
|
|
82
|
+
|
|
83
|
+
```mermaid
|
|
84
|
+
graph TD
|
|
85
|
+
A[Local Python Code] --> B[AST Parser]
|
|
86
|
+
B --> C{Delta-Sync}
|
|
87
|
+
C -- Changed/New --> D[LLM Distiller]
|
|
88
|
+
C -- Unchanged --> E[Local Cache]
|
|
89
|
+
D --> F[Semantic Summary]
|
|
90
|
+
E --> F
|
|
91
|
+
F --> G[ONNX Embedder]
|
|
92
|
+
G --> H[(DuckDB VSS)]
|
|
93
|
+
H --> I[Semantic Search / JSON API]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### ✨ Key Features
|
|
97
|
+
* **⚡ No PyTorch**: Uses `onnxruntime` and `tokenizers` (Rust) for a tiny footprint and instant startup.
|
|
98
|
+
* **💾 DuckDB VSS**: High-performance vector similarity search stored in a single local file.
|
|
99
|
+
* **🔄 Delta-Sync**: Uses SHA-256 hashing to only re-distill changed code, saving your API budget.
|
|
100
|
+
* **🔌 Hybrid Intelligence**: Works offline using name-based embeddings; adds AI-distilled reasoning when an LLM is connected.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 📡 API Discovery
|
|
105
|
+
To help your agent understand a specific library version installed in your environment:
|
|
106
|
+
```bash
|
|
107
|
+
agent-coderag api pydantic
|
|
108
|
+
```
|
|
109
|
+
Returns the *live* public API, methods, and signatures.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🏃 Quick Start
|
|
114
|
+
|
|
115
|
+
### 1. Install
|
|
116
|
+
```bash
|
|
117
|
+
pip install agent-coderag
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2. Setup AI Models
|
|
121
|
+
Download the lightweight `paraphrase-multilingual-MiniLM` ONNX model to your global cache:
|
|
122
|
+
```bash
|
|
123
|
+
agent-coderag setup
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. Configure your LLM (For Distillation)
|
|
127
|
+
|
|
128
|
+
**Option A: Cloud (OpenAI)**
|
|
129
|
+
```bash
|
|
130
|
+
agent-coderag config --url "https://api.openai.com/v1" --model "gpt-4o-mini" --key "sk-..."
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Option B: Local (Ollama) - Recommended**
|
|
134
|
+
```bash
|
|
135
|
+
agent-coderag config --url "http://localhost:11434" --provider "ollama" --model "qwen2.5-coder:7b"
|
|
136
|
+
```
|
|
137
|
+
*We recommend using `qwen2.5-coder` or `llama3.2` for fast and private local distillation.*
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### 4. Index your Project
|
|
141
|
+
```bash
|
|
142
|
+
agent-coderag sync --all
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 5. Search
|
|
146
|
+
* **Human Mode (Compact)**: `agent-coderag search "how to handle errors"`
|
|
147
|
+
* **Agent Mode (JSON)**: `agent-coderag --json search "data storage" --limit 1`
|
|
148
|
+
|
|
149
|
+
### 🐳 Docker (Alternative)
|
|
150
|
+
```bash
|
|
151
|
+
docker build -t agent-coderag .
|
|
152
|
+
docker run -v ~/.cache/agent-coderag:/root/.cache/agent-coderag agent-coderag setup
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 🤖 For AI Agents
|
|
158
|
+
|
|
159
|
+
Agent-CodeRAG is built specifically for programmatic consumption.
|
|
160
|
+
|
|
161
|
+
### Agent Strategy
|
|
162
|
+
1. **Search First**: Use `agent-coderag --json search "topic"` to find relevant code units before reading files.
|
|
163
|
+
2. **Use Intent**: The `summary` field provides technical intent, allowing you to skip reading complex implementation details.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🔧 Development
|
|
168
|
+
|
|
169
|
+
### Running Tests
|
|
170
|
+
```bash
|
|
171
|
+
pytest tests/
|
|
172
|
+
pytest e2e_tests/
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Pre-commit Hooks
|
|
176
|
+
We use `pre-commit` to maintain high code standards:
|
|
177
|
+
```bash
|
|
178
|
+
pip install pre-commit
|
|
179
|
+
pre-commit install
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 📄 License
|
|
185
|
+
MIT © 2026 Igor Boloban
|
|
186
|
+
|
|
187
|
+
## 🙏 Acknowledgments
|
|
188
|
+
This project stands on the shoulders of giants. See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) for a full list of open-source libraries used in Agent-CodeRAG.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
agent_coderag-1.1.0.dist-info/licenses/LICENSE,sha256=OV_lazXI3hc0Pqgm9hZhnJpHRUXWK4GS3sxaWmCDSHY,1069
|
|
2
|
+
agent_coderag-1.1.0.dist-info/licenses/NOTICE,sha256=Ssc-bWjkZBz0sx1egO429_fmuKz9_vZxmO2frUZe5uk,802
|
|
3
|
+
code_rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
code_rag/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
code_rag/core/interfaces.py,sha256=V9f2JLBxx2R9zwC8jk2gQGRS1ZC7UNh8C0pYxBKfFG4,867
|
|
6
|
+
code_rag/core/manager.py,sha256=W_G8AeKf63pGEhqByB0DQUfiZJCf-wcn2btCzqH-wSo,2832
|
|
7
|
+
code_rag/core/models.py,sha256=m9qd_eGqIhTgu4cHQGNfzIcvQk4XUz0mngHKgYc4QUM,881
|
|
8
|
+
code_rag/discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
code_rag/discovery/dependency.py,sha256=yatONVT_ubx0c_uaGQ9G5Yy1jgTivjtEsKcrLVuUsR4,1450
|
|
10
|
+
code_rag/entry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
code_rag/entry/cli.py,sha256=Tj3VCdc0VGJa9ypAiWdtkdo9eCwjK7-b-sHYOUIFv7A,7576
|
|
12
|
+
code_rag/intelligence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
code_rag/intelligence/distiller.py,sha256=P9NkgqSJJtAZm4Ngr98gW_WoP-Xk9gtvCfGp_q5jV9I,2732
|
|
14
|
+
code_rag/intelligence/embedder.py,sha256=hbgv8OiLkvFIsMgTxwBekDGC7Xu853Mt_zWHeex1nrA,4264
|
|
15
|
+
code_rag/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
code_rag/parsers/ast_index.py,sha256=1SCAR7glGesFtEg26-GGvzqqqsUD-i7nWiXodVYK2OE,2776
|
|
17
|
+
code_rag/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
code_rag/storage/duckdb_impl.py,sha256=kU87-4qaERGv9pcZzwUWilxhbBYvp6GQygl49GVeaJ4,3986
|
|
19
|
+
agent_coderag-1.1.0.dist-info/METADATA,sha256=Q7a5TmjFZlctVBEwBT1rWTa7z8_22DiywGKAhkOWH2E,6798
|
|
20
|
+
agent_coderag-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
agent_coderag-1.1.0.dist-info/entry_points.txt,sha256=qt05eUIhUxid27O-C1RK1-Dz9uvKqSBeGpbzZCkm1fk,58
|
|
22
|
+
agent_coderag-1.1.0.dist-info/top_level.txt,sha256=8W3_FE9oVHnpu46qinUXMuDnvAGnGWd1AHqsqvpIzOw,9
|
|
23
|
+
agent_coderag-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Boloban
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CodeRAG
|
|
2
|
+
Copyright 2026 Igor Boloban
|
|
3
|
+
|
|
4
|
+
This product includes software developed by the following third-party projects:
|
|
5
|
+
|
|
6
|
+
- DuckDB (MIT License) - https://duckdb.org/
|
|
7
|
+
- NumPy (BSD License) - https://numpy.org/
|
|
8
|
+
- LiteLLM (MIT License) - https://litellm.ai/
|
|
9
|
+
- ONNX Runtime (MIT License) - https://onnxruntime.ai/
|
|
10
|
+
- Tokenizers (Apache License 2.0) - https://github.com/huggingface/tokenizers
|
|
11
|
+
- Pydantic (MIT License) - https://github.com/pydantic/pydantic
|
|
12
|
+
- HTTPX (BSD License) - https://github.com/encode/httpx
|
|
13
|
+
- Aiofiles (Apache License 2.0) - https://github.com/Tinche/aiofiles
|
|
14
|
+
- Sentence Transformers (Apache License 2.0) - https://www.sbert.net/
|
|
15
|
+
|
|
16
|
+
This product also uses various other open-source libraries as listed in THIRD_PARTY_LICENSES.md.
|
|
17
|
+
All trademarks are the property of their respective owners.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
code_rag
|
code_rag/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from .models import KnowledgeUnit
|
|
4
|
+
|
|
5
|
+
class IParser(ABC):
|
|
6
|
+
"""Interface for extracting structure from code."""
|
|
7
|
+
@abstractmethod
|
|
8
|
+
async def distill_file(self, file_path: str) -> List[KnowledgeUnit]:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class IStorage(ABC):
|
|
12
|
+
"""Interface for storing the index."""
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def upsert_unit(self, unit: KnowledgeUnit):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def get_unit(self, unit_id: str) -> Optional[KnowledgeUnit]:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def search_units(self, query: str, limit: int = 5) -> List[KnowledgeUnit]:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class IIntelligence(ABC):
|
|
26
|
+
"""Interface for LLM-based analysis (distillation, embeddings)."""
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def summarize(self, code: str, unit_name: str) -> str:
|
|
29
|
+
pass
|
code_rag/core/manager.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import List
|
|
4
|
+
from .interfaces import IStorage, IParser, IIntelligence
|
|
5
|
+
from .models import KnowledgeUnit
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class CodeRAGManager:
|
|
10
|
+
"""
|
|
11
|
+
Orchestrates the RAG workflow: parsing, distillation, and storage.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, storage: IStorage, parser: IParser, intelligence: IIntelligence):
|
|
15
|
+
self.storage = storage
|
|
16
|
+
self.parser = parser
|
|
17
|
+
self.intelligence = intelligence
|
|
18
|
+
|
|
19
|
+
async def sync_file(self, file_path: str, force_distill: bool = False):
|
|
20
|
+
"""
|
|
21
|
+
Processes a single file and syncs it with the storage.
|
|
22
|
+
"""
|
|
23
|
+
# 1. Parse AST to get units
|
|
24
|
+
current_units = await self.parser.distill_file(file_path)
|
|
25
|
+
|
|
26
|
+
for unit in current_units:
|
|
27
|
+
# v5.40: Delta-distillation logic
|
|
28
|
+
raw_code = unit.metadata.pop("raw_code", "")
|
|
29
|
+
|
|
30
|
+
# 2. Get existing unit to check hash
|
|
31
|
+
existing_unit = await self.storage.get_unit(unit.id)
|
|
32
|
+
|
|
33
|
+
should_distill = force_distill
|
|
34
|
+
if not existing_unit:
|
|
35
|
+
should_distill = True
|
|
36
|
+
logger.info("New unit discovered: %s", unit.name)
|
|
37
|
+
elif existing_unit.code_hash != unit.code_hash:
|
|
38
|
+
should_distill = True
|
|
39
|
+
logger.info("Unit %s changed (hash mismatch)", unit.name)
|
|
40
|
+
elif not existing_unit.summary:
|
|
41
|
+
should_distill = True
|
|
42
|
+
logger.info("Summary missing for %s", unit.name)
|
|
43
|
+
|
|
44
|
+
if should_distill:
|
|
45
|
+
logger.info("Distilling summary for %s...", unit.id)
|
|
46
|
+
try:
|
|
47
|
+
unit.summary = await self.intelligence.summarize(raw_code, unit.name)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error("Failed to distill %s: %s", unit.name, e)
|
|
50
|
+
# Keep old summary if available, otherwise stay None
|
|
51
|
+
unit.summary = existing_unit.summary if existing_unit else None
|
|
52
|
+
else:
|
|
53
|
+
# Reuse existing summary if code hasn't changed
|
|
54
|
+
unit.summary = existing_unit.summary if existing_unit else None
|
|
55
|
+
|
|
56
|
+
# 3. Save to storage (includes embedding generation)
|
|
57
|
+
await self.storage.upsert_unit(unit)
|
|
58
|
+
|
|
59
|
+
async def search(self, query: str, limit: int = 5) -> List[KnowledgeUnit]:
|
|
60
|
+
"""
|
|
61
|
+
Performs semantic search across all indexed units.
|
|
62
|
+
"""
|
|
63
|
+
return await self.storage.search_units(query, limit=limit)
|
|
64
|
+
|
|
65
|
+
async def sync_project(self, paths: List[str], force_distill: bool = False):
|
|
66
|
+
"""
|
|
67
|
+
Concurrent synchronization of multiple files.
|
|
68
|
+
"""
|
|
69
|
+
tasks = [self.sync_file(p, force_distill=force_distill) for p in paths]
|
|
70
|
+
await asyncio.gather(*tasks)
|
code_rag/core/models.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
class UnitKind(str, Enum):
|
|
6
|
+
MODULE = "module"
|
|
7
|
+
CLASS = "class"
|
|
8
|
+
FUNCTION = "function"
|
|
9
|
+
METHOD = "method"
|
|
10
|
+
|
|
11
|
+
class RelationType(str, Enum):
|
|
12
|
+
IMPORTS = "imports" # used
|
|
13
|
+
CALLS = "calls" # used
|
|
14
|
+
INHERITS = "inherits" # used
|
|
15
|
+
DEFINES = "defines" # used
|
|
16
|
+
|
|
17
|
+
class KnowledgeUnit(BaseModel):
|
|
18
|
+
"""Represents a piece of code (function, class, module)."""
|
|
19
|
+
id: str
|
|
20
|
+
kind: UnitKind
|
|
21
|
+
name: str
|
|
22
|
+
path: str
|
|
23
|
+
signature: Optional[str] = None
|
|
24
|
+
summary: Optional[str] = None
|
|
25
|
+
code_hash: str
|
|
26
|
+
tags: List[str] = Field(default_factory=list)
|
|
27
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
class Relation(BaseModel):
|
|
30
|
+
"""Connection between knowledge units."""
|
|
31
|
+
from_id: str # used
|
|
32
|
+
to_id: str # used
|
|
33
|
+
type: RelationType # used
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
def _get_method_signature(obj) -> str:
|
|
8
|
+
"""Helper to safely get a signature string."""
|
|
9
|
+
try:
|
|
10
|
+
return str(inspect.signature(obj))
|
|
11
|
+
except (ValueError, TypeError):
|
|
12
|
+
return "(...)"
|
|
13
|
+
|
|
14
|
+
async def extract_library_api(library_name: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Extracts the public API (classes, methods) of an installed library using introspection.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
lib = importlib.import_module(library_name)
|
|
20
|
+
output = [f"# Public API for '{library_name}':"]
|
|
21
|
+
|
|
22
|
+
for name, obj in inspect.getmembers(lib):
|
|
23
|
+
if name.startswith("_"):
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
if inspect.isclass(obj):
|
|
27
|
+
output.append(f"- **Class: {name}**")
|
|
28
|
+
for m_name, m_obj in inspect.getmembers(obj):
|
|
29
|
+
if m_name.startswith("_"):
|
|
30
|
+
continue
|
|
31
|
+
if inspect.isfunction(m_obj) or inspect.ismethod(m_obj):
|
|
32
|
+
sig = _get_method_signature(m_obj)
|
|
33
|
+
output.append(f" - `{m_name}{sig}`")
|
|
34
|
+
elif inspect.isfunction(obj) or inspect.isbuiltin(obj):
|
|
35
|
+
sig = _get_method_signature(obj)
|
|
36
|
+
output.append(f"- **Function: {name}{sig}**")
|
|
37
|
+
|
|
38
|
+
return "\n".join(output[:100]) # Limit output length
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return f"Failed to extract API for '{library_name}': {e}"
|
|
File without changes
|
code_rag/entry/cli.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
# Suppress external library noise before they are imported by other modules
|
|
12
|
+
os.environ["LITELLM_VERBOSE"] = "FALSE"
|
|
13
|
+
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
|
|
14
|
+
logging.getLogger("onnxruntime").setLevel(logging.ERROR)
|
|
15
|
+
|
|
16
|
+
# pylint: disable=wrong-import-position
|
|
17
|
+
from ..core.manager import CodeRAGManager
|
|
18
|
+
from ..storage.duckdb_impl import DuckDBStorage
|
|
19
|
+
from ..parsers.ast_index import AstIndexParser
|
|
20
|
+
from ..intelligence.embedder import Embedder, get_default_model_dir
|
|
21
|
+
from ..intelligence.distiller import Distiller, DistillerConfig
|
|
22
|
+
from ..discovery.dependency import extract_library_api
|
|
23
|
+
# pylint: enable=wrong-import-position
|
|
24
|
+
|
|
25
|
+
# Setup basic logging - default to WARNING for clean output
|
|
26
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
|
|
27
|
+
logger = logging.getLogger("agent-coderag")
|
|
28
|
+
|
|
29
|
+
def get_manager(db_path: str, onnx_path: Optional[str] = None, verbose: bool = False):
|
|
30
|
+
"""Initializes the RAG manager."""
|
|
31
|
+
if verbose:
|
|
32
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
33
|
+
|
|
34
|
+
config = DistillerConfig.load()
|
|
35
|
+
config.api_base = os.getenv("AGENT_PROXY_URL", config.api_base)
|
|
36
|
+
config.api_key = os.getenv("AGENT_PROXY_KEY", config.api_key)
|
|
37
|
+
config.model = os.getenv("AGENT_MODEL", config.model)
|
|
38
|
+
config.provider = os.getenv("AGENT_PROVIDER", config.provider)
|
|
39
|
+
|
|
40
|
+
embedder = Embedder(model_path=onnx_path)
|
|
41
|
+
storage = DuckDBStorage(db_path, embedder=embedder)
|
|
42
|
+
parser = AstIndexParser()
|
|
43
|
+
distiller = Distiller(config)
|
|
44
|
+
|
|
45
|
+
return CodeRAGManager(storage, parser, distiller)
|
|
46
|
+
|
|
47
|
+
def should_index(path: Path) -> bool:
|
|
48
|
+
"""Filters files that should NOT be indexed."""
|
|
49
|
+
p_str = str(path)
|
|
50
|
+
exclude_patterns = ["tests/", "venv/", "__pycache__/", ".git/"]
|
|
51
|
+
for pattern in exclude_patterns:
|
|
52
|
+
if pattern in p_str:
|
|
53
|
+
return False
|
|
54
|
+
return path.suffix == ".py"
|
|
55
|
+
|
|
56
|
+
async def sync_cmd(args):
|
|
57
|
+
manager = get_manager(args.db, args.onnx, args.verbose)
|
|
58
|
+
if args.path:
|
|
59
|
+
target_path = Path(args.path)
|
|
60
|
+
if target_path.is_file():
|
|
61
|
+
await manager.sync_file(str(target_path), force_distill=args.force)
|
|
62
|
+
else:
|
|
63
|
+
paths = [str(p) for p in target_path.rglob("*.py") if should_index(p)]
|
|
64
|
+
if args.verbose:
|
|
65
|
+
logger.info("Indexing %d files...", len(paths))
|
|
66
|
+
await manager.sync_project(paths, force_distill=args.force)
|
|
67
|
+
elif args.all:
|
|
68
|
+
paths = [str(p) for p in Path(".").rglob("*.py") if should_index(p)]
|
|
69
|
+
if args.verbose:
|
|
70
|
+
logger.info("Indexing %d files...", len(paths))
|
|
71
|
+
await manager.sync_project(paths, force_distill=args.force)
|
|
72
|
+
|
|
73
|
+
if not args.json:
|
|
74
|
+
print("Done.")
|
|
75
|
+
else:
|
|
76
|
+
print(json.dumps({"status": "success"}))
|
|
77
|
+
|
|
78
|
+
async def search_cmd(args):
|
|
79
|
+
manager = get_manager(args.db, args.onnx, args.verbose)
|
|
80
|
+
results = await manager.search(args.query, limit=args.limit)
|
|
81
|
+
|
|
82
|
+
if args.json:
|
|
83
|
+
output = [unit.model_dump() for unit in results]
|
|
84
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if not results:
|
|
88
|
+
print("No results.")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
for unit in results:
|
|
92
|
+
print(f"[{unit.kind.value}] {unit.name} | {unit.path}")
|
|
93
|
+
if unit.summary:
|
|
94
|
+
print(f" {unit.summary}")
|
|
95
|
+
print("-" * 20)
|
|
96
|
+
|
|
97
|
+
async def api_cmd(args):
|
|
98
|
+
output = await extract_library_api(args.library)
|
|
99
|
+
if args.json:
|
|
100
|
+
print(json.dumps({"api": output}))
|
|
101
|
+
else:
|
|
102
|
+
print(output)
|
|
103
|
+
|
|
104
|
+
async def download_file(url: str, dest: Path):
|
|
105
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
107
|
+
async with client.stream("GET", url) as response:
|
|
108
|
+
if response.status_code != 200:
|
|
109
|
+
return False
|
|
110
|
+
with open(dest, "wb") as f:
|
|
111
|
+
async for chunk in response.aiter_bytes():
|
|
112
|
+
f.write(chunk)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
async def setup_cmd(args):
|
|
116
|
+
dest_dir = get_default_model_dir()
|
|
117
|
+
base_url = "https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/resolve/main"
|
|
118
|
+
files = {
|
|
119
|
+
"model.onnx": f"{base_url}/onnx/model.onnx",
|
|
120
|
+
"tokenizer.json": f"{base_url}/tokenizer.json"
|
|
121
|
+
}
|
|
122
|
+
force = getattr(args, "force", False)
|
|
123
|
+
for filename, url in files.items():
|
|
124
|
+
dest_path = dest_dir / filename
|
|
125
|
+
if not dest_path.exists() or force:
|
|
126
|
+
await download_file(url, dest_path)
|
|
127
|
+
print("Setup complete.")
|
|
128
|
+
|
|
129
|
+
def config_cmd(args):
|
|
130
|
+
config = DistillerConfig.load()
|
|
131
|
+
if args.url:
|
|
132
|
+
config.api_base = args.url
|
|
133
|
+
if args.key:
|
|
134
|
+
config.api_key = args.key
|
|
135
|
+
if args.model:
|
|
136
|
+
config.model = args.model
|
|
137
|
+
if args.provider:
|
|
138
|
+
config.provider = args.provider
|
|
139
|
+
config.save()
|
|
140
|
+
if args.json:
|
|
141
|
+
print(json.dumps(config.model_dump()))
|
|
142
|
+
else:
|
|
143
|
+
print("Config updated.")
|
|
144
|
+
|
|
145
|
+
async def rebuild_cmd(args):
|
|
146
|
+
db_path = Path(args.db)
|
|
147
|
+
if db_path.exists():
|
|
148
|
+
if args.verbose:
|
|
149
|
+
logger.info("Removing old database: %s", db_path)
|
|
150
|
+
db_path.unlink()
|
|
151
|
+
|
|
152
|
+
# Trigger full sync
|
|
153
|
+
args.all = True
|
|
154
|
+
args.force = True
|
|
155
|
+
args.path = None
|
|
156
|
+
await sync_cmd(args)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
parser = argparse.ArgumentParser(description="Agent-CodeRAG CLI Tool")
|
|
161
|
+
parser.add_argument("--db", default=".code_rag.db", help="Path to DuckDB database")
|
|
162
|
+
parser.add_argument("--onnx", help="Path to ONNX embedding model")
|
|
163
|
+
parser.add_argument("--verbose", action="store_true", help="Show debug logs")
|
|
164
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
165
|
+
|
|
166
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
167
|
+
|
|
168
|
+
# Commands
|
|
169
|
+
subparsers.add_parser("setup", help="Download models")
|
|
170
|
+
subparsers.add_parser("rebuild", help="Nuke database and re-index everything")
|
|
171
|
+
|
|
172
|
+
conf_p = subparsers.add_parser("config", help="AI settings")
|
|
173
|
+
conf_p.add_argument("--url", help="API base URL")
|
|
174
|
+
conf_p.add_argument("--key", help="API key")
|
|
175
|
+
conf_p.add_argument("--model", help="Model name")
|
|
176
|
+
conf_p.add_argument("--provider", help="Provider")
|
|
177
|
+
|
|
178
|
+
sync_p = subparsers.add_parser("sync", help="Index files")
|
|
179
|
+
sync_p.add_argument("path", nargs="?", help="Path to index")
|
|
180
|
+
sync_p.add_argument("--all", action="store_true", help="Index all python files")
|
|
181
|
+
sync_p.add_argument("--force", action="store_true", help="Force distillation")
|
|
182
|
+
|
|
183
|
+
search_p = subparsers.add_parser("search", help="Semantic search")
|
|
184
|
+
search_p.add_argument("query", help="Query")
|
|
185
|
+
search_p.add_argument("--limit", type=int, default=5, help="Max results")
|
|
186
|
+
|
|
187
|
+
api_p = subparsers.add_parser("api", help="Discover library API")
|
|
188
|
+
api_p.add_argument("library", help="Library name")
|
|
189
|
+
|
|
190
|
+
args = parser.parse_args()
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
if args.command == "setup":
|
|
194
|
+
asyncio.run(setup_cmd(args))
|
|
195
|
+
elif args.command == "config":
|
|
196
|
+
config_cmd(args)
|
|
197
|
+
elif args.command == "rebuild":
|
|
198
|
+
asyncio.run(rebuild_cmd(args))
|
|
199
|
+
elif args.command == "sync":
|
|
200
|
+
asyncio.run(sync_cmd(args))
|
|
201
|
+
elif args.command == "search":
|
|
202
|
+
asyncio.run(search_cmd(args))
|
|
203
|
+
elif args.command == "api":
|
|
204
|
+
asyncio.run(api_cmd(args))
|
|
205
|
+
except Exception as e:
|
|
206
|
+
if args.json:
|
|
207
|
+
print(json.dumps({"error": str(e)}))
|
|
208
|
+
else:
|
|
209
|
+
logger.error(e)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
import litellm
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from ..core.interfaces import IIntelligence
|
|
6
|
+
from .embedder import get_global_dir
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class DistillerConfig(BaseModel):
|
|
11
|
+
model: str = "auto"
|
|
12
|
+
api_base: str = "http://localhost:8081/api/v1"
|
|
13
|
+
api_key: str = "sk-not-required"
|
|
14
|
+
provider: str = "openai"
|
|
15
|
+
temperature: float = 0.0
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def load(cls) -> "DistillerConfig":
|
|
19
|
+
"""Loads config from the global agent-coderag directory."""
|
|
20
|
+
config_path = get_global_dir() / "config.json"
|
|
21
|
+
if config_path.exists():
|
|
22
|
+
try:
|
|
23
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
24
|
+
data = json.load(f)
|
|
25
|
+
return cls(**data)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.error("Failed to load config from %s: %s", config_path, e)
|
|
28
|
+
return cls()
|
|
29
|
+
|
|
30
|
+
def save(self):
|
|
31
|
+
"""Saves current config to the global agent-coderag directory."""
|
|
32
|
+
config_path = get_global_dir() / "config.json"
|
|
33
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
try:
|
|
35
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(self.model_dump(), f, indent=4)
|
|
37
|
+
logger.info("Config saved to %s", config_path)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error("Failed to save config to %s: %s", config_path, e)
|
|
40
|
+
|
|
41
|
+
class Distiller(IIntelligence):
|
|
42
|
+
"""
|
|
43
|
+
LLM-based code analyst that extracts the 'intent' from raw code.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: DistillerConfig):
|
|
47
|
+
self.config = config
|
|
48
|
+
|
|
49
|
+
async def summarize(self, code: str, unit_name: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Generates a concise technical summary of what the code DOES.
|
|
52
|
+
"""
|
|
53
|
+
prompt = f"""
|
|
54
|
+
Analyze the following code block for '{unit_name}'.
|
|
55
|
+
Provide a concise, 1-2 sentence technical description of its core logic and intent.
|
|
56
|
+
Focus on WHAT it accomplishes and its role in the system.
|
|
57
|
+
DO NOT repeat the signature.
|
|
58
|
+
DO NOT include docstrings or comments in your summary.
|
|
59
|
+
|
|
60
|
+
CODE:
|
|
61
|
+
{code}
|
|
62
|
+
|
|
63
|
+
SUMMARY:
|
|
64
|
+
"""
|
|
65
|
+
model_id = self.config.model
|
|
66
|
+
if self.config.provider == "ollama" and not model_id.startswith("ollama/"):
|
|
67
|
+
model_id = f"ollama/{model_id}"
|
|
68
|
+
|
|
69
|
+
response = await litellm.acompletion(
|
|
70
|
+
model=model_id,
|
|
71
|
+
messages=[{"role": "user", "content": prompt}],
|
|
72
|
+
api_base=self.config.api_base,
|
|
73
|
+
api_key=self.config.api_key,
|
|
74
|
+
temperature=self.config.temperature,
|
|
75
|
+
timeout=30,
|
|
76
|
+
custom_llm_provider=self.config.provider
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
summary = response.choices[0].message.content.strip()
|
|
80
|
+
return summary
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import onnxruntime as ort
|
|
7
|
+
from tokenizers import Tokenizer
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
def get_global_dir() -> Path:
|
|
12
|
+
"""Returns the default global directory for agent-coderag data (cross-platform)."""
|
|
13
|
+
if os.name == 'nt': # Windows
|
|
14
|
+
base_dir = Path(os.environ.get('LOCALAPPDATA', Path.home() / 'AppData' / 'Local'))
|
|
15
|
+
else: # Linux/macOS
|
|
16
|
+
base_dir = Path(os.environ.get('XDG_CACHE_HOME', Path.home() / '.cache'))
|
|
17
|
+
|
|
18
|
+
return base_dir / "agent-coderag"
|
|
19
|
+
|
|
20
|
+
def get_default_model_dir() -> Path:
|
|
21
|
+
"""Returns the default global directory for models."""
|
|
22
|
+
return get_global_dir() / "models" / "mini-lm"
|
|
23
|
+
|
|
24
|
+
class Embedder:
|
|
25
|
+
"""
|
|
26
|
+
Local multilingual embedder using ONNX Runtime and Tokenizers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, model_path: Optional[str] = None):
|
|
30
|
+
self.model_path = model_path
|
|
31
|
+
self.session: Optional[ort.InferenceSession] = None
|
|
32
|
+
self.tokenizer: Optional[Tokenizer] = None
|
|
33
|
+
|
|
34
|
+
if not self.model_path:
|
|
35
|
+
global_dir = get_default_model_dir()
|
|
36
|
+
potential_path = global_dir / "model.onnx"
|
|
37
|
+
if potential_path.exists():
|
|
38
|
+
self.model_path = str(potential_path)
|
|
39
|
+
logger.info("Using global model from %s", self.model_path)
|
|
40
|
+
else:
|
|
41
|
+
logger.warning("No model found at %s. Please run 'agent-coderag setup'.", potential_path)
|
|
42
|
+
|
|
43
|
+
if self.model_path and os.path.exists(self.model_path):
|
|
44
|
+
self._init_tokenizer()
|
|
45
|
+
self._init_session()
|
|
46
|
+
|
|
47
|
+
def _init_tokenizer(self):
|
|
48
|
+
if not self.model_path:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
model_dir = os.path.dirname(self.model_path)
|
|
52
|
+
tokenizer_file = os.path.join(model_dir, "tokenizer.json")
|
|
53
|
+
if not os.path.exists(tokenizer_file):
|
|
54
|
+
tokenizer_file = os.path.join(os.path.dirname(model_dir), "tokenizer.json")
|
|
55
|
+
|
|
56
|
+
if os.path.exists(tokenizer_file):
|
|
57
|
+
try:
|
|
58
|
+
self.tokenizer = Tokenizer.from_file(tokenizer_file)
|
|
59
|
+
self.tokenizer.enable_padding(pad_id=0, pad_token="[PAD]") # nosec B106
|
|
60
|
+
self.tokenizer.enable_truncation(max_length=512)
|
|
61
|
+
logger.debug("Loaded tokenizer from %s", tokenizer_file)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error("Failed to load tokenizer from %s: %s", tokenizer_file, e)
|
|
64
|
+
else:
|
|
65
|
+
logger.error("tokenizer.json not found near %s", self.model_path)
|
|
66
|
+
|
|
67
|
+
def _init_session(self):
|
|
68
|
+
if not self.model_path:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
self.session = ort.InferenceSession(self.model_path, providers=['CPUExecutionProvider'])
|
|
73
|
+
logger.info("ONNX session initialized with model: %s", self.model_path)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error("Failed to initialize ONNX session: %s", e)
|
|
76
|
+
|
|
77
|
+
def embed(self, texts: List[str]) -> np.ndarray:
|
|
78
|
+
if not self.session or not self.tokenizer:
|
|
79
|
+
return np.zeros((len(texts), 384), dtype=np.float32)
|
|
80
|
+
|
|
81
|
+
encodings = self.tokenizer.encode_batch(texts)
|
|
82
|
+
input_ids = np.array([e.ids for e in encodings], dtype=np.int64)
|
|
83
|
+
attention_mask = np.array([e.attention_mask for e in encodings], dtype=np.int64)
|
|
84
|
+
|
|
85
|
+
inputs = {"input_ids": input_ids, "attention_mask": attention_mask}
|
|
86
|
+
model_inputs = [i.name for i in self.session.get_inputs()]
|
|
87
|
+
if "token_type_ids" in model_inputs:
|
|
88
|
+
inputs["token_type_ids"] = np.array([e.type_ids for e in encodings], dtype=np.int64)
|
|
89
|
+
|
|
90
|
+
outputs = self.session.run(None, inputs)
|
|
91
|
+
embeddings = self._mean_pooling(outputs[0], attention_mask)
|
|
92
|
+
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
|
|
93
|
+
norms = np.clip(norms, a_min=1e-12, a_max=None)
|
|
94
|
+
return embeddings / norms
|
|
95
|
+
|
|
96
|
+
def _mean_pooling(self, last_hidden_state, attention_mask):
|
|
97
|
+
input_mask_expanded = np.expand_dims(attention_mask, -1).astype(float)
|
|
98
|
+
sum_embeddings = np.sum(last_hidden_state * input_mask_expanded, axis=1)
|
|
99
|
+
sum_mask = np.clip(input_mask_expanded.sum(axis=1), a_min=1e-9, a_max=None)
|
|
100
|
+
return sum_embeddings / sum_mask
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from ..core.interfaces import IParser
|
|
7
|
+
from ..core.models import KnowledgeUnit, UnitKind
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class AstIndexParser(IParser):
|
|
12
|
+
"""
|
|
13
|
+
Parses Python code using the built-in AST module to extract units.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
async def distill_file(self, file_path: str) -> List[KnowledgeUnit]:
|
|
17
|
+
"""
|
|
18
|
+
Parses a file and returns a list of knowledge units.
|
|
19
|
+
"""
|
|
20
|
+
if not os.path.exists(file_path):
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
25
|
+
source = f.read()
|
|
26
|
+
|
|
27
|
+
tree = ast.parse(source)
|
|
28
|
+
units = []
|
|
29
|
+
|
|
30
|
+
# Module level unit
|
|
31
|
+
file_hash = hashlib.sha256(source.strip().encode()).hexdigest()
|
|
32
|
+
units.append(KnowledgeUnit(
|
|
33
|
+
id=f"{file_path}:module",
|
|
34
|
+
kind=UnitKind.MODULE,
|
|
35
|
+
name=os.path.basename(file_path),
|
|
36
|
+
path=file_path,
|
|
37
|
+
code_hash=file_hash,
|
|
38
|
+
metadata={"raw_code": source}
|
|
39
|
+
))
|
|
40
|
+
|
|
41
|
+
for node in ast.walk(tree):
|
|
42
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
43
|
+
unit = self._parse_node(node, file_path, source)
|
|
44
|
+
if unit:
|
|
45
|
+
units.append(unit)
|
|
46
|
+
|
|
47
|
+
return units
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error("Failed to parse %s: %s", file_path, e)
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
def _parse_node(self, node, file_path: str, source: str) -> Optional[KnowledgeUnit]:
|
|
53
|
+
"""Helper to create a KnowledgeUnit from an AST node."""
|
|
54
|
+
kind = UnitKind.FUNCTION
|
|
55
|
+
if isinstance(node, ast.ClassDef):
|
|
56
|
+
kind = UnitKind.CLASS
|
|
57
|
+
elif hasattr(node, "parent") and isinstance(node.parent, ast.ClassDef):
|
|
58
|
+
kind = UnitKind.METHOD
|
|
59
|
+
|
|
60
|
+
# Extract source code for the node
|
|
61
|
+
try:
|
|
62
|
+
node_source = ast.get_source_segment(source, node) or ""
|
|
63
|
+
node_hash = hashlib.sha256(node_source.strip().encode()).hexdigest()
|
|
64
|
+
|
|
65
|
+
# Simplified signature extraction
|
|
66
|
+
signature = None
|
|
67
|
+
if hasattr(node, "args"):
|
|
68
|
+
args = [arg.arg for arg in node.args.args]
|
|
69
|
+
signature = f"({', '.join(args)})"
|
|
70
|
+
|
|
71
|
+
return KnowledgeUnit(
|
|
72
|
+
id=f"{file_path}:{node.name}",
|
|
73
|
+
kind=kind,
|
|
74
|
+
name=node.name,
|
|
75
|
+
path=file_path,
|
|
76
|
+
signature=signature,
|
|
77
|
+
code_hash=node_hash,
|
|
78
|
+
metadata={"raw_code": node_source}
|
|
79
|
+
)
|
|
80
|
+
except Exception:
|
|
81
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import duckdb
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from ..core.interfaces import IStorage
|
|
6
|
+
from ..core.models import KnowledgeUnit, UnitKind
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class DuckDBStorage(IStorage):
|
|
11
|
+
"""
|
|
12
|
+
DuckDB-based storage with Vector Similarity Search (VSS) capabilities.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, db_path: str, embedder=None):
|
|
16
|
+
self.db_path = db_path
|
|
17
|
+
self.embedder = embedder
|
|
18
|
+
self.conn = duckdb.connect(self.db_path)
|
|
19
|
+
self._setup_db()
|
|
20
|
+
|
|
21
|
+
def _setup_db(self):
|
|
22
|
+
"""Initializes tables and extensions."""
|
|
23
|
+
self.conn.execute("INSTALL vss;")
|
|
24
|
+
self.conn.execute("LOAD vss;")
|
|
25
|
+
|
|
26
|
+
# Metadata table
|
|
27
|
+
self.conn.execute("""
|
|
28
|
+
CREATE TABLE IF NOT EXISTS units (
|
|
29
|
+
id VARCHAR PRIMARY KEY,
|
|
30
|
+
kind VARCHAR,
|
|
31
|
+
name VARCHAR,
|
|
32
|
+
path VARCHAR,
|
|
33
|
+
signature VARCHAR,
|
|
34
|
+
summary VARCHAR,
|
|
35
|
+
code_hash VARCHAR,
|
|
36
|
+
tags VARCHAR[],
|
|
37
|
+
metadata JSON
|
|
38
|
+
)
|
|
39
|
+
""")
|
|
40
|
+
|
|
41
|
+
# Vector table (MiniLM dimension is 384)
|
|
42
|
+
self.conn.execute("""
|
|
43
|
+
CREATE TABLE IF NOT EXISTS unit_embeddings (
|
|
44
|
+
id VARCHAR PRIMARY KEY,
|
|
45
|
+
vec FLOAT[384]
|
|
46
|
+
)
|
|
47
|
+
""")
|
|
48
|
+
logger.info("Storage initialized at %s", self.db_path)
|
|
49
|
+
|
|
50
|
+
async def upsert_unit(self, unit: KnowledgeUnit):
|
|
51
|
+
"""Inserts or updates a knowledge unit and its embedding."""
|
|
52
|
+
# 1. Upsert metadata
|
|
53
|
+
self.conn.execute("""
|
|
54
|
+
INSERT OR REPLACE INTO units (id, kind, name, path, signature, summary, code_hash, tags, metadata)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
56
|
+
""", [
|
|
57
|
+
unit.id, unit.kind.value, unit.name, unit.path,
|
|
58
|
+
unit.signature, unit.summary, unit.code_hash,
|
|
59
|
+
unit.tags, json.dumps(unit.metadata)
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
# 2. Update embedding
|
|
63
|
+
if self.embedder:
|
|
64
|
+
# Fallback to name/signature if summary is missing
|
|
65
|
+
text_to_embed = unit.summary or f"{unit.kind.value} {unit.name} {unit.signature or ''}"
|
|
66
|
+
vec = self.embedder.embed([text_to_embed])[0]
|
|
67
|
+
self.conn.execute("""
|
|
68
|
+
INSERT OR REPLACE INTO unit_embeddings (id, vec)
|
|
69
|
+
VALUES (?, ?)
|
|
70
|
+
""", [unit.id, vec.tolist()])
|
|
71
|
+
|
|
72
|
+
async def get_unit(self, unit_id: str) -> Optional[KnowledgeUnit]:
|
|
73
|
+
"""Retrieves a unit by its unique ID."""
|
|
74
|
+
res = self.conn.execute("SELECT * FROM units WHERE id = ?", [unit_id]).fetchone()
|
|
75
|
+
if not res:
|
|
76
|
+
return None
|
|
77
|
+
return self._map_row_to_unit(res)
|
|
78
|
+
|
|
79
|
+
async def search_units(self, query: str, limit: int = 5) -> List[KnowledgeUnit]:
|
|
80
|
+
"""Hybrid search using VSS and FTS."""
|
|
81
|
+
if not self.embedder:
|
|
82
|
+
# Fallback to basic text search if no embedder
|
|
83
|
+
res = self.conn.execute("""
|
|
84
|
+
SELECT * FROM units
|
|
85
|
+
WHERE name ILIKE ? OR summary ILIKE ?
|
|
86
|
+
LIMIT ?
|
|
87
|
+
""", [f"%{query}%", f"%{query}%", limit]).fetchall()
|
|
88
|
+
else:
|
|
89
|
+
query_vec = self.embedder.embed([query])[0]
|
|
90
|
+
res = self.conn.execute("""
|
|
91
|
+
SELECT u.*, array_distance(e.vec, ?::FLOAT[384]) as dist
|
|
92
|
+
FROM units u
|
|
93
|
+
JOIN unit_embeddings e ON u.id = e.id
|
|
94
|
+
ORDER BY dist ASC
|
|
95
|
+
LIMIT ?
|
|
96
|
+
""", [query_vec.tolist(), limit]).fetchall()
|
|
97
|
+
|
|
98
|
+
return [self._map_row_to_unit(row) for row in res]
|
|
99
|
+
|
|
100
|
+
def _map_row_to_unit(self, row) -> KnowledgeUnit:
|
|
101
|
+
return KnowledgeUnit(
|
|
102
|
+
id=row[0],
|
|
103
|
+
kind=UnitKind(row[1]),
|
|
104
|
+
name=row[2],
|
|
105
|
+
path=row[3],
|
|
106
|
+
signature=row[4],
|
|
107
|
+
summary=row[5],
|
|
108
|
+
code_hash=row[6],
|
|
109
|
+
tags=row[7] if row[7] else [],
|
|
110
|
+
metadata=json.loads(row[8]) if row[8] else {}
|
|
111
|
+
)
|