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.
Files changed (86) hide show
  1. graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
  2. graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
  3. graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
  4. graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
  5. graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/application/__init__.py +0 -0
  8. src/application/dtos/__init__.py +0 -0
  9. src/application/dtos/affected_component.py +59 -0
  10. src/application/dtos/batch_processing_result.py +17 -0
  11. src/application/dtos/impact_analysis_response.py +56 -0
  12. src/application/dtos/impact_query_request.py +38 -0
  13. src/application/dtos/index_batch_request.py +53 -0
  14. src/application/dtos/index_batch_response.py +72 -0
  15. src/application/dtos/index_result.py +20 -0
  16. src/application/dtos/repository_index_result.py +79 -0
  17. src/application/exceptions.py +41 -0
  18. src/application/services/__init__.py +0 -0
  19. src/application/services/file_scanner_service.py +258 -0
  20. src/application/services/progress_tracker.py +132 -0
  21. src/application/use_cases/__init__.py +0 -0
  22. src/application/use_cases/clear_all_data_use_case.py +180 -0
  23. src/application/use_cases/index_batch_use_case.py +153 -0
  24. src/application/use_cases/index_repository_use_case.py +683 -0
  25. src/application/use_cases/indexing_orchestrator.py +181 -0
  26. src/application/use_cases/list_repositories_use_case.py +84 -0
  27. src/cli.py +512 -0
  28. src/config/__init__.py +5 -0
  29. src/config/container.py +211 -0
  30. src/config/logging_config.py +123 -0
  31. src/config/settings.py +165 -0
  32. src/domain/__init__.py +0 -0
  33. src/domain/entities/__init__.py +20 -0
  34. src/domain/entities/ast_node.py +32 -0
  35. src/domain/entities/code_chunk.py +45 -0
  36. src/domain/entities/code_file.py +44 -0
  37. src/domain/entities/dependency_graph.py +110 -0
  38. src/domain/entities/repository.py +46 -0
  39. src/domain/exceptions.py +36 -0
  40. src/domain/repositories/__init__.py +22 -0
  41. src/domain/repositories/i_code_parser.py +50 -0
  42. src/domain/repositories/i_dependency_extractor.py +35 -0
  43. src/domain/repositories/i_embedding_provider.py +31 -0
  44. src/domain/repositories/i_graph_repository.py +78 -0
  45. src/domain/repositories/i_vector_repository.py +55 -0
  46. src/domain/services/__init__.py +8 -0
  47. src/domain/services/dependency_extractor.py +962 -0
  48. src/domain/services/semantic_chunk_builder.py +719 -0
  49. src/domain/value_objects/__init__.py +12 -0
  50. src/domain/value_objects/chunk_type.py +24 -0
  51. src/domain/value_objects/dependency_type.py +22 -0
  52. src/domain/value_objects/node_type.py +22 -0
  53. src/domain/value_objects/qualified_name.py +78 -0
  54. src/infrastructure/__init__.py +0 -0
  55. src/infrastructure/exceptions.py +41 -0
  56. src/infrastructure/external_apis/__init__.py +0 -0
  57. src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
  58. src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
  59. src/infrastructure/parsing/__init__.py +20 -0
  60. src/infrastructure/parsing/config_file_parser.py +347 -0
  61. src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
  62. src/infrastructure/parsing/groovy_parser.py +198 -0
  63. src/infrastructure/parsing/maven_dependency_parser.py +147 -0
  64. src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
  65. src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
  66. src/infrastructure/parsing/python_parser.py +211 -0
  67. src/infrastructure/parsing/sql_schema_parser.py +658 -0
  68. src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
  69. src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
  70. src/infrastructure/persistence/__init__.py +0 -0
  71. src/infrastructure/persistence/graph_export_repository.py +443 -0
  72. src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
  73. src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
  74. src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
  75. src/presentation/__init__.py +0 -0
  76. src/presentation/api/__init__.py +0 -0
  77. src/presentation/api/rest/__init__.py +0 -0
  78. src/presentation/api/rest/exception_handlers.py +174 -0
  79. src/presentation/api/rest/main.py +80 -0
  80. src/presentation/api/rest/routers/__init__.py +5 -0
  81. src/presentation/api/rest/routers/admin.py +103 -0
  82. src/presentation/api/rest/routers/analysis.py +980 -0
  83. src/presentation/api/rest/routers/export.py +442 -0
  84. src/presentation/api/rest/routers/health.py +117 -0
  85. src/presentation/api/rest/routers/indexing.py +148 -0
  86. src/presentation/api/rest/routers/relationships.py +1089 -0
@@ -0,0 +1,41 @@
1
+ """
2
+ Application Layer Exceptions (Task 9.3)
3
+
4
+ Exception hierarchy for the application layer.
5
+ """
6
+
7
+
8
+ class ApplicationException(Exception):
9
+ """Base exception for application layer errors."""
10
+
11
+ def __init__(self, message: str, details: dict | None = None):
12
+ """
13
+ Initialize application exception.
14
+
15
+ Args:
16
+ message: Human-readable error message
17
+ details: Optional dict with additional context
18
+ """
19
+ super().__init__(message)
20
+ self.message = message
21
+ self.details = details or {}
22
+
23
+
24
+ class IndexingError(ApplicationException):
25
+ """Error during indexing operations."""
26
+ pass
27
+
28
+
29
+ class RepositoryNotFoundError(ApplicationException):
30
+ """Repository not found in filesystem."""
31
+ pass
32
+
33
+
34
+ class NoValidRepositoriesError(ApplicationException):
35
+ """No valid repositories found in the provided list."""
36
+ pass
37
+
38
+
39
+ class EmptyRepositoryListError(ApplicationException):
40
+ """Repository list is empty."""
41
+ pass
File without changes
@@ -0,0 +1,258 @@
1
+ """
2
+ FileScannerService Implementation (Task 7.1)
3
+
4
+ Scans repository directory for Java files, filtering out build artifacts
5
+ and large/binary files.
6
+ """
7
+ from pathlib import Path
8
+ import structlog
9
+
10
+ logger = structlog.get_logger(__name__)
11
+
12
+
13
+ class FileScannerService:
14
+ """
15
+ Application service for scanning repository for valid Java and TypeScript/JavaScript files.
16
+
17
+ Filters out:
18
+ - Build directories (target/, build/, .git/, .idea/, node_modules/, dist/)
19
+ - Large files (>1MB)
20
+ - Binary files
21
+ - Non-source files
22
+
23
+ Requirements: 3.3, 3.4, 3.5, 8.1, 12.1 (Task 6.1)
24
+ """
25
+
26
+ # Directories to skip during scanning
27
+ SKIP_DIRECTORIES = {
28
+ "target", "build", ".git", ".idea", "node_modules", ".gradle", "dist",
29
+ # test / load-test dirs
30
+ "k6-tests", "__tests__", "e2e", "cypress", "playwright",
31
+ # frontend build outputs
32
+ ".next", ".nuxt", ".svelte-kit", "coverage", "storybook-static",
33
+ }
34
+
35
+ # File name suffixes to skip (test files)
36
+ SKIP_FILE_SUFFIXES = {
37
+ ".test.ts", ".test.tsx", ".test.js", ".test.jsx",
38
+ ".spec.ts", ".spec.tsx", ".spec.js", ".spec.jsx",
39
+ ".stories.ts", ".stories.tsx", ".stories.js",
40
+ }
41
+
42
+ # TypeScript/JavaScript file extensions
43
+ TYPESCRIPT_EXTENSIONS = {".ts", ".tsx"}
44
+ JAVASCRIPT_EXTENSIONS = {".js", ".jsx"}
45
+
46
+ # SQL / DB schema file extensions
47
+ SQL_EXTENSIONS = {".sql", ".SQL", ".pks", ".pkb", ".prc", ".fnc",
48
+ ".trg", ".vw", ".tbl", ".seq", ".idx", ".typ"}
49
+
50
+ # Python file extensions
51
+ PYTHON_EXTENSIONS = {".py", ".pyw"}
52
+
53
+ # Groovy / Jenkinsfile extensions
54
+ GROOVY_EXTENSIONS = {".groovy"}
55
+
56
+ # Maximum file size (1MB)
57
+ MAX_FILE_SIZE = 1024 * 1024 # 1MB in bytes
58
+
59
+ def scan(self, repository_path: Path) -> dict[str, list[Path]]:
60
+ """
61
+ Scan repository for valid Java, TypeScript, and JavaScript files.
62
+
63
+ Args:
64
+ repository_path: Absolute path to repository root
65
+
66
+ Returns:
67
+ Dict with keys "java", "typescript", "javascript" containing lists of file paths
68
+
69
+ Raises:
70
+ ValueError: If path does not exist or is not a directory
71
+ """
72
+ # Convert to Path if string
73
+ if isinstance(repository_path, str):
74
+ repository_path = Path(repository_path)
75
+
76
+ # Validate path exists
77
+ if not repository_path.exists():
78
+ raise ValueError(f"Path does not exist: {repository_path}")
79
+
80
+ # Validate path is a directory
81
+ if not repository_path.is_dir():
82
+ raise ValueError(f"Path is not a directory: {repository_path}")
83
+
84
+ # Resolve to absolute path
85
+ repository_path = repository_path.resolve()
86
+
87
+ java_files = []
88
+ typescript_files = []
89
+ javascript_files = []
90
+ sql_files = []
91
+ python_files = []
92
+ groovy_files = []
93
+ other_extensions = {}
94
+ large_files_count = 0
95
+ binary_files_count = 0
96
+
97
+ # Walk directory tree
98
+ for file_path in repository_path.rglob("*"):
99
+ # Skip if not a file
100
+ if not file_path.is_file():
101
+ continue
102
+
103
+ # Skip if in excluded directory
104
+ if self._is_in_skip_directory(file_path, repository_path):
105
+ continue
106
+
107
+ # Skip test/spec files by name suffix
108
+ if self._is_test_file(file_path):
109
+ continue
110
+
111
+ # Count all file extensions
112
+ ext = file_path.suffix
113
+ fname_lower = file_path.name.lower()
114
+ if ext:
115
+ other_extensions[ext] = other_extensions.get(ext, 0) + 1
116
+
117
+ # SQL query properties files (e.g. webServiceSql.properties)
118
+ is_sql_props = (
119
+ ext == ".properties"
120
+ and (fname_lower.endswith("sql.properties") or fname_lower.endswith("sql.props"))
121
+ )
122
+
123
+ # Skip files without supported extensions
124
+ all_supported = {".java"} | self.TYPESCRIPT_EXTENSIONS | self.JAVASCRIPT_EXTENSIONS | self.SQL_EXTENSIONS | self.PYTHON_EXTENSIONS | self.GROOVY_EXTENSIONS
125
+ if ext not in all_supported and not is_sql_props:
126
+ continue
127
+
128
+ # Check file size
129
+ try:
130
+ file_size = file_path.stat().st_size
131
+ if file_size > self.MAX_FILE_SIZE:
132
+ large_files_count += 1
133
+ logger.warning(
134
+ "file_too_large",
135
+ file_path=str(file_path),
136
+ size_mb=file_size / (1024 * 1024)
137
+ )
138
+ continue
139
+ except OSError as e:
140
+ logger.warning(
141
+ "file_stat_error",
142
+ file_path=str(file_path),
143
+ error=str(e)
144
+ )
145
+ continue
146
+
147
+ # Check if file is text (not binary)
148
+ if not self._is_text_file(file_path):
149
+ binary_files_count += 1
150
+ logger.warning(
151
+ "binary_file_skipped",
152
+ file_path=str(file_path)
153
+ )
154
+ continue
155
+
156
+ # Categorize by extension
157
+ if ext == ".java":
158
+ java_files.append(file_path)
159
+ elif ext in self.TYPESCRIPT_EXTENSIONS:
160
+ typescript_files.append(file_path)
161
+ elif ext in self.JAVASCRIPT_EXTENSIONS:
162
+ javascript_files.append(file_path)
163
+ elif ext in self.SQL_EXTENSIONS or is_sql_props:
164
+ sql_files.append(file_path)
165
+ elif ext in self.PYTHON_EXTENSIONS:
166
+ python_files.append(file_path)
167
+ elif ext in self.GROOVY_EXTENSIONS:
168
+ groovy_files.append(file_path)
169
+ elif fname_lower in ('jenkinsfile',) or fname_lower.startswith('jenkinsfile'):
170
+ groovy_files.append(file_path)
171
+
172
+ # Log summary
173
+ logger.info(
174
+ "repository_scan_complete",
175
+ repository=str(repository_path),
176
+ java_files_found=len(java_files),
177
+ typescript_files_found=len(typescript_files),
178
+ javascript_files_found=len(javascript_files),
179
+ sql_files_found=len(sql_files),
180
+ python_files_found=len(python_files),
181
+ groovy_files_found=len(groovy_files),
182
+ large_files_filtered=large_files_count,
183
+ binary_files_filtered=binary_files_count,
184
+ extensions_found=dict(other_extensions)
185
+ )
186
+
187
+ return {
188
+ "java": java_files,
189
+ "typescript": typescript_files,
190
+ "javascript": javascript_files,
191
+ "sql": sql_files,
192
+ "python": python_files,
193
+ "groovy": groovy_files,
194
+ }
195
+
196
+ def _is_in_skip_directory(self, file_path: Path, repository_root: Path) -> bool:
197
+ """
198
+ Check if file is inside a directory that should be skipped.
199
+
200
+ Args:
201
+ file_path: Absolute path to file
202
+ repository_root: Repository root path
203
+
204
+ Returns:
205
+ True if file is in a skip directory
206
+ """
207
+ # Get relative path from repository root
208
+ try:
209
+ relative_path = file_path.relative_to(repository_root)
210
+ except ValueError:
211
+ # File is not relative to repository root
212
+ return False
213
+
214
+ # Check all parts of the path
215
+ for part in relative_path.parts:
216
+ if part in self.SKIP_DIRECTORIES:
217
+ return True
218
+
219
+ return False
220
+
221
+ def _is_test_file(self, file_path: Path) -> bool:
222
+ """Check if file is a test/spec file by name pattern."""
223
+ name = file_path.name
224
+ return any(name.endswith(suffix) for suffix in self.SKIP_FILE_SUFFIXES)
225
+
226
+ def _is_text_file(self, file_path: Path) -> bool:
227
+ """
228
+ Check if file is a text file (not binary).
229
+
230
+ Args:
231
+ file_path: Path to file
232
+
233
+ Returns:
234
+ True if file appears to be text
235
+ """
236
+ try:
237
+ # Read first 8192 bytes
238
+ with open(file_path, 'rb') as f:
239
+ chunk = f.read(8192)
240
+
241
+ # Check for null bytes (indicator of binary file)
242
+ if b'\x00' in chunk:
243
+ return False
244
+
245
+ # Try to decode as UTF-8
246
+ try:
247
+ chunk.decode('utf-8')
248
+ return True
249
+ except UnicodeDecodeError:
250
+ # Try other common encodings
251
+ try:
252
+ chunk.decode('latin-1')
253
+ return True
254
+ except UnicodeDecodeError:
255
+ return False
256
+
257
+ except Exception:
258
+ return False
@@ -0,0 +1,132 @@
1
+ """
2
+ ProgressTracker Implementation (Task 7.2)
3
+
4
+ Tracks progress of repository indexing through 8 stages with percentage mapping.
5
+ """
6
+ from typing import Callable, TypedDict
7
+
8
+
9
+ class ProgressUpdate(TypedDict):
10
+ """Progress update data structure"""
11
+ repository_name: str
12
+ stage: str
13
+ message: str
14
+ percentage: int
15
+ status: str
16
+
17
+
18
+ class ProgressTracker:
19
+ """
20
+ Application service for tracking indexing progress.
21
+
22
+ Maps 8 stages to percentage ranges:
23
+ - validation: 0%
24
+ - scan: 0-10% (midpoint: 5%)
25
+ - ast: 10-40% (midpoint: 25%)
26
+ - deps: 40-60% (midpoint: 50%)
27
+ - chunking: 60-70% (midpoint: 65%)
28
+ - embeddings: 70-85% (midpoint: 77%)
29
+ - qdrant: 85-95% (midpoint: 90%)
30
+ - neo4j: 95-100% (midpoint: 97%)
31
+
32
+ Requirements: 2.7, 3.15
33
+ """
34
+
35
+ # Stage to percentage mapping (midpoint of ranges)
36
+ STAGE_PERCENTAGES = {
37
+ "validation": 0,
38
+ "scan": 5,
39
+ "ast": 25,
40
+ "deps": 50,
41
+ "chunking": 65,
42
+ "embeddings": 77,
43
+ "qdrant": 90,
44
+ "neo4j": 97,
45
+ }
46
+
47
+ def __init__(
48
+ self,
49
+ repository_name: str,
50
+ callback: Callable[[ProgressUpdate], None] | None = None
51
+ ):
52
+ """
53
+ Initialize progress tracker.
54
+
55
+ Args:
56
+ repository_name: Name of repository being indexed
57
+ callback: Optional callback to invoke on progress updates
58
+ """
59
+ self.repository_name = repository_name
60
+ self.callback = callback
61
+ self.current_percentage = 0
62
+ self.current_stage = ""
63
+ self.current_status = "in_progress"
64
+
65
+ def update(self, stage: str, message: str) -> None:
66
+ """
67
+ Update progress to a specific stage.
68
+
69
+ Args:
70
+ stage: Stage name (validation, scan, ast, deps, chunking, embeddings, qdrant, neo4j)
71
+ message: Progress message
72
+
73
+ Raises:
74
+ ValueError: If stage is not recognized
75
+ """
76
+ if stage not in self.STAGE_PERCENTAGES:
77
+ raise ValueError(f"Unknown stage: {stage}")
78
+
79
+ self.current_stage = stage
80
+ self.current_percentage = self.STAGE_PERCENTAGES[stage]
81
+ self.current_status = "in_progress"
82
+
83
+ if self.callback:
84
+ progress_data: ProgressUpdate = {
85
+ "repository_name": self.repository_name,
86
+ "stage": stage,
87
+ "message": message,
88
+ "percentage": self.current_percentage,
89
+ "status": self.current_status,
90
+ }
91
+ self.callback(progress_data)
92
+
93
+ def complete(self, message: str = "Completed") -> None:
94
+ """
95
+ Mark indexing as complete (100%).
96
+
97
+ Args:
98
+ message: Completion message
99
+ """
100
+ self.current_percentage = 100
101
+ self.current_status = "completed"
102
+
103
+ if self.callback:
104
+ progress_data: ProgressUpdate = {
105
+ "repository_name": self.repository_name,
106
+ "stage": "complete",
107
+ "message": message,
108
+ "percentage": 100,
109
+ "status": "completed",
110
+ }
111
+ self.callback(progress_data)
112
+
113
+ def fail(self, message: str) -> None:
114
+ """
115
+ Mark indexing as failed.
116
+
117
+ Keeps current percentage but sets status to error.
118
+
119
+ Args:
120
+ message: Error message
121
+ """
122
+ self.current_status = "error"
123
+
124
+ if self.callback:
125
+ progress_data: ProgressUpdate = {
126
+ "repository_name": self.repository_name,
127
+ "stage": self.current_stage,
128
+ "message": message,
129
+ "percentage": self.current_percentage,
130
+ "status": "error",
131
+ }
132
+ self.callback(progress_data)
File without changes
@@ -0,0 +1,180 @@
1
+ """Use case for clearing all data from Qdrant and Neo4j."""
2
+ import structlog
3
+
4
+ from src.infrastructure.persistence.neo4j_dependency_repository import Neo4jDependencyGraphRepository
5
+ from qdrant_client import QdrantClient
6
+
7
+ logger = structlog.get_logger(__name__)
8
+
9
+
10
+ class ClearAllDataUseCase:
11
+ """
12
+ Use case for clearing all indexed data.
13
+
14
+ Deletes:
15
+ - All collections in Qdrant
16
+ - All nodes and relationships in Neo4j
17
+
18
+ ⚠️ DESTRUCTIVE OPERATION - Use with caution!
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ graph_repository: Neo4jDependencyGraphRepository,
24
+ qdrant_client: QdrantClient,
25
+ collection_name: str
26
+ ):
27
+ """
28
+ Initialize use case.
29
+
30
+ Args:
31
+ graph_repository: Neo4j dependency graph repository
32
+ qdrant_client: Qdrant client
33
+ collection_name: Name of Qdrant collection to delete
34
+ """
35
+ self.graph_repository = graph_repository
36
+ self.qdrant_client = qdrant_client
37
+ self.collection_name = collection_name
38
+
39
+ async def execute(self) -> dict:
40
+ """
41
+ Execute clear all operation.
42
+
43
+ Returns:
44
+ Dict with operation results
45
+ """
46
+ logger.warning("clear_all_data_starting", operation="DESTRUCTIVE")
47
+
48
+ results = {
49
+ "neo4j": {"success": False, "nodes_deleted": 0, "relationships_deleted": 0},
50
+ "qdrant": {"success": False, "collection_deleted": False}
51
+ }
52
+
53
+ # 1. Clear Neo4j
54
+ try:
55
+ neo4j_result = await self._clear_neo4j()
56
+ results["neo4j"] = neo4j_result
57
+ logger.info(
58
+ "neo4j_cleared",
59
+ nodes_deleted=neo4j_result["nodes_deleted"],
60
+ relationships_deleted=neo4j_result["relationships_deleted"]
61
+ )
62
+ except Exception as e:
63
+ logger.error(
64
+ "neo4j_clear_failed",
65
+ error=str(e),
66
+ error_type=type(e).__name__
67
+ )
68
+ results["neo4j"]["error"] = str(e)
69
+
70
+ # 2. Clear Qdrant
71
+ try:
72
+ qdrant_result = await self._clear_qdrant()
73
+ results["qdrant"] = qdrant_result
74
+ logger.info(
75
+ "qdrant_cleared",
76
+ collection_deleted=qdrant_result["collection_deleted"]
77
+ )
78
+ except Exception as e:
79
+ logger.error(
80
+ "qdrant_clear_failed",
81
+ error=str(e),
82
+ error_type=type(e).__name__
83
+ )
84
+ results["qdrant"]["error"] = str(e)
85
+
86
+ logger.warning("clear_all_data_completed", results=results)
87
+
88
+ return results
89
+
90
+ async def _clear_neo4j(self) -> dict:
91
+ """
92
+ Clear all nodes and relationships from Neo4j.
93
+
94
+ Returns:
95
+ Dict with deletion metrics
96
+ """
97
+ async with self.graph_repository.driver.session() as session:
98
+ # Get counts before deletion
99
+ count_result = await session.run("""
100
+ MATCH (n)
101
+ OPTIONAL MATCH (n)-[r]->()
102
+ RETURN count(DISTINCT n) as nodes, count(r) as rels
103
+ """)
104
+ count_record = await count_result.single()
105
+ nodes_before = count_record["nodes"]
106
+ rels_before = count_record["rels"]
107
+
108
+ # Delete in batches of 10k to avoid Neo4j heap overflow
109
+ # (single DETACH DELETE on large graphs silently fails with OOM)
110
+ total_deleted = 0
111
+ while True:
112
+ result = await session.run("""
113
+ MATCH (n)
114
+ WITH n LIMIT 10000
115
+ DETACH DELETE n
116
+ RETURN count(*) AS deleted
117
+ """)
118
+ row = await result.single()
119
+ batch = row["deleted"] if row else 0
120
+ total_deleted += batch
121
+ if batch == 0:
122
+ break
123
+
124
+ logger.debug(
125
+ "neo4j_deletion_executed",
126
+ nodes_deleted=total_deleted,
127
+ relationships_deleted=rels_before
128
+ )
129
+
130
+ return {
131
+ "success": True,
132
+ "nodes_deleted": total_deleted,
133
+ "relationships_deleted": rels_before
134
+ }
135
+
136
+ async def _clear_qdrant(self) -> dict:
137
+ """
138
+ Clear Qdrant collection.
139
+
140
+ Returns:
141
+ Dict with deletion result
142
+ """
143
+ try:
144
+ # Check if collection exists
145
+ collections = self.qdrant_client.get_collections().collections
146
+ collection_exists = any(c.name == self.collection_name for c in collections)
147
+
148
+ if collection_exists:
149
+ # Delete collection
150
+ self.qdrant_client.delete_collection(collection_name=self.collection_name)
151
+
152
+ logger.debug(
153
+ "qdrant_collection_deleted",
154
+ collection=self.collection_name
155
+ )
156
+
157
+ return {
158
+ "success": True,
159
+ "collection_deleted": True,
160
+ "collection_name": self.collection_name
161
+ }
162
+ else:
163
+ logger.debug(
164
+ "qdrant_collection_not_found",
165
+ collection=self.collection_name
166
+ )
167
+
168
+ return {
169
+ "success": True,
170
+ "collection_deleted": False,
171
+ "message": f"Collection '{self.collection_name}' does not exist"
172
+ }
173
+
174
+ except Exception as e:
175
+ logger.error(
176
+ "qdrant_deletion_failed",
177
+ error=str(e),
178
+ collection=self.collection_name
179
+ )
180
+ raise