concierge-graph 3.8.2__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.
- agents/__init__.py +14 -0
- agents/revisor_critico.py +610 -0
- concierge_graph-3.8.2.dist-info/METADATA +327 -0
- concierge_graph-3.8.2.dist-info/RECORD +37 -0
- concierge_graph-3.8.2.dist-info/WHEEL +5 -0
- concierge_graph-3.8.2.dist-info/entry_points.txt +3 -0
- concierge_graph-3.8.2.dist-info/licenses/LICENSE +21 -0
- concierge_graph-3.8.2.dist-info/top_level.txt +8 -0
- core/__init__.py +23 -0
- core/config.py +192 -0
- core/hybrid_search.py +288 -0
- core/memory_extractor.py +245 -0
- core/middleware.py +723 -0
- core/probabilistic_retriever.py +99 -0
- core/project_index.py +316 -0
- core/vector_backend.py +471 -0
- ingestion/__init__.py +25 -0
- ingestion/crawler.py +722 -0
- ingestion/orchestrator.py +920 -0
- ingestion/parser.py +984 -0
- ingestion/summarizer.py +948 -0
- interface/__init__.py +16 -0
- interface/action_hooks.py +261 -0
- interface/cli.py +391 -0
- interface/mcp_server.py +1737 -0
- main.py +281 -0
- scripts/bootstrap_core_memory.py +93 -0
- services/__init__.py +13 -0
- services/janitor.py +762 -0
- storage/__init__.py +31 -0
- storage/base_backend.py +241 -0
- storage/connection.py +310 -0
- storage/logic.py +703 -0
- storage/schema.py +390 -0
- storage/semantic_logic.py +125 -0
- storage/store.py +802 -0
- storage/vector_store.py +759 -0
core/vector_backend.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/vector_backend.py — Grafo Concierge v3.8.0 (Conversational Expansion)
|
|
3
|
+
|
|
4
|
+
Concrete vector backend based on Qdrant with support for:
|
|
5
|
+
- Namespace separation by scope (scope_type, scope_id)
|
|
6
|
+
- Isolated collection for episodic memory (episodic_memory)
|
|
7
|
+
- Strict validation of temporal payloads
|
|
8
|
+
- Defensive import (Qdrant available or NO-OP)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("grafo-concierge.qdrant-backend")
|
|
18
|
+
|
|
19
|
+
# Defensive import of QdrantClient
|
|
20
|
+
try:
|
|
21
|
+
import qdrant_client
|
|
22
|
+
from qdrant_client.http import models as qdrant_models
|
|
23
|
+
QDRANT_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
QDRANT_AVAILABLE = False
|
|
26
|
+
logger.error(
|
|
27
|
+
"[CRITICAL] qdrant-client not found. QdrantVectorStore operating in NO-OP mode. Semantic searches will return empty!"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from storage.base_backend import BaseVectorBackend, VectorSearchResult
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class QdrantVectorStore(BaseVectorBackend):
|
|
34
|
+
"""Vector backend based on Qdrant.
|
|
35
|
+
|
|
36
|
+
Ensures isolation between the code AST collection and the
|
|
37
|
+
episodic/conversational history collection.
|
|
38
|
+
|
|
39
|
+
The episodic_memory collection payload requires keys:
|
|
40
|
+
- scope_type
|
|
41
|
+
- scope_id
|
|
42
|
+
- timestamp
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
url: str = "http://localhost:6333",
|
|
48
|
+
api_key: Optional[str] = None,
|
|
49
|
+
location: Optional[str] = None,
|
|
50
|
+
memory: bool = False,
|
|
51
|
+
collection_name: str = "grafo_concierge",
|
|
52
|
+
embedding_dimensions: int = 384,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._collection_name = collection_name
|
|
55
|
+
self._dimensions = embedding_dimensions
|
|
56
|
+
self._last_warn_time = 0.0
|
|
57
|
+
|
|
58
|
+
# Saved parameters for reconnection
|
|
59
|
+
self._url = url
|
|
60
|
+
self._api_key = api_key
|
|
61
|
+
self._location = location
|
|
62
|
+
self._memory = memory
|
|
63
|
+
|
|
64
|
+
# Dynamic connection control and backoff
|
|
65
|
+
self._connected = False
|
|
66
|
+
self._client: Optional[qdrant_client.QdrantClient] = None
|
|
67
|
+
self._backoff = 1.0
|
|
68
|
+
self._max_backoff = 60.0
|
|
69
|
+
self._last_connect_attempt = 0.0
|
|
70
|
+
|
|
71
|
+
import os
|
|
72
|
+
if os.environ.get("GRAFO_LIGHTWEIGHT_MODE", "false").lower() == "true":
|
|
73
|
+
logger.info("QdrantVectorStore operating in NO-OP mode (Lightweight Mode active).")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Tries initial connection
|
|
77
|
+
self._ensure_connected()
|
|
78
|
+
|
|
79
|
+
def _ensure_connected(self) -> bool:
|
|
80
|
+
"""Ensures a dynamic and resilient connection to Qdrant using exponential backoff."""
|
|
81
|
+
if not QDRANT_AVAILABLE:
|
|
82
|
+
return False
|
|
83
|
+
if self._connected and self._client:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
current_time = time.time()
|
|
87
|
+
# Respects the current backoff interval to avoid frantic requests
|
|
88
|
+
if current_time - self._last_connect_attempt < self._backoff:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
self._last_connect_attempt = current_time
|
|
92
|
+
try:
|
|
93
|
+
logger.info("Attempting to establish connection to Qdrant...")
|
|
94
|
+
if self._memory:
|
|
95
|
+
self._client = qdrant_client.QdrantClient(":memory:")
|
|
96
|
+
elif self._location:
|
|
97
|
+
self._client = qdrant_client.QdrantClient(location=self._location, api_key=self._api_key)
|
|
98
|
+
else:
|
|
99
|
+
self._client = qdrant_client.QdrantClient(url=self._url, api_key=self._api_key)
|
|
100
|
+
|
|
101
|
+
# Initializes necessary collections
|
|
102
|
+
self._ensure_collection(self._collection_name)
|
|
103
|
+
self._ensure_collection("episodic_memory")
|
|
104
|
+
|
|
105
|
+
self._connected = True
|
|
106
|
+
self._backoff = 1.0 # reset backoff
|
|
107
|
+
logger.info("Qdrant connected and collections initialized successfully.")
|
|
108
|
+
return True
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self._connected = False
|
|
111
|
+
self._client = None
|
|
112
|
+
logger.warning(
|
|
113
|
+
"Failed to connect or initialize Qdrant collections: %s. Next attempt in %.1fs",
|
|
114
|
+
e,
|
|
115
|
+
self._backoff
|
|
116
|
+
)
|
|
117
|
+
# Exponentially increases backoff
|
|
118
|
+
self._backoff = min(self._backoff * 2.0, self._max_backoff)
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
def _ensure_collection(self, name: str) -> None:
|
|
122
|
+
"""Creates the collection if it does not exist in an idempotent way."""
|
|
123
|
+
if not self._client:
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
exists = self._client.collection_exists(collection_name=name)
|
|
127
|
+
if not exists:
|
|
128
|
+
self._client.create_collection(
|
|
129
|
+
collection_name=name,
|
|
130
|
+
vectors_config=qdrant_models.VectorParams(
|
|
131
|
+
size=self._dimensions,
|
|
132
|
+
distance=qdrant_models.Distance.COSINE
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
logger.info("Qdrant collection '%s' created successfully.", name)
|
|
136
|
+
else:
|
|
137
|
+
logger.debug("Qdrant collection '%s' already exists.", name)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error("Failed to initialize Qdrant collection '%s': %s", name, e)
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
def _log_noop_warning(self) -> None:
|
|
143
|
+
"""Logs a warning when operating in NO-OP mode, throttled to once every 5 seconds."""
|
|
144
|
+
current_time = time.time()
|
|
145
|
+
if current_time - self._last_warn_time >= 5.0:
|
|
146
|
+
logger.warning("Operation ignored: Qdrant in NO-OP mode")
|
|
147
|
+
self._last_warn_time = current_time
|
|
148
|
+
|
|
149
|
+
def _validate_payload(self, metadata: dict) -> None:
|
|
150
|
+
"""Validates the mandatory fields depending on the target collection."""
|
|
151
|
+
if self._collection_name == "episodic_memory":
|
|
152
|
+
required = ["scope_type", "scope_id", "timestamp", "utility_alpha", "utility_beta"]
|
|
153
|
+
for r in required:
|
|
154
|
+
if r not in metadata:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Payload de 'episodic_memory' exige a chave '{r}' para indexação temporal."
|
|
157
|
+
)
|
|
158
|
+
# Scope type must be one of the permitted scopes
|
|
159
|
+
scope_type = metadata["scope_type"]
|
|
160
|
+
valid_scopes = {"user", "session", "agent", "org"}
|
|
161
|
+
if scope_type not in valid_scopes:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"scope_type inválido '{scope_type}'. Aceitos: {sorted(valid_scopes)}"
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
# Default code/AST collection requires node_id and project_uuid
|
|
167
|
+
if "node_id" not in metadata:
|
|
168
|
+
raise ValueError("metadata deve conter 'node_id' (int).")
|
|
169
|
+
if "project_uuid" not in metadata:
|
|
170
|
+
raise ValueError("metadata deve conter 'project_uuid' (str).")
|
|
171
|
+
|
|
172
|
+
# ===================================================================
|
|
173
|
+
# IMPLEMENTATION OF BaseVectorBackend CONTRACT
|
|
174
|
+
# ===================================================================
|
|
175
|
+
|
|
176
|
+
def store_embedding(
|
|
177
|
+
self,
|
|
178
|
+
doc_id: str,
|
|
179
|
+
embedding: list[float],
|
|
180
|
+
metadata: dict,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Stores an embedding with metadata/payload in Qdrant (with dynamic collection routing)."""
|
|
183
|
+
if not self._ensure_connected():
|
|
184
|
+
self._log_noop_warning()
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
self._validate_payload(metadata)
|
|
188
|
+
|
|
189
|
+
collection = "episodic_memory" if "timestamp" in metadata else self._collection_name
|
|
190
|
+
|
|
191
|
+
self._client.upsert(
|
|
192
|
+
collection_name=collection,
|
|
193
|
+
points=[
|
|
194
|
+
qdrant_models.PointStruct(
|
|
195
|
+
id=doc_id if isinstance(doc_id, (int, str)) else hash(doc_id),
|
|
196
|
+
vector=embedding,
|
|
197
|
+
payload=metadata
|
|
198
|
+
)
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
logger.debug("Embedding stored in Qdrant (collection: %s): %s", collection, doc_id)
|
|
202
|
+
|
|
203
|
+
def store_embeddings_batch(self, items: list[dict]) -> int:
|
|
204
|
+
"""Stores multiple embeddings in batch in Qdrant (with multiple collections support)."""
|
|
205
|
+
if not self._ensure_connected():
|
|
206
|
+
self._log_noop_warning()
|
|
207
|
+
return 0
|
|
208
|
+
if not items:
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
points_by_collection: dict[str, list] = {}
|
|
212
|
+
for item in items:
|
|
213
|
+
doc_id = item["doc_id"]
|
|
214
|
+
embedding = item["embedding"]
|
|
215
|
+
metadata = item["metadata"]
|
|
216
|
+
|
|
217
|
+
if embedding is None:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
self._validate_payload(metadata)
|
|
222
|
+
collection = "episodic_memory" if "timestamp" in metadata else self._collection_name
|
|
223
|
+
if collection not in points_by_collection:
|
|
224
|
+
points_by_collection[collection] = []
|
|
225
|
+
|
|
226
|
+
points_by_collection[collection].append(
|
|
227
|
+
qdrant_models.PointStruct(
|
|
228
|
+
id=doc_id if isinstance(doc_id, (int, str)) else hash(doc_id),
|
|
229
|
+
vector=embedding,
|
|
230
|
+
payload=metadata
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
except (ValueError, KeyError) as e:
|
|
234
|
+
logger.warning("Item ignored in Qdrant batch due to invalid payload: %s", e)
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
total_upserted = 0
|
|
238
|
+
for collection, points in points_by_collection.items():
|
|
239
|
+
if points:
|
|
240
|
+
self._client.upsert(
|
|
241
|
+
collection_name=collection,
|
|
242
|
+
points=points
|
|
243
|
+
)
|
|
244
|
+
total_upserted += len(points)
|
|
245
|
+
return total_upserted
|
|
246
|
+
|
|
247
|
+
def search(
|
|
248
|
+
self,
|
|
249
|
+
query_embedding: list[float],
|
|
250
|
+
project_uuids: list[str],
|
|
251
|
+
top_k: int = 10,
|
|
252
|
+
filters: Optional[dict] = None,
|
|
253
|
+
) -> list[VectorSearchResult]:
|
|
254
|
+
"""Searches by cosine similarity in Qdrant applying filters."""
|
|
255
|
+
if not self._ensure_connected():
|
|
256
|
+
self._log_noop_warning()
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
# Builds the search filter in Qdrant format
|
|
260
|
+
qdrant_filter = self._build_filter(project_uuids, filters)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
results = self._client.search(
|
|
264
|
+
collection_name=self._collection_name,
|
|
265
|
+
query_vector=query_embedding,
|
|
266
|
+
query_filter=qdrant_filter,
|
|
267
|
+
limit=top_k,
|
|
268
|
+
)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error("Error in Qdrant vector search: %s", e)
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
search_results: list[VectorSearchResult] = []
|
|
274
|
+
for res in results:
|
|
275
|
+
payload = res.payload or {}
|
|
276
|
+
# For the code collection, we extract node_id and project_uuid
|
|
277
|
+
node_id = int(payload.get("node_id", 0))
|
|
278
|
+
project_uuid = str(payload.get("project_uuid", ""))
|
|
279
|
+
|
|
280
|
+
search_results.append(
|
|
281
|
+
VectorSearchResult(
|
|
282
|
+
doc_id=str(res.id),
|
|
283
|
+
node_id=node_id,
|
|
284
|
+
project_uuid=project_uuid,
|
|
285
|
+
score=float(res.score),
|
|
286
|
+
metadata=payload,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return search_results
|
|
290
|
+
|
|
291
|
+
def delete(self, doc_id: str) -> None:
|
|
292
|
+
"""Deletes a vector by ID."""
|
|
293
|
+
if not self._ensure_connected():
|
|
294
|
+
self._log_noop_warning()
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
self._client.delete(
|
|
299
|
+
collection_name=self._collection_name,
|
|
300
|
+
points_selector=qdrant_models.PointIdsList(
|
|
301
|
+
points=[doc_id]
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.warning("Failed to delete vector in Qdrant %s: %s", doc_id, e)
|
|
306
|
+
|
|
307
|
+
def delete_batch(self, doc_ids: list[str]) -> int:
|
|
308
|
+
"""Deletes multiple vectors in batch."""
|
|
309
|
+
if not self._ensure_connected():
|
|
310
|
+
self._log_noop_warning()
|
|
311
|
+
return 0
|
|
312
|
+
if not doc_ids:
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
self._client.delete(
|
|
317
|
+
collection_name=self._collection_name,
|
|
318
|
+
points_selector=qdrant_models.PointIdsList(
|
|
319
|
+
points=doc_ids
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
return len(doc_ids)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.error("Failed to delete batch in Qdrant: %s", e)
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
def verify_sync(self, sqlite_node_ids: set[int]) -> list[str]:
|
|
328
|
+
"""Detects orphan vectors in Qdrant."""
|
|
329
|
+
if not self._ensure_connected():
|
|
330
|
+
self._log_noop_warning()
|
|
331
|
+
return []
|
|
332
|
+
|
|
333
|
+
orphans: list[str] = []
|
|
334
|
+
try:
|
|
335
|
+
# Scroll all points using Qdrant scroll API
|
|
336
|
+
offset = None
|
|
337
|
+
while True:
|
|
338
|
+
res, next_offset = self._client.scroll(
|
|
339
|
+
collection_name=self._collection_name,
|
|
340
|
+
limit=100,
|
|
341
|
+
with_payload=True,
|
|
342
|
+
offset=offset
|
|
343
|
+
)
|
|
344
|
+
for point in res:
|
|
345
|
+
payload = point.payload or {}
|
|
346
|
+
node_id = payload.get("node_id")
|
|
347
|
+
if node_id is None or int(node_id) not in sqlite_node_ids:
|
|
348
|
+
orphans.append(str(point.id))
|
|
349
|
+
if not next_offset:
|
|
350
|
+
break
|
|
351
|
+
offset = next_offset
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error("Failed in Qdrant verify_sync: %s", e)
|
|
354
|
+
|
|
355
|
+
return orphans
|
|
356
|
+
|
|
357
|
+
def health_check(self) -> bool:
|
|
358
|
+
"""Verifies if the Qdrant cluster is accessible."""
|
|
359
|
+
if not self._ensure_connected():
|
|
360
|
+
return False
|
|
361
|
+
try:
|
|
362
|
+
# A simple ping or collection query
|
|
363
|
+
self._client.get_collections()
|
|
364
|
+
return True
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.error("Qdrant health check failed: %s", e)
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
def count(self, project_uuid: Optional[str] = None) -> int:
|
|
370
|
+
"""Counts the quantity of stored vectors."""
|
|
371
|
+
if not self._ensure_connected():
|
|
372
|
+
return 0
|
|
373
|
+
try:
|
|
374
|
+
if project_uuid:
|
|
375
|
+
q_filter = qdrant_models.Filter(
|
|
376
|
+
must=[
|
|
377
|
+
qdrant_models.FieldCondition(
|
|
378
|
+
key="project_uuid",
|
|
379
|
+
match=qdrant_models.MatchValue(value=project_uuid)
|
|
380
|
+
)
|
|
381
|
+
]
|
|
382
|
+
)
|
|
383
|
+
res = self._client.count(
|
|
384
|
+
collection_name=self._collection_name,
|
|
385
|
+
count_filter=q_filter
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
res = self._client.count(collection_name=self._collection_name)
|
|
389
|
+
return res.count
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.error("Failed to count vectors in Qdrant: %s", e)
|
|
392
|
+
return 0
|
|
393
|
+
|
|
394
|
+
def _build_filter(
|
|
395
|
+
self,
|
|
396
|
+
project_uuids: list[str],
|
|
397
|
+
extra_filters: Optional[dict] = None
|
|
398
|
+
) -> Optional[qdrant_models.Filter]:
|
|
399
|
+
"""Helper to convert filters to Qdrant Filter format."""
|
|
400
|
+
must_conditions: list[Any] = []
|
|
401
|
+
|
|
402
|
+
if project_uuids:
|
|
403
|
+
if len(project_uuids) == 1:
|
|
404
|
+
must_conditions.append(
|
|
405
|
+
qdrant_models.FieldCondition(
|
|
406
|
+
key="project_uuid",
|
|
407
|
+
match=qdrant_models.MatchValue(value=project_uuids[0])
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
must_conditions.append(
|
|
412
|
+
qdrant_models.FieldCondition(
|
|
413
|
+
key="project_uuid",
|
|
414
|
+
match=qdrant_models.MatchAny(any=project_uuids)
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if extra_filters:
|
|
419
|
+
for k, v in extra_filters.items():
|
|
420
|
+
must_conditions.append(
|
|
421
|
+
qdrant_models.FieldCondition(
|
|
422
|
+
key=k,
|
|
423
|
+
match=qdrant_models.MatchValue(value=v)
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if not must_conditions:
|
|
428
|
+
return None
|
|
429
|
+
return qdrant_models.Filter(must=must_conditions)
|
|
430
|
+
|
|
431
|
+
def update_metadata(self, doc_id: str, metadata: dict) -> None:
|
|
432
|
+
"""Updates metadata/payload of a point in Qdrant."""
|
|
433
|
+
if not self._ensure_connected() or not self._client:
|
|
434
|
+
self._log_noop_warning()
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
self._client.set_payload(
|
|
439
|
+
collection_name=self._collection_name,
|
|
440
|
+
payload=metadata,
|
|
441
|
+
points=[doc_id if isinstance(doc_id, (int, str)) else hash(doc_id)]
|
|
442
|
+
)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.error("Failed to update metadata in Qdrant: %s", e)
|
|
445
|
+
|
|
446
|
+
def get_all_stored_node_ids(self) -> set[int]:
|
|
447
|
+
"""Returns all numeric node_ids present in the Qdrant collection."""
|
|
448
|
+
if not self._ensure_connected() or not self._client:
|
|
449
|
+
return set()
|
|
450
|
+
node_ids = set()
|
|
451
|
+
try:
|
|
452
|
+
offset = None
|
|
453
|
+
while True:
|
|
454
|
+
res, next_offset = self._client.scroll(
|
|
455
|
+
collection_name=self._collection_name,
|
|
456
|
+
limit=100,
|
|
457
|
+
with_payload=True,
|
|
458
|
+
offset=offset
|
|
459
|
+
)
|
|
460
|
+
for point in res:
|
|
461
|
+
payload = point.payload or {}
|
|
462
|
+
n_id = payload.get("node_id")
|
|
463
|
+
if n_id is not None:
|
|
464
|
+
node_ids.add(int(n_id))
|
|
465
|
+
if not next_offset:
|
|
466
|
+
break
|
|
467
|
+
offset = next_offset
|
|
468
|
+
return node_ids
|
|
469
|
+
except Exception as e:
|
|
470
|
+
logger.error("Error obtaining saved node_ids in Qdrant: %s", e)
|
|
471
|
+
return set()
|
ingestion/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ingestion/ - Apex Ingestion Engine - Grafo Concierge v3.8.0
|
|
3
|
+
|
|
4
|
+
Modules:
|
|
5
|
+
crawler.py → Filesystem scanning, delta detection via SHA256, .gitignore
|
|
6
|
+
parser.py → Semantic/AST Chunking with Prompt Armor (XML sanitization)
|
|
7
|
+
summarizer.py → Zoom Gear (L0/L1/L2) with Model Tiering
|
|
8
|
+
orchestrator.py → IngestionManager — coordinates Crawl → Parse → Summarize → Store
|
|
9
|
+
"""
|
|
10
|
+
from ingestion.crawler import ProjectCrawler, CrawlReport, CrawlResult
|
|
11
|
+
from ingestion.parser import FileParser, ParsedChunk
|
|
12
|
+
from ingestion.summarizer import ZoomSummarizer, SummaryResult
|
|
13
|
+
from ingestion.orchestrator import IngestionManager, IngestionResult
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ProjectCrawler",
|
|
17
|
+
"CrawlReport",
|
|
18
|
+
"CrawlResult",
|
|
19
|
+
"FileParser",
|
|
20
|
+
"ParsedChunk",
|
|
21
|
+
"ZoomSummarizer",
|
|
22
|
+
"SummaryResult",
|
|
23
|
+
"IngestionManager",
|
|
24
|
+
"IngestionResult",
|
|
25
|
+
]
|