Kareman commited on
Commit
acc643d
·
0 Parent(s):

initial commit: full implemntation of git explorer project

Browse files
.gitignore ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual Environment
2
+ venv/
3
+ .venv/
4
+ env/
5
+
6
+ # Environment variables
7
+ .env
8
+ .env.*
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+
15
+ # Distribution / packaging
16
+ build/
17
+ dist/
18
+ *.egg-info/
19
+ .eggs/
20
+
21
+ # Pytest
22
+ .pytest_cache/
23
+ .coverage
24
+ htmlcov/
25
+
26
+ # MyPy
27
+ .mypy_cache/
28
+
29
+ # Ruff
30
+ .ruff_cache/
31
+
32
+ # IDEs
33
+ .vscode/
34
+ .idea/
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # Logs
41
+ *.log
42
+
43
+ # Local database files
44
+ *.db
45
+ *.sqlite3
46
+
47
+ # Jupyter
48
+ .ipynb_checkpoints/
49
+
50
+
51
+ # cloned repositories
52
+ storage/repos/
app/controllers/chat_controller.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.services.context_formatter import (
2
+ ContextFormatter,
3
+ )
4
+ from app.services.llm_service import (
5
+ LLMService,
6
+ )
7
+ from app.services.retrieval_service import (
8
+ RetrievalService,
9
+ )
10
+
11
+
12
+ class ChatController:
13
+
14
+ def __init__(self):
15
+
16
+ self.retrieval = RetrievalService()
17
+
18
+ self.formatter = ContextFormatter()
19
+
20
+ self.llm = LLMService()
21
+
22
+ def ask(
23
+ self,
24
+ repository_id: str,
25
+ question: str,
26
+ ):
27
+
28
+ context = self.retrieval.search(
29
+ repository_id=repository_id,
30
+ query=question,
31
+ )
32
+
33
+ formatted_context = self.formatter.format(
34
+ context,
35
+ )
36
+
37
+ return self.llm.chat(
38
+ question=question,
39
+ context=formatted_context,
40
+ )
app/controllers/repository_controller.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.core.config import REPOSITORIES_DIR
2
+
3
+ from app.services.github_service import GithubService
4
+ from app.services.metadata_service import MetadataService
5
+ from app.services.parser_service import ParserService
6
+ from app.services.index_service import IndexService
7
+ from app.services.graph_service import GraphService
8
+ from app.services.resolver_service import ResolverService
9
+ from app.services.embedding_service import EmbeddingService
10
+
11
+ from app.utils.file_utils import get_python_files
12
+
13
+ from app.core.constants import (
14
+ METADATA_FILE,
15
+ CODE_INDEX_FILE,
16
+ RESOLVED_CODE_FILE,
17
+ )
18
+
19
+
20
+ class RepositoryController:
21
+
22
+ def __init__(self):
23
+
24
+ self.github = GithubService()
25
+
26
+ self.metadata = MetadataService()
27
+
28
+ self.parser = ParserService()
29
+
30
+ self.index = IndexService()
31
+
32
+ self.resolver = ResolverService()
33
+
34
+ self.graph_service = GraphService()
35
+
36
+ self.embedding_service = EmbeddingService()
37
+
38
+ def clone(
39
+ self,
40
+ url: str,
41
+ ):
42
+
43
+ clone_result = self.github.clone(
44
+ url,
45
+ REPOSITORIES_DIR,
46
+ )
47
+
48
+ repository_id = clone_result["repository_id"]
49
+
50
+ repo_path = clone_result["repo_path"]
51
+
52
+ metadata = self.metadata.extract(
53
+ repo_path,
54
+ )
55
+
56
+ self.index.save(
57
+ repo_path,
58
+ METADATA_FILE,
59
+ metadata,
60
+ )
61
+
62
+ parsed_files = []
63
+
64
+ for file in get_python_files(repo_path):
65
+
66
+ parsed_files.append(
67
+ self.parser.parse_file(
68
+ file,
69
+ )
70
+ )
71
+
72
+ self.index.save(
73
+ repo_path,
74
+ CODE_INDEX_FILE,
75
+ parsed_files,
76
+ )
77
+
78
+ code_index = [
79
+ item.model_dump()
80
+ for item in parsed_files
81
+ ]
82
+
83
+ resolved = self.resolver.resolve(
84
+ code_index,
85
+ )
86
+
87
+ self.index.save(
88
+ repo_path,
89
+ RESOLVED_CODE_FILE,
90
+ resolved,
91
+ )
92
+
93
+ print("Building graph...")
94
+
95
+ self.graph_service.build(
96
+ repository_id=repository_id,
97
+ resolved=resolved,
98
+ )
99
+
100
+ print("Building embeddings...")
101
+
102
+ self.embedding_service.build(
103
+ repository_id=repository_id,
104
+ resolved=resolved,
105
+ )
106
+
107
+ print("Repository indexed successfully.")
108
+
109
+ metadata.repository_id = repository_id
110
+
111
+ return metadata
app/core/config.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import os
3
+ from app.core.settings import settings
4
+
5
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
6
+
7
+ REPOSITORIES_DIR = BASE_DIR / "storage" / "repos"
8
+
9
+ REPOSITORIES_DIR.mkdir(parents=True, exist_ok=True)
10
+
app/core/constants.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ METADATA_FILE = "metadata.json"
2
+
3
+ CODE_INDEX_FILE = "code.json"
4
+
5
+ GRAPH_FILE = "graph.json"
6
+
7
+ CHUNKS_FILE = "chunks.json"
8
+
9
+ EMBEDDINGS_FILE = "embeddings.json"
10
+
11
+ ENTITIES_FILE = "entities.json"
12
+
13
+ RESOLVED_CODE_FILE = "resolved_code.json"
14
+
15
+ GRAPH_FILE = "graph.json"
app/core/settings.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+
6
+ neo4j_uri: str
7
+ neo4j_username: str
8
+ neo4j_password: str
9
+
10
+ qdrant_url: str
11
+ qdrant_api_key: str = ""
12
+
13
+ model_config = SettingsConfigDict(
14
+ env_file=".env",
15
+ )
16
+ openrouter_api_key: str
17
+
18
+ llm_model: str
19
+
20
+
21
+ settings = Settings()
app/main.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from app.routes.repository_routes import router
3
+ from app.routes.chat_routes import router as chat_router
4
+
5
+
6
+ app = FastAPI(
7
+ title="Repository GraphRAG",
8
+ )
9
+
10
+ app.include_router(router)
11
+ app.include_router(
12
+ chat_router,
13
+ )
app/models/chat.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ChatRequest(BaseModel):
5
+
6
+ repository_id: str
7
+
8
+ question: str
app/models/code.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class ImportInfo(BaseModel):
5
+ module: str | None
6
+ name: str
7
+ alias: str | None = None
8
+
9
+
10
+ class CallInfo(BaseModel):
11
+ name: str
12
+ line: int
13
+ qualified_name: str | None = None
14
+
15
+
16
+ class FunctionInfo(BaseModel):
17
+ name: str
18
+
19
+ qualified_name: str
20
+
21
+ line: int
22
+
23
+ end_line: int
24
+
25
+ docstring: str | None
26
+
27
+ source_code: str
28
+
29
+ calls: list[CallInfo] = Field(default_factory=list)
30
+
31
+
32
+ class ClassInfo(BaseModel):
33
+
34
+ name: str
35
+
36
+ qualified_name: str
37
+
38
+ line: int
39
+
40
+ end_line: int
41
+
42
+ source_code: str
43
+
44
+ methods: list[FunctionInfo] = Field(default_factory=list)
45
+
46
+
47
+ class PythonFileInfo(BaseModel):
48
+ path: str
49
+
50
+ imports: list[ImportInfo] = Field(default_factory=list)
51
+
52
+ classes: list[ClassInfo] = Field(default_factory=list)
53
+
54
+ functions: list[FunctionInfo] = Field(default_factory=list)
55
+
app/models/graph_context.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class GraphNode(BaseModel):
5
+
6
+ qualified_name: str | None = None
7
+
8
+ labels: list[str]
9
+
10
+ properties: dict
11
+
12
+
13
+ class GraphEdge(BaseModel):
14
+
15
+ source: str
16
+
17
+ target: str
18
+
19
+ type: str
20
+
21
+
22
+ class GraphContext(BaseModel):
23
+
24
+ nodes: list[GraphNode]
25
+
26
+ edges: list[GraphEdge]
app/models/metadata.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class RepositoryInfo(BaseModel):
5
+ name: str
6
+ path: str
7
+ branch: str
8
+ latest_commit: str
9
+ size_mb: float
10
+
11
+
12
+ class StructureInfo(BaseModel):
13
+ total_files: int
14
+ python_files: int
15
+ directories: int
16
+
17
+
18
+ class ConfigurationInfo(BaseModel):
19
+ has_readme: bool
20
+ has_license: bool
21
+ has_dockerfile: bool
22
+ has_docker_compose: bool
23
+ requirements: list[str]
24
+
25
+
26
+ class RepositoryMetadata(BaseModel):
27
+
28
+ repository_id: str | None = None
29
+
30
+ repository: RepositoryInfo
31
+
32
+ structure: StructureInfo
33
+
34
+ configuration: ConfigurationInfo
app/models/requests.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from pydantic import BaseModel, HttpUrl
2
+
3
+
4
+ class CloneRepositoryRequest(BaseModel):
5
+ url: HttpUrl
app/models/responses.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ from app.models.metadata import RepositoryMetadata
4
+
5
+
6
+ class CloneRepositoryResponse(BaseModel):
7
+ metadata: RepositoryMetadata
app/models/symbols.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Symbol(BaseModel):
5
+
6
+ name: str
7
+
8
+ qualified_name: str
app/repositories/neo4j_repository.py ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from neo4j import GraphDatabase
2
+
3
+ from app.core.settings import settings
4
+ from app.models.graph_context import (
5
+ GraphContext,
6
+ GraphNode,
7
+ GraphEdge,
8
+ )
9
+
10
+
11
+ class Neo4jRepository:
12
+
13
+ _instance = None
14
+
15
+ def __new__(cls):
16
+
17
+ if cls._instance is None:
18
+
19
+ cls._instance = super().__new__(cls)
20
+
21
+ return cls._instance
22
+
23
+ def __init__(self):
24
+
25
+ if hasattr(self, "_initialized"):
26
+ return
27
+
28
+ self.driver = GraphDatabase.driver(
29
+ settings.neo4j_uri,
30
+ auth=(
31
+ settings.neo4j_username,
32
+ settings.neo4j_password,
33
+ ),
34
+ )
35
+
36
+ self._initialized = True
37
+
38
+ def close(self):
39
+
40
+ if hasattr(self, "driver"):
41
+
42
+ self.driver.close()
43
+
44
+ self._initialized = False
45
+
46
+ Neo4jRepository._instance = None
47
+
48
+ def execute(
49
+ self,
50
+ query,
51
+ **params,
52
+ ):
53
+
54
+ with self.driver.session() as session:
55
+
56
+ session.run(
57
+ query,
58
+ **params,
59
+ )
60
+
61
+ def merge_function(
62
+ self,
63
+ repository_id: str,
64
+ qualified_name: str,
65
+ name: str,
66
+ embedding: list[float] | None = None,
67
+ source_code: str | None = None,
68
+ ):
69
+
70
+ module = ".".join(
71
+ qualified_name.split(".")[:-1]
72
+ )
73
+
74
+ self.execute(
75
+ """
76
+ MERGE (f:Function {
77
+ repository_id: $repository_id,
78
+ qualified_name: $qualified_name
79
+ })
80
+
81
+ SET
82
+ f.name = $name,
83
+ f.module = $module,
84
+ f.embedding = $embedding,
85
+ f.source_code = $source_code
86
+ """,
87
+ repository_id=repository_id,
88
+ qualified_name=qualified_name,
89
+ name=name,
90
+ module=module,
91
+ embedding=embedding,
92
+ source_code=source_code,
93
+ )
94
+
95
+ def merge_class(
96
+ self,
97
+ repository_id: str,
98
+ qualified_name: str,
99
+ name: str,
100
+ embedding: list[float] | None = None,
101
+ source_code: str | None = None,
102
+ ):
103
+
104
+ module = ".".join(
105
+ qualified_name.split(".")[:-1]
106
+ )
107
+
108
+ self.execute(
109
+ """
110
+ MERGE (c:Class {
111
+ repository_id: $repository_id,
112
+ qualified_name: $qualified_name
113
+ })
114
+
115
+ SET
116
+ c.name = $name,
117
+ c.module = $module,
118
+ c.embedding = $embedding,
119
+ c.source_code = $source_code
120
+ """,
121
+ repository_id=repository_id,
122
+ qualified_name=qualified_name,
123
+ name=name,
124
+ module=module,
125
+ embedding=embedding,
126
+ source_code=source_code,
127
+ )
128
+
129
+ def merge_external_function(
130
+ self,
131
+ repository_id: str,
132
+ qualified_name: str,
133
+ embedding: list[float] | None = None,
134
+ ):
135
+
136
+ name = qualified_name.split(".")[-1]
137
+
138
+ module = ".".join(
139
+ qualified_name.split(".")[:-1]
140
+ )
141
+
142
+ package = qualified_name.split(".")[0]
143
+
144
+ self.execute(
145
+ """
146
+ MERGE (e:ExternalFunction {
147
+ repository_id: $repository_id,
148
+ qualified_name: $qualified_name
149
+ })
150
+
151
+ SET
152
+ e.name = $name,
153
+ e.module = $module,
154
+ e.package = $package,
155
+ e.embedding = $embedding
156
+ """,
157
+ repository_id=repository_id,
158
+ qualified_name=qualified_name,
159
+ name=name,
160
+ module=module,
161
+ package=package,
162
+ embedding=embedding,
163
+ )
164
+
165
+ def merge_call(
166
+ self,
167
+ repository_id: str,
168
+ source: str,
169
+ target: str,
170
+ ):
171
+
172
+ self.execute(
173
+ """
174
+ MATCH (s {
175
+ repository_id: $repository_id,
176
+ qualified_name: $source
177
+ })
178
+
179
+ MATCH (t {
180
+ repository_id: $repository_id,
181
+ qualified_name: $target
182
+ })
183
+
184
+ MERGE (s)-[:CALLS]->(t)
185
+ """,
186
+ repository_id=repository_id,
187
+ source=source,
188
+ target=target,
189
+ )
190
+ def merge_file(
191
+ self,
192
+ repository_id: str,
193
+ path: str,
194
+ ):
195
+
196
+ self.execute(
197
+ """
198
+ MERGE (f:File {
199
+ repository_id: $repository_id,
200
+ path: $path
201
+ })
202
+ """,
203
+ repository_id=repository_id,
204
+ path=path,
205
+ )
206
+ def create_declares(
207
+ self,
208
+ repository_id: str,
209
+ file_path: str,
210
+ qualified_name: str,
211
+ ):
212
+
213
+ self.execute(
214
+ """
215
+ MATCH (f:File {
216
+ repository_id: $repository_id,
217
+ path: $file_path
218
+ })
219
+
220
+ MATCH (n {
221
+ repository_id: $repository_id,
222
+ qualified_name: $qualified_name
223
+ })
224
+
225
+ MERGE (f)-[:DECLARES]->(n)
226
+ """,
227
+ repository_id=repository_id,
228
+ file_path=file_path,
229
+ qualified_name=qualified_name,
230
+ )
231
+
232
+ def create_has_method(
233
+ self,
234
+ repository_id: str,
235
+ class_name: str,
236
+ method_name: str,
237
+ ):
238
+
239
+ self.execute(
240
+ """
241
+ MATCH (c:Class {
242
+ repository_id: $repository_id,
243
+ qualified_name: $class_name
244
+ })
245
+
246
+ MATCH (m:Function {
247
+ repository_id: $repository_id,
248
+ qualified_name: $method_name
249
+ })
250
+
251
+ MERGE (c)-[:HAS_METHOD]->(m)
252
+ """,
253
+ repository_id=repository_id,
254
+ class_name=class_name,
255
+ method_name=method_name,
256
+ )
257
+ def merge_module(
258
+ self,
259
+ repository_id: str,
260
+ module: str,
261
+ ):
262
+
263
+ package = module.split(".")[0]
264
+
265
+ is_internal = module.startswith("app")
266
+
267
+ self.execute(
268
+ """
269
+ MERGE (m:Module {
270
+ repository_id: $repository_id,
271
+ name: $module
272
+ })
273
+
274
+ SET
275
+ m.package = $package,
276
+ m.is_internal = $is_internal
277
+ """,
278
+ repository_id=repository_id,
279
+ module=module,
280
+ package=package,
281
+ is_internal=is_internal,
282
+ )
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+ def create_import(
292
+ self,
293
+ repository_id: str,
294
+ file_path: str,
295
+ module: str,
296
+ symbol: str | None,
297
+ alias: str | None,
298
+ ):
299
+
300
+ self.execute(
301
+ """
302
+ MATCH (f:File {
303
+ repository_id:$repository_id,
304
+ path:$file_path
305
+ })
306
+
307
+ MATCH (m:Module {
308
+ repository_id:$repository_id,
309
+ name:$module
310
+ })
311
+
312
+ CREATE (f)-[r:IMPORTS]->(m)
313
+
314
+ SET
315
+ r.symbol=$symbol,
316
+ r.alias=$alias
317
+ """,
318
+ repository_id=repository_id,
319
+ file_path=file_path,
320
+ module=module,
321
+ symbol=symbol,
322
+ alias=alias,
323
+ )
324
+ def create_belongs_to(
325
+ self,
326
+ repository_id: str,
327
+ qualified_name: str,
328
+ ):
329
+
330
+ module = ".".join(
331
+ qualified_name.split(".")[:-1]
332
+ )
333
+
334
+ if not module:
335
+ return
336
+
337
+ self.merge_module(
338
+ repository_id,
339
+ module,
340
+ )
341
+
342
+ self.execute(
343
+ """
344
+ MATCH (f:ExternalFunction {
345
+ repository_id:$repository_id,
346
+ qualified_name:$qualified_name
347
+ })
348
+
349
+ MATCH (m:Module {
350
+ repository_id:$repository_id,
351
+ name:$module
352
+ })
353
+
354
+ MERGE (f)-[:BELONGS_TO]->(m)
355
+ """,
356
+ repository_id=repository_id,
357
+ qualified_name=qualified_name,
358
+ module=module,
359
+ )
360
+
361
+ def clear_database(self):
362
+
363
+ self.execute(
364
+ """
365
+ MATCH (n)
366
+
367
+ DETACH DELETE n
368
+ """
369
+ )
370
+ def create_vector_indexes(self):
371
+
372
+ self.execute(
373
+ """
374
+ CREATE VECTOR INDEX function_embeddings
375
+ IF NOT EXISTS
376
+
377
+ FOR (f:Function)
378
+
379
+ ON (f.embedding)
380
+
381
+ OPTIONS {
382
+
383
+ indexConfig: {
384
+
385
+ `vector.dimensions`: 384,
386
+
387
+ `vector.similarity_function`: 'cosine'
388
+
389
+ }
390
+
391
+ }
392
+ """
393
+ )
394
+
395
+ self.execute(
396
+ """
397
+ CREATE VECTOR INDEX class_embeddings
398
+ IF NOT EXISTS
399
+
400
+ FOR (c:Class)
401
+
402
+ ON (c.embedding)
403
+
404
+ OPTIONS {
405
+
406
+ indexConfig: {
407
+
408
+ `vector.dimensions`: 384,
409
+
410
+ `vector.similarity_function`: 'cosine'
411
+
412
+ }
413
+
414
+ }
415
+ """
416
+ )
417
+ def update_embedding(
418
+ self,
419
+ repository_id: str,
420
+ qualified_name: str,
421
+ embedding: list[float],
422
+ ):
423
+
424
+ self.execute(
425
+ """
426
+ MATCH (n {
427
+ repository_id:$repository_id,
428
+ qualified_name:$qualified_name
429
+ })
430
+
431
+ SET
432
+ n.embedding=$embedding
433
+ """,
434
+ repository_id=repository_id,
435
+ qualified_name=qualified_name,
436
+ embedding=embedding,
437
+ )
438
+
439
+
440
+ def retrieve_context(
441
+ self,
442
+ repository_id: str,
443
+ embedding: list[float],
444
+ limit: int = 5,
445
+ hops: int = 1,
446
+ ):
447
+
448
+ query = f"""
449
+ CALL {{
450
+ CALL db.index.vector.queryNodes(
451
+ 'function_embeddings',
452
+ $limit,
453
+ $embedding
454
+ )
455
+ YIELD node, score
456
+
457
+ WHERE node.repository_id = $repository_id
458
+
459
+ RETURN node, score
460
+
461
+ UNION
462
+
463
+ CALL db.index.vector.queryNodes(
464
+ 'class_embeddings',
465
+ $limit,
466
+ $embedding
467
+ )
468
+ YIELD node, score
469
+
470
+ WHERE node.repository_id = $repository_id
471
+
472
+ RETURN node, score
473
+ }}
474
+
475
+ WITH collect(DISTINCT node) AS seeds
476
+
477
+ UNWIND seeds AS seed
478
+
479
+ WITH seed
480
+ WHERE seed.repository_id = $repository_id
481
+
482
+ MATCH path=(seed)-[*0..{hops}]-(neighbor)
483
+
484
+ WHERE neighbor.repository_id = $repository_id
485
+
486
+ WITH
487
+ collect(DISTINCT neighbor) AS nodes,
488
+ collect(DISTINCT relationships(path)) AS rel_lists
489
+
490
+ UNWIND rel_lists AS rel_list
491
+ UNWIND rel_list AS rel
492
+
493
+ RETURN
494
+ nodes,
495
+ collect(
496
+ DISTINCT {{
497
+ source: coalesce(
498
+ startNode(rel).qualified_name,
499
+ startNode(rel).name,
500
+ startNode(rel).path
501
+ ),
502
+ target: coalesce(
503
+ endNode(rel).qualified_name,
504
+ endNode(rel).name,
505
+ endNode(rel).path
506
+ ),
507
+ type: type(rel)
508
+ }}
509
+ ) AS edges
510
+ """
511
+
512
+ with self.driver.session() as session:
513
+
514
+ record = session.run(
515
+ query,
516
+ repository_id=repository_id,
517
+ embedding=embedding,
518
+ limit=limit,
519
+ ).single()
520
+
521
+ if record is None:
522
+ return GraphContext(
523
+ nodes=[],
524
+ edges=[],
525
+ )
526
+
527
+ nodes = []
528
+
529
+ for node in record["nodes"]:
530
+ nodes.append(
531
+ GraphNode(
532
+ qualified_name=node.get("qualified_name"),
533
+ labels=list(node.labels),
534
+ properties=dict(node),
535
+ )
536
+ )
537
+
538
+ edges = []
539
+
540
+ for edge in record["edges"]:
541
+
542
+ if edge["source"] is None or edge["target"] is None:
543
+ continue
544
+
545
+ edges.append(
546
+ GraphEdge(
547
+ source=edge["source"],
548
+ target=edge["target"],
549
+ type=edge["type"],
550
+ )
551
+ )
552
+
553
+ return GraphContext(
554
+ nodes=nodes,
555
+ edges=edges,
556
+ )
app/routes/chat_routes.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ from app.controllers.chat_controller import ChatController
4
+ from app.models.chat import ChatRequest
5
+
6
+
7
+ router = APIRouter()
8
+
9
+ controller = ChatController()
10
+
11
+
12
+ @router.post("/chat")
13
+ def chat(
14
+ request: ChatRequest,
15
+ ):
16
+
17
+ return controller.ask(
18
+ repository_id=request.repository_id,
19
+ question=request.question,
20
+ )
app/routes/repository_routes.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ from app.controllers.repository_controller import RepositoryController
4
+ from app.models.requests import CloneRepositoryRequest
5
+ from app.models.responses import CloneRepositoryResponse
6
+
7
+ router = APIRouter(prefix="/repositories", tags=["Repositories"])
8
+
9
+ controller = RepositoryController()
10
+
11
+
12
+ @router.post("", response_model=CloneRepositoryResponse)
13
+ def clone_repository(request: CloneRepositoryRequest):
14
+
15
+ metadata = controller.clone(str(request.url))
16
+
17
+ return CloneRepositoryResponse(
18
+ metadata=metadata,
19
+ )
app/services/context_formatter.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.models.graph_context import GraphContext
2
+
3
+
4
+ class ContextFormatter:
5
+
6
+ def format(
7
+ self,
8
+ context: GraphContext,
9
+ ) -> str:
10
+
11
+ seed_names = {
12
+ node.qualified_name
13
+ for node in context.nodes
14
+ if node.properties.get("source_code")
15
+ }
16
+
17
+ lines = []
18
+
19
+ lines.append("# Relevant Code")
20
+ lines.append("")
21
+
22
+ for node in context.nodes:
23
+
24
+ source = node.properties.get(
25
+ "source_code"
26
+ )
27
+
28
+ if not source:
29
+ continue
30
+
31
+ lines.append(
32
+ f"## {node.qualified_name}"
33
+ )
34
+
35
+ lines.append("")
36
+
37
+ lines.append("```python")
38
+ lines.append(source)
39
+ lines.append("```")
40
+ lines.append("")
41
+
42
+ lines.append("# Related Graph Nodes")
43
+ lines.append("")
44
+
45
+ for node in context.nodes:
46
+
47
+ if node.qualified_name in seed_names:
48
+ continue
49
+
50
+ props = []
51
+
52
+ if node.properties.get("name"):
53
+
54
+ props.append(
55
+ f"name={node.properties['name']}"
56
+ )
57
+
58
+ if node.properties.get("module"):
59
+
60
+ props.append(
61
+ f"module={node.properties['module']}"
62
+ )
63
+
64
+ label = ",".join(node.labels)
65
+
66
+ lines.append(
67
+ f"- {node.qualified_name} [{label}] ({', '.join(props)})"
68
+ )
69
+
70
+ lines.append("")
71
+ lines.append("# Relationships")
72
+ lines.append("")
73
+
74
+ for edge in context.edges:
75
+
76
+ lines.append(
77
+ f"- {edge.source} --{edge.type}--> {edge.target}"
78
+ )
79
+
80
+ return "\n".join(lines)
app/services/embedding_model_service.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import SentenceTransformer
2
+
3
+
4
+ class EmbeddingModelService:
5
+
6
+ def __init__(self):
7
+
8
+ self.model = SentenceTransformer(
9
+ "BAAI/bge-small-en-v1.5"
10
+ )
11
+
12
+ def encode(
13
+ self,
14
+ text: str,
15
+ ) -> list[float]:
16
+
17
+ return self.model.encode(
18
+ text,
19
+ normalize_embeddings=True,
20
+ ).tolist()
app/services/embedding_service.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.repositories.neo4j_repository import (
2
+ Neo4jRepository,
3
+ )
4
+
5
+ from app.services.embedding_model_service import (
6
+ EmbeddingModelService,
7
+ )
8
+
9
+
10
+ class EmbeddingService:
11
+
12
+ def __init__(self):
13
+
14
+ self.embedding_model = (
15
+ EmbeddingModelService()
16
+ )
17
+
18
+ self.neo = Neo4jRepository()
19
+
20
+ def build(
21
+ self,
22
+ repository_id: str,
23
+ resolved,
24
+ ):
25
+
26
+ for file in resolved:
27
+
28
+ self._embed_functions(
29
+ repository_id,
30
+ file["functions"],
31
+ )
32
+
33
+ self._embed_classes(
34
+ repository_id,
35
+ file["classes"],
36
+ )
37
+
38
+ def _embed_functions(
39
+ self,
40
+ repository_id: str,
41
+ functions,
42
+ ):
43
+
44
+ for function in functions:
45
+
46
+ source = function.get(
47
+ "source_code"
48
+ )
49
+
50
+ if not source:
51
+ continue
52
+
53
+ embedding = (
54
+ self.embedding_model.encode(
55
+ source
56
+ )
57
+ )
58
+
59
+ self.neo.update_embedding(
60
+ repository_id=repository_id,
61
+ qualified_name=function[
62
+ "qualified_name"
63
+ ],
64
+ embedding=embedding,
65
+ )
66
+
67
+ def _embed_classes(
68
+ self,
69
+ repository_id: str,
70
+ classes,
71
+ ):
72
+
73
+ for cls in classes:
74
+
75
+ source = cls.get(
76
+ "source_code"
77
+ )
78
+
79
+ if source:
80
+
81
+ embedding = (
82
+ self.embedding_model.encode(
83
+ source
84
+ )
85
+ )
86
+
87
+ self.neo.update_embedding(
88
+ repository_id=repository_id,
89
+ qualified_name=cls[
90
+ "qualified_name"
91
+ ],
92
+ embedding=embedding,
93
+ )
94
+
95
+ self._embed_functions(
96
+ repository_id,
97
+ cls["methods"],
98
+ )
app/services/github_service.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from uuid import uuid4
3
+
4
+ from git import Repo
5
+
6
+
7
+ class GithubService:
8
+
9
+ def clone(
10
+ self,
11
+ url: str,
12
+ destination_root: Path,
13
+ ):
14
+
15
+ repo_name = url.rstrip("/").split("/")[-1]
16
+
17
+ repository_id = uuid4().hex[:8]
18
+
19
+ folder_name = f"{repo_name}_{repository_id}"
20
+
21
+ destination = destination_root / folder_name
22
+
23
+ Repo.clone_from(
24
+ url,
25
+ destination,
26
+ )
27
+
28
+ return {
29
+ "repository_id": repository_id,
30
+ "repo_path": destination,
31
+ }
32
+
33
+ def get_repo(
34
+ self,
35
+ repo_path,
36
+ ):
37
+ return Repo(repo_path)
app/services/graph_service.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.repositories.neo4j_repository import Neo4jRepository
2
+
3
+
4
+ class GraphService:
5
+
6
+ def __init__(self):
7
+
8
+ self.neo = Neo4jRepository()
9
+
10
+ def build(
11
+ self,
12
+ repository_id: str,
13
+ resolved,
14
+ ):
15
+
16
+ for file in resolved:
17
+
18
+ self.neo.merge_file(
19
+ repository_id=repository_id,
20
+ path=file["path"],
21
+ )
22
+
23
+ self._create_imports(
24
+ repository_id,
25
+ file,
26
+ )
27
+
28
+ self._create_functions(
29
+ repository_id,
30
+ file,
31
+ )
32
+
33
+ self._create_classes(
34
+ repository_id,
35
+ file,
36
+ )
37
+
38
+ self._create_declares(
39
+ repository_id,
40
+ file,
41
+ )
42
+
43
+ self._create_has_methods(
44
+ repository_id,
45
+ file,
46
+ )
47
+
48
+ self._create_edges(
49
+ repository_id,
50
+ file,
51
+ )
52
+
53
+ self.neo.create_vector_indexes()
54
+
55
+ def _create_functions(
56
+ self,
57
+ repository_id: str,
58
+ file,
59
+ ):
60
+
61
+ for function in file["functions"]:
62
+
63
+ self.neo.merge_function(
64
+ repository_id=repository_id,
65
+ qualified_name=function["qualified_name"],
66
+ name=function["name"],
67
+ source_code=function["source_code"],
68
+ )
69
+
70
+ def _create_classes(
71
+ self,
72
+ repository_id: str,
73
+ file,
74
+ ):
75
+
76
+ for cls in file["classes"]:
77
+
78
+ self.neo.merge_class(
79
+ repository_id=repository_id,
80
+ qualified_name=cls["qualified_name"],
81
+ name=cls["name"],
82
+ source_code=cls["source_code"],
83
+ )
84
+
85
+ for method in cls["methods"]:
86
+
87
+ self.neo.merge_function(
88
+ repository_id=repository_id,
89
+ qualified_name=method["qualified_name"],
90
+ name=method["name"],
91
+ source_code=method["source_code"],
92
+ )
93
+
94
+ def _create_declares(
95
+ self,
96
+ repository_id: str,
97
+ file,
98
+ ):
99
+
100
+ for function in file["functions"]:
101
+
102
+ self.neo.create_declares(
103
+ repository_id=repository_id,
104
+ file_path=file["path"],
105
+ qualified_name=function["qualified_name"],
106
+ )
107
+
108
+ for cls in file["classes"]:
109
+
110
+ self.neo.create_declares(
111
+ repository_id=repository_id,
112
+ file_path=file["path"],
113
+ qualified_name=cls["qualified_name"],
114
+ )
115
+
116
+ def _create_has_methods(
117
+ self,
118
+ repository_id: str,
119
+ file,
120
+ ):
121
+
122
+ for cls in file["classes"]:
123
+
124
+ for method in cls["methods"]:
125
+
126
+ self.neo.create_has_method(
127
+ repository_id=repository_id,
128
+ class_name=cls["qualified_name"],
129
+ method_name=method["qualified_name"],
130
+ )
131
+
132
+ def _create_imports(
133
+ self,
134
+ repository_id: str,
135
+ file,
136
+ ):
137
+
138
+ for imp in file["imports"]:
139
+
140
+ module = imp["module"]
141
+
142
+ if not module:
143
+ continue
144
+
145
+ self.neo.merge_module(
146
+ repository_id=repository_id,
147
+ module=module,
148
+ )
149
+
150
+ self.neo.create_import(
151
+ repository_id=repository_id,
152
+ file_path=file["path"],
153
+ module=module,
154
+ symbol=imp["name"],
155
+ alias=imp["alias"],
156
+ )
157
+
158
+ def _create_edges(
159
+ self,
160
+ repository_id: str,
161
+ file,
162
+ ):
163
+
164
+ self._create_function_edges(
165
+ repository_id,
166
+ file["functions"],
167
+ )
168
+
169
+ for cls in file["classes"]:
170
+
171
+ self._create_function_edges(
172
+ repository_id,
173
+ cls["methods"],
174
+ )
175
+
176
+ def _create_function_edges(
177
+ self,
178
+ repository_id: str,
179
+ functions,
180
+ ):
181
+
182
+ for function in functions:
183
+
184
+ source = function["qualified_name"]
185
+
186
+ for call in function["calls"]:
187
+
188
+ target = call["qualified_name"]
189
+
190
+ if target is None:
191
+ continue
192
+
193
+ if target.startswith("app."):
194
+
195
+ self.neo.merge_call(
196
+ repository_id=repository_id,
197
+ source=source,
198
+ target=target,
199
+ )
200
+
201
+ else:
202
+
203
+ self.neo.merge_external_function(
204
+ repository_id=repository_id,
205
+ qualified_name=target,
206
+ )
207
+
208
+ self.neo.create_belongs_to(
209
+ repository_id=repository_id,
210
+ qualified_name=target,
211
+ )
212
+
213
+ self.neo.merge_call(
214
+ repository_id=repository_id,
215
+ source=source,
216
+ target=target,
217
+ )
app/services/index_service.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import json
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class IndexService:
7
+
8
+ INDEX_FOLDER = "index"
9
+
10
+ def _get_index_path(self, repo_path: Path) -> Path:
11
+
12
+ index_path = repo_path / self.INDEX_FOLDER
13
+ index_path.mkdir(exist_ok=True)
14
+
15
+ return index_path
16
+
17
+ def save(self, repo_path: Path, filename: str, data):
18
+
19
+ index_path = self._get_index_path(repo_path)
20
+
21
+ output = index_path / filename
22
+
23
+ if isinstance(data, BaseModel):
24
+ payload = data.model_dump()
25
+
26
+ elif isinstance(data, list):
27
+ payload = [
28
+ item.model_dump() if isinstance(item, BaseModel) else item
29
+ for item in data
30
+ ]
31
+
32
+ else:
33
+ payload = data
34
+
35
+ with open(output, "w", encoding="utf-8") as f:
36
+ json.dump(
37
+ payload,
38
+ f,
39
+ indent=4,
40
+ ensure_ascii=False,
41
+ )
42
+
43
+ def load(self, repo_path: Path, filename: str):
44
+
45
+ index_path = self._get_index_path(repo_path)
46
+
47
+ with open(index_path / filename, encoding="utf-8") as f:
48
+ return json.load(f)
app/services/llm_service.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+
3
+ from app.core.settings import settings
4
+
5
+
6
+ class LLMService:
7
+
8
+ def __init__(self):
9
+
10
+ self.client = OpenAI(
11
+ api_key=settings.openrouter_api_key,
12
+ base_url="https://openrouter.ai/api/v1",
13
+ )
14
+
15
+ def chat(
16
+ self,
17
+ question: str,
18
+ context: str,
19
+ ) -> str:
20
+
21
+ prompt = self._build_prompt(
22
+ question,
23
+ context,
24
+ )
25
+
26
+ response = self.client.chat.completions.create(
27
+ model=settings.llm_model,
28
+ temperature=0.2,
29
+ messages=[
30
+ {
31
+ "role": "system",
32
+ "content": (
33
+ "You are an expert software engineer. "
34
+ "Answer only using the provided repository context."
35
+ ),
36
+ },
37
+ {
38
+ "role": "user",
39
+ "content": prompt,
40
+ },
41
+ ],
42
+ )
43
+
44
+ return response.choices[0].message.content
45
+
46
+ def _build_prompt(
47
+ self,
48
+ question: str,
49
+ context: str,
50
+ ) -> str:
51
+
52
+ return f"""
53
+ Repository Context
54
+ ==================
55
+
56
+ {context}
57
+
58
+ ==================
59
+
60
+ Question:
61
+ {question}
62
+
63
+ Instructions:
64
+
65
+ - Answer only using the repository context.
66
+ - If the answer cannot be inferred from the context, say so.
67
+ - Be concise.
68
+ - Mention function and class names using their qualified names.
69
+ """
app/services/metadata_service.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from git import Repo
4
+
5
+ from app.models.metadata import (
6
+ RepositoryMetadata,
7
+ RepositoryInfo,
8
+ StructureInfo,
9
+ ConfigurationInfo,
10
+ )
11
+
12
+
13
+ class MetadataService:
14
+
15
+ def extract(self, repo_path: Path):
16
+
17
+ repo = Repo(repo_path)
18
+
19
+ branch = repo.active_branch.name
20
+
21
+ latest_commit = repo.head.commit.hexsha
22
+
23
+ total_files = 0
24
+ python_files = 0
25
+ directories = 0
26
+
27
+ size = 0
28
+
29
+ has_readme = False
30
+ has_license = False
31
+ has_dockerfile = False
32
+ has_docker_compose = False
33
+
34
+ requirements = []
35
+
36
+ for path in repo_path.rglob("*"):
37
+
38
+ if path.is_dir():
39
+ directories += 1
40
+ continue
41
+
42
+ total_files += 1
43
+
44
+ size += path.stat().st_size
45
+
46
+ if path.suffix == ".py":
47
+ python_files += 1
48
+
49
+ filename = path.name.lower()
50
+
51
+ if filename.startswith("readme"):
52
+ has_readme = True
53
+
54
+ elif filename.startswith("license"):
55
+ has_license = True
56
+
57
+ elif filename == "dockerfile":
58
+ has_dockerfile = True
59
+
60
+ elif filename == "docker-compose.yml":
61
+ has_docker_compose = True
62
+
63
+ elif filename == "requirements.txt":
64
+
65
+ with open(path) as f:
66
+
67
+ for line in f:
68
+
69
+ line = line.strip()
70
+
71
+ if line and not line.startswith("#"):
72
+
73
+ requirements.append(line)
74
+
75
+ return RepositoryMetadata(
76
+
77
+ repository=RepositoryInfo(
78
+
79
+ name=repo_path.name,
80
+
81
+ path=str(repo_path),
82
+
83
+ branch=branch,
84
+
85
+ latest_commit=latest_commit,
86
+
87
+ size_mb=round(size / 1024 / 1024, 2),
88
+
89
+ ),
90
+
91
+ structure=StructureInfo(
92
+
93
+ total_files=total_files,
94
+
95
+ python_files=python_files,
96
+
97
+ directories=directories,
98
+
99
+ ),
100
+
101
+ configuration=ConfigurationInfo(
102
+
103
+ has_readme=has_readme,
104
+
105
+ has_license=has_license,
106
+
107
+ has_dockerfile=has_dockerfile,
108
+
109
+ has_docker_compose=has_docker_compose,
110
+
111
+ requirements=requirements,
112
+
113
+ ),
114
+ )
app/services/parser_service.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ from pathlib import Path
3
+
4
+ from app.models.code import (
5
+ ImportInfo,
6
+ FunctionInfo,
7
+ ClassInfo,
8
+ PythonFileInfo,
9
+ CallInfo,
10
+ )
11
+
12
+
13
+ class FunctionCallVisitor(ast.NodeVisitor):
14
+
15
+ def __init__(self):
16
+ self.calls = []
17
+
18
+ def visit_Call(self, node: ast.Call):
19
+
20
+ name = self._get_call_name(node.func)
21
+
22
+ if name is not None:
23
+
24
+ self.calls.append(
25
+ CallInfo(
26
+ name=name,
27
+ line=node.lineno,
28
+ )
29
+ )
30
+
31
+ self.generic_visit(node)
32
+
33
+ def _get_call_name(self, node):
34
+
35
+ if isinstance(node, ast.Name):
36
+ return node.id
37
+
38
+ if isinstance(node, ast.Attribute):
39
+
40
+ parts = []
41
+
42
+ while isinstance(node, ast.Attribute):
43
+ parts.append(node.attr)
44
+ node = node.value
45
+
46
+ if isinstance(node, ast.Name):
47
+ parts.append(node.id)
48
+
49
+ return ".".join(reversed(parts))
50
+
51
+ return None
52
+
53
+
54
+ class ParserService:
55
+ def _module_name(self, file_path: Path):
56
+
57
+ parts = list(file_path.parts)
58
+
59
+ if "app" in parts:
60
+
61
+ index = parts.index("app")
62
+
63
+ parts = parts[index:]
64
+
65
+ parts[-1] = parts[-1].replace(".py", "")
66
+
67
+ return ".".join(parts)
68
+
69
+ def parse_file(self, file_path: Path) -> PythonFileInfo:
70
+
71
+ module_name = self._module_name(file_path)
72
+
73
+ source = file_path.read_text(
74
+ encoding="utf-8",
75
+ errors="ignore",
76
+ )
77
+
78
+ lines = source.splitlines()
79
+
80
+ tree = ast.parse(source)
81
+
82
+ imports = []
83
+ classes = []
84
+ functions = []
85
+
86
+ for node in tree.body:
87
+
88
+ if isinstance(node, ast.Import):
89
+
90
+ for alias in node.names:
91
+
92
+ imports.append(
93
+ ImportInfo(
94
+ module=alias.name,
95
+ name=alias.name,
96
+ alias=alias.asname,
97
+ )
98
+ )
99
+
100
+ elif isinstance(node, ast.ImportFrom):
101
+
102
+ for alias in node.names:
103
+
104
+ imports.append(
105
+ ImportInfo(
106
+ module=node.module,
107
+ name=alias.name,
108
+ alias=alias.asname,
109
+ )
110
+ )
111
+
112
+ elif isinstance(node, ast.FunctionDef):
113
+
114
+ functions.append(
115
+ self._parse_function(
116
+ node,
117
+ module_name,
118
+ lines=lines,
119
+ )
120
+ )
121
+
122
+ elif isinstance(node, ast.ClassDef):
123
+
124
+ methods = []
125
+
126
+ for child in node.body:
127
+
128
+ if isinstance(child, ast.FunctionDef):
129
+
130
+ methods.append(
131
+ self._parse_function(
132
+ node=child,
133
+ module_name=module_name,
134
+ lines=lines,
135
+ class_name=node.name,
136
+ )
137
+ )
138
+ class_source = self._get_source_code(
139
+ node,
140
+ lines,
141
+ )
142
+
143
+ classes.append(
144
+
145
+ ClassInfo(
146
+
147
+ name=node.name,
148
+
149
+ qualified_name=f"{module_name}.{node.name}",
150
+
151
+ line=node.lineno,
152
+
153
+ end_line=node.end_lineno,
154
+
155
+ source_code=class_source,
156
+
157
+ methods=methods,
158
+ )
159
+ )
160
+
161
+ return PythonFileInfo(
162
+ path=str(file_path),
163
+ imports=imports,
164
+ classes=classes,
165
+ functions=functions,
166
+ )
167
+ def _get_source_code(
168
+ self,
169
+ node,
170
+ lines,
171
+ ) -> str:
172
+
173
+ return "\n".join(
174
+ lines[node.lineno - 1 : node.end_lineno]
175
+ )
176
+
177
+ def _parse_function(
178
+ self,
179
+ node,
180
+ module_name,
181
+ lines,
182
+ class_name=None,
183
+ ):
184
+
185
+ visitor = FunctionCallVisitor()
186
+
187
+ visitor.visit(node)
188
+
189
+ if class_name:
190
+
191
+ qualified = f"{module_name}.{class_name}.{node.name}"
192
+
193
+ else:
194
+
195
+ qualified = f"{module_name}.{node.name}"
196
+
197
+ print("module_name:", repr(module_name))
198
+ print("class_name:", repr(class_name))
199
+ print("node.name:", repr(node.name))
200
+ print("qualified:", repr(qualified))
201
+ source_code = self._get_source_code(
202
+ node,
203
+ lines,
204
+ )
205
+
206
+ return FunctionInfo(
207
+
208
+ name=node.name,
209
+
210
+ qualified_name=qualified,
211
+
212
+ line=node.lineno,
213
+
214
+ end_line=node.end_lineno,
215
+
216
+ docstring=ast.get_docstring(node),
217
+
218
+ source_code=source_code,
219
+
220
+ calls=visitor.calls,
221
+ )
app/services/resolver_service.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+
3
+ # TODO:
4
+ # If multiple symbols have the same name,
5
+ # keep all of them instead of overwriting.
6
+ class ResolverService:
7
+
8
+ def resolve(self, code_index: list[dict]) -> list[dict]:
9
+
10
+ resolved = deepcopy(code_index)
11
+
12
+ symbols = self._build_symbol_table(resolved)
13
+
14
+ for file in resolved:
15
+
16
+ import_table = self._build_import_table(file)
17
+
18
+ self._resolve_functions(
19
+ file["functions"],
20
+ symbols,
21
+ import_table,
22
+ )
23
+
24
+ for cls in file["classes"]:
25
+
26
+ self._resolve_functions(
27
+ cls["methods"],
28
+ symbols,
29
+ import_table,
30
+ )
31
+
32
+ return resolved
33
+
34
+ def _build_symbol_table(self, code_index):
35
+
36
+ table = {}
37
+
38
+ for file in code_index:
39
+
40
+ for function in file["functions"]:
41
+
42
+ table[
43
+ function["qualified_name"]
44
+ .split(".")[-1]
45
+ ] = function["qualified_name"]
46
+
47
+ for cls in file["classes"]:
48
+
49
+ table[
50
+ cls["qualified_name"]
51
+ .split(".")[-1]
52
+ ] = cls["qualified_name"]
53
+
54
+ for method in cls["methods"]:
55
+
56
+ table[
57
+ method["qualified_name"]
58
+ .split(".")[-1]
59
+ ] = method["qualified_name"]
60
+
61
+ return table
62
+
63
+ def _resolve_functions(
64
+ self,
65
+ functions,
66
+ symbols,
67
+ imports,
68
+ ):
69
+
70
+ for function in functions:
71
+
72
+ for call in function["calls"]:
73
+
74
+ raw = call["name"]
75
+
76
+ parts = raw.split(".")
77
+
78
+ first = parts[0]
79
+
80
+ # اول importهای فایل
81
+ if first in imports:
82
+
83
+ resolved = imports[first]
84
+
85
+ if len(parts) > 1:
86
+ resolved += "." + ".".join(parts[1:])
87
+
88
+ call["qualified_name"] = resolved
89
+
90
+ # بعد symbolهای پروژه
91
+ elif first in symbols:
92
+
93
+ resolved = symbols[first]
94
+
95
+ if len(parts) > 1:
96
+ resolved += "." + ".".join(parts[1:])
97
+
98
+ call["qualified_name"] = resolved
99
+
100
+ def _build_import_table(self, file):
101
+
102
+ table = {}
103
+
104
+ for imp in file["imports"]:
105
+
106
+ key = imp["alias"] or imp["name"]
107
+
108
+ if imp["module"]:
109
+
110
+ if imp["module"] == imp["name"]:
111
+ table[key] = imp["module"]
112
+ else:
113
+ table[key] = f'{imp["module"]}.{imp["name"]}'
114
+
115
+ return table
app/services/retrieval_service.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.repositories.neo4j_repository import (
2
+ Neo4jRepository,
3
+ )
4
+
5
+ from app.services.embedding_model_service import (
6
+ EmbeddingModelService,
7
+ )
8
+
9
+
10
+ class RetrievalService:
11
+
12
+ def __init__(self):
13
+
14
+ self.embedding = EmbeddingModelService()
15
+
16
+ self.neo = Neo4jRepository()
17
+
18
+ def search(
19
+ self,
20
+ repository_id: str,
21
+ query: str,
22
+ limit: int = 5,
23
+ hops: int = 1,
24
+ ):
25
+
26
+ vector = self.embedding.encode(
27
+ query
28
+ )
29
+
30
+ return self.neo.retrieve_context(
31
+ repository_id=repository_id,
32
+ embedding=vector,
33
+ limit=limit,
34
+ hops=hops,
35
+ )
app/tests/retrieval.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.services.retrieval_service import (
2
+ RetrievalService,
3
+ )
4
+
5
+ service = RetrievalService()
6
+
7
+ context = service.search(
8
+ "graph creation",
9
+ )
10
+
11
+ print()
12
+
13
+ print("========== NODES ==========")
14
+
15
+ for node in context.nodes:
16
+
17
+ print(node.labels)
18
+
19
+ print(node.qualified_name)
20
+
21
+ print(node.properties)
22
+
23
+ print("-" * 50)
24
+
25
+ print()
26
+
27
+ print("========== EDGES ==========")
28
+
29
+ for edge in context.edges:
30
+
31
+ print(
32
+ edge.source,
33
+ "--",
34
+ edge.type,
35
+ "-->",
36
+ edge.target,
37
+ )
38
+ print(len(context.nodes))
39
+ print(len(context.edges))
app/utils/file_utils.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+
4
+ def get_python_files(root: Path):
5
+
6
+ return sorted(root.rglob("*.py"))