graph-dependency-analyzer 0.2.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.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- src/presentation/api/rest/routers/relationships.py +1089 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qdrant Vector Repository Implementation (Task 5.2)
|
|
3
|
+
|
|
4
|
+
Implements persistence layer for code chunk embeddings in Qdrant.
|
|
5
|
+
Supports batch upsert with 1000 points per batch for optimal performance.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
from typing import Any
|
|
10
|
+
import structlog
|
|
11
|
+
|
|
12
|
+
from qdrant_client import QdrantClient
|
|
13
|
+
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue, MatchAny
|
|
14
|
+
from qdrant_client.http.exceptions import UnexpectedResponse
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class VectorStorageResult:
|
|
21
|
+
"""Result of storing embeddings in Qdrant"""
|
|
22
|
+
points_upserted: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QdrantVectorRepository:
|
|
26
|
+
"""
|
|
27
|
+
Qdrant-based repository for storing code chunk embeddings.
|
|
28
|
+
|
|
29
|
+
Implements:
|
|
30
|
+
- Collection management (create if not exists)
|
|
31
|
+
- Batch upsert with 1000 points per batch
|
|
32
|
+
- 1536-dimensional embeddings (text-embedding-3-small)
|
|
33
|
+
- Complete metadata payload
|
|
34
|
+
- Unique repository listing
|
|
35
|
+
|
|
36
|
+
Requirements: 9.1-9.6
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
client: QdrantClient,
|
|
42
|
+
collection_name: str = "code_chunks",
|
|
43
|
+
batch_size: int = 1000,
|
|
44
|
+
vector_size: int = 1536,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize repository with Qdrant client.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
client: QdrantClient instance
|
|
51
|
+
collection_name: Name of Qdrant collection
|
|
52
|
+
batch_size: Batch size for upsert operations (default 1000)
|
|
53
|
+
vector_size: Embedding dimension — must match the provider model
|
|
54
|
+
(1536 for OpenAI/Flow, 768 for nomic-embed-text, etc.)
|
|
55
|
+
"""
|
|
56
|
+
self.client = client
|
|
57
|
+
self.collection_name = collection_name
|
|
58
|
+
self.batch_size = batch_size
|
|
59
|
+
self.vector_size = vector_size
|
|
60
|
+
|
|
61
|
+
async def upsert_batch(
|
|
62
|
+
self,
|
|
63
|
+
chunks: list,
|
|
64
|
+
embeddings: list[list[float]],
|
|
65
|
+
batch_size: int | None = None
|
|
66
|
+
) -> VectorStorageResult:
|
|
67
|
+
"""
|
|
68
|
+
Upsert code chunks with embeddings to Qdrant.
|
|
69
|
+
|
|
70
|
+
Uses batch processing for performance. Each point contains:
|
|
71
|
+
- UUID v4 id (from chunk.id)
|
|
72
|
+
- 1536-dimensional embedding vector
|
|
73
|
+
- Complete metadata payload
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
chunks: List of CodeChunk entities
|
|
77
|
+
embeddings: List of embedding vectors (1536-dim each)
|
|
78
|
+
batch_size: Override default batch size (optional)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
VectorStorageResult with count of points upserted
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If validation fails (dimensions, length mismatch)
|
|
85
|
+
"""
|
|
86
|
+
# Validation
|
|
87
|
+
self._validate_inputs(chunks, embeddings)
|
|
88
|
+
|
|
89
|
+
# Use provided batch_size or default
|
|
90
|
+
effective_batch_size = batch_size or self.batch_size
|
|
91
|
+
|
|
92
|
+
# Ensure collection exists
|
|
93
|
+
await self.ensure_collection_exists()
|
|
94
|
+
|
|
95
|
+
# Process in batches
|
|
96
|
+
total_upserted = 0
|
|
97
|
+
for i in range(0, len(chunks), effective_batch_size):
|
|
98
|
+
batch_chunks = chunks[i:i + effective_batch_size]
|
|
99
|
+
batch_embeddings = embeddings[i:i + effective_batch_size]
|
|
100
|
+
|
|
101
|
+
# Create points for this batch
|
|
102
|
+
points = self._create_points(batch_chunks, batch_embeddings)
|
|
103
|
+
|
|
104
|
+
# Upsert to Qdrant
|
|
105
|
+
self.client.upsert(
|
|
106
|
+
collection_name=self.collection_name,
|
|
107
|
+
points=points
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
total_upserted += len(points)
|
|
111
|
+
|
|
112
|
+
logger.info(
|
|
113
|
+
"embeddings_upserted",
|
|
114
|
+
collection=self.collection_name,
|
|
115
|
+
points_upserted=total_upserted,
|
|
116
|
+
batch_count=(len(chunks) + effective_batch_size - 1) // effective_batch_size
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return VectorStorageResult(points_upserted=total_upserted)
|
|
120
|
+
|
|
121
|
+
def _validate_inputs(
|
|
122
|
+
self,
|
|
123
|
+
chunks: list,
|
|
124
|
+
embeddings: list[list[float]]
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Validate chunks and embeddings inputs.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
chunks: List of CodeChunk
|
|
131
|
+
embeddings: List of embedding vectors
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If validation fails
|
|
135
|
+
"""
|
|
136
|
+
# Validate lengths match
|
|
137
|
+
if len(chunks) != len(embeddings):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Chunks and embeddings must have same length. "
|
|
140
|
+
f"Got {len(chunks)} chunks and {len(embeddings)} embeddings."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Validate embedding dimensions — uses self.vector_size set at init
|
|
144
|
+
for i, embedding in enumerate(embeddings):
|
|
145
|
+
if len(embedding) != self.vector_size:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Embedding at index {i} must have {self.vector_size} dimensions, "
|
|
148
|
+
f"got {len(embedding)}. "
|
|
149
|
+
f"Check QDRANT_VECTOR_SIZE in .env matches your embedding model."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _create_points(
|
|
153
|
+
self,
|
|
154
|
+
chunks: list,
|
|
155
|
+
embeddings: list[list[float]]
|
|
156
|
+
) -> list[PointStruct]:
|
|
157
|
+
"""
|
|
158
|
+
Create Qdrant PointStruct from chunks and embeddings.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
chunks: List of CodeChunk
|
|
162
|
+
embeddings: List of embedding vectors
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of PointStruct ready for upsert
|
|
166
|
+
"""
|
|
167
|
+
points = []
|
|
168
|
+
|
|
169
|
+
for chunk, embedding in zip(chunks, embeddings):
|
|
170
|
+
# Build payload from chunk metadata
|
|
171
|
+
payload = {
|
|
172
|
+
"repository": chunk.metadata.get("repository", ""),
|
|
173
|
+
"file_path": chunk.metadata.get("file_path", ""),
|
|
174
|
+
"language": chunk.metadata.get("language", "java"),
|
|
175
|
+
"type": chunk.chunk_type,
|
|
176
|
+
"name": chunk.metadata.get("name", ""),
|
|
177
|
+
"qualified_name": chunk.metadata.get("qualified_name", ""),
|
|
178
|
+
"content": chunk.content,
|
|
179
|
+
"line_start": chunk.metadata.get("line_start", 0),
|
|
180
|
+
"line_end": chunk.metadata.get("line_end", 0),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Add optional metadata fields
|
|
184
|
+
if "parameters" in chunk.metadata:
|
|
185
|
+
payload["parameters"] = chunk.metadata["parameters"]
|
|
186
|
+
if "return_type" in chunk.metadata:
|
|
187
|
+
payload["return_type"] = chunk.metadata["return_type"]
|
|
188
|
+
if "parent_class" in chunk.metadata:
|
|
189
|
+
payload["parent_class"] = chunk.metadata["parent_class"]
|
|
190
|
+
|
|
191
|
+
# Create point with UUID from chunk
|
|
192
|
+
point = PointStruct(
|
|
193
|
+
id=str(chunk.id),
|
|
194
|
+
vector=embedding,
|
|
195
|
+
payload=payload
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
points.append(point)
|
|
199
|
+
|
|
200
|
+
return points
|
|
201
|
+
|
|
202
|
+
async def search_similar(
|
|
203
|
+
self,
|
|
204
|
+
query_vector: list[float],
|
|
205
|
+
limit: int = 10,
|
|
206
|
+
repositories: list[str] | None = None,
|
|
207
|
+
score_threshold: float = 0.35,
|
|
208
|
+
) -> list[dict]:
|
|
209
|
+
"""
|
|
210
|
+
Search for code chunks semantically similar to the query vector.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
query_vector: 1536-dimensional embedding of the search query
|
|
214
|
+
limit: Maximum number of results to return
|
|
215
|
+
repositories: Optional list of repo names to filter by
|
|
216
|
+
score_threshold: Minimum cosine similarity score (0-1)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of dicts with keys: score, repository, file_path,
|
|
220
|
+
qualified_name, name, type, content, line_start, line_end
|
|
221
|
+
"""
|
|
222
|
+
# Build optional filter for repository
|
|
223
|
+
query_filter = None
|
|
224
|
+
if repositories:
|
|
225
|
+
if len(repositories) == 1:
|
|
226
|
+
query_filter = Filter(
|
|
227
|
+
must=[FieldCondition(
|
|
228
|
+
key="repository",
|
|
229
|
+
match=MatchValue(value=repositories[0])
|
|
230
|
+
)]
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
# MatchAny matches any of the given values — equivalent to OR
|
|
234
|
+
query_filter = Filter(
|
|
235
|
+
must=[FieldCondition(
|
|
236
|
+
key="repository",
|
|
237
|
+
match=MatchAny(any=repositories)
|
|
238
|
+
)]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# qdrant-client >= 1.7 uses query_points; older versions use search.
|
|
243
|
+
# Try query_points first, fall back to search.
|
|
244
|
+
if hasattr(self.client, "query_points"):
|
|
245
|
+
response = self.client.query_points(
|
|
246
|
+
collection_name=self.collection_name,
|
|
247
|
+
query=query_vector,
|
|
248
|
+
limit=limit,
|
|
249
|
+
query_filter=query_filter,
|
|
250
|
+
score_threshold=score_threshold,
|
|
251
|
+
with_payload=True,
|
|
252
|
+
)
|
|
253
|
+
results = response.points
|
|
254
|
+
else:
|
|
255
|
+
results = self.client.search(
|
|
256
|
+
collection_name=self.collection_name,
|
|
257
|
+
query_vector=query_vector,
|
|
258
|
+
limit=limit,
|
|
259
|
+
query_filter=query_filter,
|
|
260
|
+
score_threshold=score_threshold,
|
|
261
|
+
with_payload=True,
|
|
262
|
+
with_vectors=False,
|
|
263
|
+
)
|
|
264
|
+
except Exception:
|
|
265
|
+
return []
|
|
266
|
+
|
|
267
|
+
hits = []
|
|
268
|
+
for point in results:
|
|
269
|
+
payload = point.payload or {}
|
|
270
|
+
hits.append({
|
|
271
|
+
"score": round(point.score, 4),
|
|
272
|
+
"repository": payload.get("repository", ""),
|
|
273
|
+
"file_path": payload.get("file_path", ""),
|
|
274
|
+
"qualified_name": payload.get("qualified_name", ""),
|
|
275
|
+
"name": payload.get("name", ""),
|
|
276
|
+
"type": payload.get("type", ""),
|
|
277
|
+
"language": payload.get("language", ""),
|
|
278
|
+
"line_start": payload.get("line_start", 0),
|
|
279
|
+
"line_end": payload.get("line_end", 0),
|
|
280
|
+
"content": (payload.get("content", "")[:500]
|
|
281
|
+
if payload.get("content") else ""),
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
logger.info(
|
|
285
|
+
"semantic_search_completed",
|
|
286
|
+
results=len(hits),
|
|
287
|
+
repositories=repositories,
|
|
288
|
+
score_threshold=score_threshold,
|
|
289
|
+
)
|
|
290
|
+
return hits
|
|
291
|
+
|
|
292
|
+
async def get_unique_repositories(self) -> list[str]:
|
|
293
|
+
"""
|
|
294
|
+
Get list of unique repository names indexed in Qdrant.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of unique repository names
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
Exception: If Qdrant query fails
|
|
301
|
+
"""
|
|
302
|
+
# Scroll through all points to collect unique repositories
|
|
303
|
+
# Using scroll instead of search since we want all data
|
|
304
|
+
records, next_page = self.client.scroll(
|
|
305
|
+
collection_name=self.collection_name,
|
|
306
|
+
limit=10000, # Large limit to get all in one call
|
|
307
|
+
with_payload=["repository"],
|
|
308
|
+
with_vectors=False # Don't need vectors for this
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Extract unique repository names
|
|
312
|
+
repositories = set()
|
|
313
|
+
for record in records:
|
|
314
|
+
if "repository" in record.payload:
|
|
315
|
+
repositories.add(record.payload["repository"])
|
|
316
|
+
|
|
317
|
+
logger.debug(
|
|
318
|
+
"unique_repositories_retrieved",
|
|
319
|
+
collection=self.collection_name,
|
|
320
|
+
count=len(repositories)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return sorted(list(repositories))
|
|
324
|
+
|
|
325
|
+
async def ensure_collection_exists(self) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Ensure Qdrant collection exists, create if not.
|
|
328
|
+
|
|
329
|
+
Creates collection with:
|
|
330
|
+
- vector_size: 1536 (text-embedding-3-small)
|
|
331
|
+
- distance: Cosine
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
Exception: If collection creation fails
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
# Try to get collection info (QdrantClient is synchronous)
|
|
338
|
+
self.client.get_collection(self.collection_name)
|
|
339
|
+
logger.debug("collection_exists", collection=self.collection_name)
|
|
340
|
+
|
|
341
|
+
except UnexpectedResponse as e:
|
|
342
|
+
if e.status_code == 404:
|
|
343
|
+
# Collection doesn't exist, create it
|
|
344
|
+
logger.info("creating_collection", collection=self.collection_name)
|
|
345
|
+
|
|
346
|
+
self.client.create_collection(
|
|
347
|
+
collection_name=self.collection_name,
|
|
348
|
+
vectors_config=VectorParams(
|
|
349
|
+
size=self.vector_size,
|
|
350
|
+
distance=Distance.COSINE
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
logger.info("collection_created", collection=self.collection_name)
|
|
355
|
+
else:
|
|
356
|
+
# Other error, re-raise
|
|
357
|
+
raise
|