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/middleware.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/middleware.py - Grafo Concierge v3.8.0 (Absolute Solidity)
|
|
3
|
+
|
|
4
|
+
The Central Facade - GrafoConcierge.
|
|
5
|
+
|
|
6
|
+
This is the class that the outside world consumes. It encapsulates ALL the
|
|
7
|
+
complexity of the internal layers (storage, ingestion, services, core)
|
|
8
|
+
into a clean, project-oriented public API.
|
|
9
|
+
|
|
10
|
+
Consumers of this class:
|
|
11
|
+
- interface/mcp_server.py (MCP Server → Claude Desktop, Cursor)
|
|
12
|
+
- interface/cli.py (Command Line Interface)
|
|
13
|
+
- interface/action_hooks.py (Operational Modules)
|
|
14
|
+
- Integration tests
|
|
15
|
+
|
|
16
|
+
Public methods:
|
|
17
|
+
- register_project() → Registers a new project in the graph
|
|
18
|
+
- wake_up() → Re-activates consciousness: compass + wings + commits
|
|
19
|
+
- mine() → Ingests files (concierge mine)
|
|
20
|
+
- hybrid_search() → Complete Hybrid Search v4
|
|
21
|
+
- commit_memory() → Registers consolidated changes
|
|
22
|
+
- get_resume() → Context Compass (concise summary)
|
|
23
|
+
- lazy_load() → On-demand node loading
|
|
24
|
+
- delete_project() → Cascaded project deletion
|
|
25
|
+
- find_similar() → Projects in the same wing
|
|
26
|
+
- status() → Project statistics
|
|
27
|
+
|
|
28
|
+
Principle: No internal class (SqliteStore, ChromaVectorStore, etc.)
|
|
29
|
+
is exposed to the outside world. Everything flows through this facade.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
import uuid as uuid_lib
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
from typing import Any, Optional
|
|
38
|
+
|
|
39
|
+
from core.config import ConciergeConfig, DEFAULT_CONFIG
|
|
40
|
+
from core.project_index import ProjectIndex
|
|
41
|
+
from core.hybrid_search import HybridSearchEngine
|
|
42
|
+
from core.memory_extractor import SemanticExtractor
|
|
43
|
+
from storage.store import SqliteStore
|
|
44
|
+
from storage.vector_store import ChromaVectorStore, EmbeddingManager
|
|
45
|
+
from ingestion.orchestrator import IngestionManager
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("grafo-concierge.middleware")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GrafoConcierge:
|
|
51
|
+
"""Central Facade of Grafo Concierge - unified public API.
|
|
52
|
+
|
|
53
|
+
By instantiating this class, all subsystems are initialized
|
|
54
|
+
automatically and interconnected.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
sqlite_store: SqliteStore instance.
|
|
58
|
+
vector_store: ChromaVectorStore instance.
|
|
59
|
+
embedding_manager: EmbeddingManager instance.
|
|
60
|
+
ingestion_manager: IngestionManager instance.
|
|
61
|
+
config: Centralized parameters (default: DEFAULT_CONFIG).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
sqlite_store: SqliteStore,
|
|
67
|
+
vector_store: ChromaVectorStore,
|
|
68
|
+
embedding_manager: EmbeddingManager,
|
|
69
|
+
ingestion_manager: IngestionManager,
|
|
70
|
+
config: ConciergeConfig = DEFAULT_CONFIG,
|
|
71
|
+
llm_adapter: Any = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self._store = sqlite_store
|
|
74
|
+
self._vector = vector_store
|
|
75
|
+
self._embedder = embedding_manager
|
|
76
|
+
self._ingestion = ingestion_manager
|
|
77
|
+
self._config = config
|
|
78
|
+
|
|
79
|
+
# Core submodules
|
|
80
|
+
self._project_index = ProjectIndex(sqlite_store, config)
|
|
81
|
+
self._search_engine = HybridSearchEngine(
|
|
82
|
+
sqlite_store=sqlite_store,
|
|
83
|
+
vector_store=vector_store,
|
|
84
|
+
embedding_manager=embedding_manager,
|
|
85
|
+
project_index=self._project_index,
|
|
86
|
+
config=config,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Semantic Extraction Engine (requires LLM adapter)
|
|
90
|
+
self._semantic_extractor: SemanticExtractor | None = (
|
|
91
|
+
SemanticExtractor(llm_adapter) if llm_adapter else None
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logger.info("GrafoConcierge (Fachada) inicializada com sucesso.")
|
|
95
|
+
|
|
96
|
+
# ===================================================================
|
|
97
|
+
# REGISTER — Registers a new project
|
|
98
|
+
# ===================================================================
|
|
99
|
+
|
|
100
|
+
def register_project(
|
|
101
|
+
self,
|
|
102
|
+
folder_name: str,
|
|
103
|
+
wing: Optional[str] = None,
|
|
104
|
+
privacy_level: str = "PUBLIC",
|
|
105
|
+
summary: Optional[str] = None,
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Registers a new project in the graph.
|
|
108
|
+
|
|
109
|
+
If the project already exists (by folder_name), returns the existing UUID.
|
|
110
|
+
Otherwise, generates a v4 UUID and creates the record.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
folder_name: Source directory / project identifier.
|
|
114
|
+
wing: Primary Wing (if None, it will be automatically categorized
|
|
115
|
+
after the first ingestion).
|
|
116
|
+
privacy_level: Privacy level (PUBLIC, INTERNAL, RESTRICTED).
|
|
117
|
+
summary: Initial description of the project.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
UUID of the project (new or existing).
|
|
121
|
+
"""
|
|
122
|
+
# Checks if already exists
|
|
123
|
+
try:
|
|
124
|
+
existing = self._store.get_project(folder_name)
|
|
125
|
+
logger.info("Projeto já existe: '%s' → %s", folder_name, existing["uuid"])
|
|
126
|
+
return existing["uuid"]
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
project_uuid = str(uuid_lib.uuid4())
|
|
131
|
+
primary_wing = wing or self._config.default_wing
|
|
132
|
+
|
|
133
|
+
self._store.create_project(
|
|
134
|
+
uuid=project_uuid,
|
|
135
|
+
folder_name=folder_name,
|
|
136
|
+
primary_wing=primary_wing,
|
|
137
|
+
privacy_level=privacy_level,
|
|
138
|
+
summary=summary,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
logger.info(
|
|
142
|
+
"Projeto registrado: '%s' → %s (wing='%s', privacy='%s')",
|
|
143
|
+
folder_name, project_uuid, primary_wing, privacy_level,
|
|
144
|
+
)
|
|
145
|
+
return project_uuid
|
|
146
|
+
|
|
147
|
+
# ===================================================================
|
|
148
|
+
# WAKE UP — Re-activation of consciousness
|
|
149
|
+
# ===================================================================
|
|
150
|
+
|
|
151
|
+
def wake_up(self, project_uuid: str) -> dict:
|
|
152
|
+
"""Re-activates the agent's consciousness for a project.
|
|
153
|
+
|
|
154
|
+
Returns the minimum context package necessary for the agent
|
|
155
|
+
to resume work: Compass, Reference Wings, and last commits.
|
|
156
|
+
|
|
157
|
+
Aligned with the MCP Tool concierge_wakeup.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
project_uuid: Project UUID.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dict containing:
|
|
164
|
+
{
|
|
165
|
+
"project": dict (project data),
|
|
166
|
+
"resume": str (Context Compass),
|
|
167
|
+
"reference_wings": list[str],
|
|
168
|
+
"recent_commits": list[dict],
|
|
169
|
+
"stats": dict,
|
|
170
|
+
}
|
|
171
|
+
"""
|
|
172
|
+
project = self._store.get_project(project_uuid)
|
|
173
|
+
ref_wings = self._project_index.get_reference_wings(project_uuid)
|
|
174
|
+
recent_commits = self._store.get_recent_commits(project_uuid, limit=5)
|
|
175
|
+
stats = self._store.get_project_stats(project_uuid)
|
|
176
|
+
|
|
177
|
+
resume = project.get("summary", "")
|
|
178
|
+
if not resume:
|
|
179
|
+
resume = f"Projeto '{project.get('folder_name', 'unknown')}' — sem Bússola de Contexto definida."
|
|
180
|
+
|
|
181
|
+
result = {
|
|
182
|
+
"project": project,
|
|
183
|
+
"resume": resume,
|
|
184
|
+
"reference_wings": ref_wings,
|
|
185
|
+
"recent_commits": recent_commits,
|
|
186
|
+
"stats": stats,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
logger.info(
|
|
190
|
+
"Wake-up: projeto=%s, commits=%d, ref_wings=%d",
|
|
191
|
+
project_uuid, len(recent_commits), len(ref_wings),
|
|
192
|
+
)
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
# ===================================================================
|
|
196
|
+
# MINE — Ingestion of files
|
|
197
|
+
# ===================================================================
|
|
198
|
+
|
|
199
|
+
def mine(
|
|
200
|
+
self,
|
|
201
|
+
project_uuid: str,
|
|
202
|
+
source_path: str,
|
|
203
|
+
auto_tag: bool = True,
|
|
204
|
+
auto_categorize: bool = True,
|
|
205
|
+
) -> dict:
|
|
206
|
+
"""Runs the complete ingestion pipeline (concierge mine).
|
|
207
|
+
|
|
208
|
+
Delegates to IngestionManager and optionally re-categorizes
|
|
209
|
+
the project's Primary Wing after ingestion.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
project_uuid: Project UUID.
|
|
213
|
+
source_path: Path to the source directory.
|
|
214
|
+
auto_tag: Enable automatic tag detection.
|
|
215
|
+
auto_categorize: Re-categorize wing after ingestion.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Dict compatible with the MCP Tool concierge_mine response.
|
|
219
|
+
"""
|
|
220
|
+
result = self._ingestion.mine(project_uuid, source_path, auto_tag)
|
|
221
|
+
result_dict = result.to_dict()
|
|
222
|
+
|
|
223
|
+
# Auto-categorization post-ingestion
|
|
224
|
+
if auto_categorize and result_dict.get("nodes_created", 0) > 0:
|
|
225
|
+
try:
|
|
226
|
+
wing = self._project_index.auto_categorize_project(project_uuid)
|
|
227
|
+
result_dict["auto_categorized_wing"] = wing
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.warning("Auto-categorização falhou: %s", e)
|
|
230
|
+
|
|
231
|
+
return result_dict
|
|
232
|
+
|
|
233
|
+
# ===================================================================
|
|
234
|
+
# SEARCH — Hybrid Search v4
|
|
235
|
+
# ===================================================================
|
|
236
|
+
|
|
237
|
+
def hybrid_search(
|
|
238
|
+
self,
|
|
239
|
+
query: str,
|
|
240
|
+
project_uuid: str,
|
|
241
|
+
top_k: Optional[int] = None,
|
|
242
|
+
include_references: bool = False,
|
|
243
|
+
all_wings: bool = False,
|
|
244
|
+
node_type: Optional[str] = None,
|
|
245
|
+
) -> list[dict]:
|
|
246
|
+
"""Hybrid Search v4 — Complete Pipeline.
|
|
247
|
+
|
|
248
|
+
Delegates tri-signal orchestration to HybridSearchEngine.
|
|
249
|
+
Aligned with the MCP Tool concierge_search.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
query: Search query.
|
|
253
|
+
project_uuid: Anchor project UUID.
|
|
254
|
+
top_k: Maximum results.
|
|
255
|
+
include_references: Include Reference Wings.
|
|
256
|
+
all_wings: Search in all wings.
|
|
257
|
+
node_type: Surgical filter by node type.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of dicts with final_score and breakdown, sorted DESC.
|
|
261
|
+
"""
|
|
262
|
+
return self._search_engine.search(
|
|
263
|
+
query=query,
|
|
264
|
+
project_uuid=project_uuid,
|
|
265
|
+
top_k=top_k,
|
|
266
|
+
include_references=include_references,
|
|
267
|
+
all_wings=all_wings,
|
|
268
|
+
node_type=node_type,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# ===================================================================
|
|
272
|
+
# COMMIT — Consolidated changes registration
|
|
273
|
+
# ===================================================================
|
|
274
|
+
|
|
275
|
+
def commit_memory(
|
|
276
|
+
self,
|
|
277
|
+
project_uuid: str,
|
|
278
|
+
phase: str,
|
|
279
|
+
technical_changes: str,
|
|
280
|
+
updated_pointers: list[str],
|
|
281
|
+
node_ids: Optional[list[int]] = None,
|
|
282
|
+
) -> int:
|
|
283
|
+
"""Registers a memory commit in the graph.
|
|
284
|
+
|
|
285
|
+
Each commit saves technical changes and updated pointers.
|
|
286
|
+
If node_ids are provided, updates the last_commit_at of each node.
|
|
287
|
+
|
|
288
|
+
Aligned with the MCP Tool concierge_commit.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
project_uuid: Project UUID.
|
|
292
|
+
phase: Current phase (planning, build, done, review).
|
|
293
|
+
technical_changes: Description of technical changes.
|
|
294
|
+
updated_pointers: List of updated pointers.
|
|
295
|
+
node_ids: IDs of affected nodes (to update recency).
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
ID of the created commit.
|
|
299
|
+
"""
|
|
300
|
+
commit_id = self._store.create_commit(
|
|
301
|
+
project_uuid=project_uuid,
|
|
302
|
+
phase=phase,
|
|
303
|
+
technical_changes=technical_changes,
|
|
304
|
+
updated_pointers=updated_pointers,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Update recency of affected nodes
|
|
308
|
+
if node_ids:
|
|
309
|
+
for nid in node_ids:
|
|
310
|
+
try:
|
|
311
|
+
self._store.touch_node_commit(nid)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning("Falha ao tocar recência do nó %d: %s", nid, e)
|
|
314
|
+
|
|
315
|
+
logger.info(
|
|
316
|
+
"Commit registrado: id=%d, projeto=%s, fase='%s', nós_afetados=%d",
|
|
317
|
+
commit_id, project_uuid, phase, len(node_ids or []),
|
|
318
|
+
)
|
|
319
|
+
return commit_id
|
|
320
|
+
|
|
321
|
+
# ===================================================================
|
|
322
|
+
# RESUME — Context Compass
|
|
323
|
+
# ===================================================================
|
|
324
|
+
|
|
325
|
+
def get_resume(self, project_uuid: str) -> str:
|
|
326
|
+
"""Returns the Context Compass (concise summary) of the project.
|
|
327
|
+
|
|
328
|
+
Aligned with the MCP Tool concierge_resume.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
project_uuid: Project UUID.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
String with the project summary (max ~300 tokens).
|
|
335
|
+
"""
|
|
336
|
+
project = self._store.get_project(project_uuid)
|
|
337
|
+
resume = project.get("summary", "")
|
|
338
|
+
|
|
339
|
+
if not resume:
|
|
340
|
+
stats = self._store.get_project_stats(project_uuid)
|
|
341
|
+
resume = (
|
|
342
|
+
f"Projeto '{project.get('folder_name', 'unknown')}' "
|
|
343
|
+
f"com {stats.get('total_nodes', 0)} nós e "
|
|
344
|
+
f"{stats.get('total_edges', 0)} arestas. "
|
|
345
|
+
f"Ala: {project.get('primary_wing', 'geral')}."
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return resume
|
|
349
|
+
|
|
350
|
+
# ===================================================================
|
|
351
|
+
# LAZY LOAD — On-demand node loading
|
|
352
|
+
# ===================================================================
|
|
353
|
+
|
|
354
|
+
def lazy_load(self, node_id: int) -> dict:
|
|
355
|
+
"""Loads complete node data under demand.
|
|
356
|
+
|
|
357
|
+
Updates the node's last_accessed to record the query.
|
|
358
|
+
Aligned with the MCP Tool concierge_load.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
node_id: ID of the node to load.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Dict with all node fields + outgoing edges.
|
|
365
|
+
"""
|
|
366
|
+
node = self._store.get_node(node_id)
|
|
367
|
+
|
|
368
|
+
# Update last_accessed (relevant for Selective Amnesia)
|
|
369
|
+
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
370
|
+
self._store.update_node(node_id, last_accessed=now)
|
|
371
|
+
|
|
372
|
+
# Load outgoing edges for context
|
|
373
|
+
edges_out = self._store.get_edges_from(node_id)
|
|
374
|
+
|
|
375
|
+
result = {
|
|
376
|
+
**node,
|
|
377
|
+
"edges_out": edges_out,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
logger.debug("Lazy Load: nó=%d, arestas=%d", node_id, len(edges_out))
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
# ===================================================================
|
|
384
|
+
# DELETE — Project removal
|
|
385
|
+
# ===================================================================
|
|
386
|
+
|
|
387
|
+
def delete_project(self, project_uuid: str) -> None:
|
|
388
|
+
"""Removes a project and all associated data.
|
|
389
|
+
|
|
390
|
+
Cascade: nodes, edges, trajectories, commits, and reference_wings.
|
|
391
|
+
Also clears associated vectors in ChromaDB.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
project_uuid: UUID of the project to remove.
|
|
395
|
+
"""
|
|
396
|
+
# Remove vectors from ChromaDB before SQLite (needs node_ids)
|
|
397
|
+
try:
|
|
398
|
+
nodes = self._store.get_nodes_by_project(project_uuid)
|
|
399
|
+
if nodes:
|
|
400
|
+
doc_ids = [f"node_{n['id']}" for n in nodes]
|
|
401
|
+
self._vector.delete_batch(doc_ids)
|
|
402
|
+
logger.info("Vetores removidos: %d embeddings do projeto %s", len(doc_ids), project_uuid)
|
|
403
|
+
except Exception as e:
|
|
404
|
+
logger.warning("Falha ao limpar vetores do projeto %s: %s", project_uuid, e)
|
|
405
|
+
|
|
406
|
+
# Remove from SQLite (CASCADE handles nodes, edges, etc.)
|
|
407
|
+
self._store.delete_project(project_uuid)
|
|
408
|
+
logger.info("Projeto removido: %s", project_uuid)
|
|
409
|
+
|
|
410
|
+
# ===================================================================
|
|
411
|
+
# SIMILAR — Projects in the same wing
|
|
412
|
+
# ===================================================================
|
|
413
|
+
|
|
414
|
+
def find_similar(
|
|
415
|
+
self,
|
|
416
|
+
project_uuid: str,
|
|
417
|
+
limit: int = 5,
|
|
418
|
+
include_references: bool = False,
|
|
419
|
+
all_wings: bool = False,
|
|
420
|
+
) -> list[dict]:
|
|
421
|
+
"""Searches similar projects by domain (same wing).
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
project_uuid: Anchor project UUID.
|
|
425
|
+
limit: Maximum results.
|
|
426
|
+
include_references: Include Reference Wings.
|
|
427
|
+
all_wings: All wings.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
List of dicts with data of similar projects.
|
|
431
|
+
"""
|
|
432
|
+
return self._project_index.find_similar_projects(
|
|
433
|
+
project_uuid, limit, include_references, all_wings,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# ===================================================================
|
|
437
|
+
# STATUS — Project statistics
|
|
438
|
+
# ===================================================================
|
|
439
|
+
|
|
440
|
+
def status(self, project_uuid: str) -> dict:
|
|
441
|
+
"""Returns complete project statistics.
|
|
442
|
+
|
|
443
|
+
Aligned with the MCP Tool concierge_status.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
project_uuid: Project UUID.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dict with counters for nodes, edges, commits, trajectories, etc.
|
|
450
|
+
"""
|
|
451
|
+
project = self._store.get_project(project_uuid)
|
|
452
|
+
stats = self._store.get_project_stats(project_uuid)
|
|
453
|
+
ref_wings = self._project_index.get_reference_wings(project_uuid)
|
|
454
|
+
last_phase = self._store.get_last_commit_phase(project_uuid)
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
"project": project,
|
|
458
|
+
"stats": stats,
|
|
459
|
+
"reference_wings": ref_wings,
|
|
460
|
+
"last_commit_phase": last_phase,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# ===================================================================
|
|
464
|
+
# Submodule access (for advanced use / testing)
|
|
465
|
+
# ===================================================================
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def project_index(self) -> ProjectIndex:
|
|
469
|
+
"""Access to ProjectIndex for advanced wing operations."""
|
|
470
|
+
return self._project_index
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def search_engine(self) -> HybridSearchEngine:
|
|
474
|
+
"""Access to HybridSearchEngine for customized searches."""
|
|
475
|
+
return self._search_engine
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def store(self) -> SqliteStore:
|
|
479
|
+
"""Access to SqliteStore (for advanced internal operations)."""
|
|
480
|
+
return self._store
|
|
481
|
+
|
|
482
|
+
def search_symbols(self, query: str, project_uuid: Optional[str] = None, limit: int = 50) -> list[dict]:
|
|
483
|
+
"""Performs quick symbol search on FTS5."""
|
|
484
|
+
return self._store.search_symbols(query, project_uuid, limit)
|
|
485
|
+
|
|
486
|
+
def get_implementations(self, symbol_id: int) -> dict:
|
|
487
|
+
"""Returns the exact AST code block stored in the node."""
|
|
488
|
+
node = self._store.get_node(symbol_id)
|
|
489
|
+
return {
|
|
490
|
+
"id": node["id"],
|
|
491
|
+
"label": node["label"],
|
|
492
|
+
"type": node["type"],
|
|
493
|
+
"project_uuid": node["project_uuid"],
|
|
494
|
+
"content": node.get("content"),
|
|
495
|
+
"file_hash": node.get("file_hash"),
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
def get_callers(self, symbol_id: int) -> list[dict]:
|
|
499
|
+
"""Queries edges to return all calls to the symbol."""
|
|
500
|
+
return self._store.get_callers(symbol_id)
|
|
501
|
+
|
|
502
|
+
def get_full_topology(self, project_uuid: Optional[str] = None) -> dict[str, list[dict]]:
|
|
503
|
+
"""Returns the complete topology (nodes and edges) in a lightweight format."""
|
|
504
|
+
return self._store.get_lightweight_topology(project_uuid)
|
|
505
|
+
|
|
506
|
+
# ===================================================================
|
|
507
|
+
# STORE FACT — Storing Semantic Facts via SemanticExtractor
|
|
508
|
+
# ===================================================================
|
|
509
|
+
|
|
510
|
+
def store_fact(
|
|
511
|
+
self,
|
|
512
|
+
scope_type: str,
|
|
513
|
+
scope_id: str,
|
|
514
|
+
fact_statement: str,
|
|
515
|
+
) -> list[dict]:
|
|
516
|
+
"""Stores a semantic fact in the graph via SemanticExtractor.
|
|
517
|
+
|
|
518
|
+
The SemanticExtractor evaluates the fact against existing facts
|
|
519
|
+
of the scope and decides: ADD, UPDATE, DELETE, or NOOP.
|
|
520
|
+
|
|
521
|
+
Aligned with the MCP Tool concierge_store_fact.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
scope_type: Scope type ('user', 'session', 'agent', 'org').
|
|
525
|
+
scope_id: Unique identifier of the scope.
|
|
526
|
+
fact_statement: Text of the fact/preference to store.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of dicts detailing the decisions made.
|
|
530
|
+
|
|
531
|
+
Raises:
|
|
532
|
+
RuntimeError: If SemanticExtractor is not configured.
|
|
533
|
+
"""
|
|
534
|
+
if self._semantic_extractor is None:
|
|
535
|
+
raise RuntimeError(
|
|
536
|
+
"SemanticExtractor not available: llm_adapter was not "
|
|
537
|
+
"provided during GrafoConcierge initialization."
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def _do_store(conn) -> list[dict]:
|
|
541
|
+
return self._semantic_extractor.evaluate_and_store_facts(
|
|
542
|
+
conn=conn,
|
|
543
|
+
scope_type=scope_type,
|
|
544
|
+
scope_id=scope_id,
|
|
545
|
+
new_facts=[fact_statement],
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
results = self._store.write_callback(_do_store)
|
|
549
|
+
logger.info(
|
|
550
|
+
"store_fact: scope=%s/%s, results=%d",
|
|
551
|
+
scope_type, scope_id, len(results),
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Episodic vector synchronization if vector backend is QdrantVectorStore
|
|
555
|
+
try:
|
|
556
|
+
from core.vector_backend import QdrantVectorStore
|
|
557
|
+
if isinstance(self._vector, QdrantVectorStore) and results:
|
|
558
|
+
for fact in results:
|
|
559
|
+
fact_id = fact.get("id")
|
|
560
|
+
statement = fact.get("fact_statement")
|
|
561
|
+
if fact_id is not None and statement:
|
|
562
|
+
emb = self._embedder.embed(statement)
|
|
563
|
+
if emb:
|
|
564
|
+
metadata = {
|
|
565
|
+
"scope_type": scope_type,
|
|
566
|
+
"scope_id": scope_id,
|
|
567
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
568
|
+
"utility_alpha": 1.0,
|
|
569
|
+
"utility_beta": 1.0,
|
|
570
|
+
"fact_id": fact_id,
|
|
571
|
+
"fact_statement": statement
|
|
572
|
+
}
|
|
573
|
+
self._vector.store_embedding(
|
|
574
|
+
doc_id=f"fact_{fact_id}",
|
|
575
|
+
embedding=emb,
|
|
576
|
+
metadata=metadata
|
|
577
|
+
)
|
|
578
|
+
logger.info("Semantic fact %d synchronized in Qdrant (episodic_memory).", fact_id)
|
|
579
|
+
except Exception as q_err:
|
|
580
|
+
logger.warning("Failed to synchronize semantic fact in Qdrant: %s", q_err)
|
|
581
|
+
|
|
582
|
+
return results
|
|
583
|
+
|
|
584
|
+
# ===================================================================
|
|
585
|
+
# USER CORE MEMORY — Patch 1
|
|
586
|
+
# ===================================================================
|
|
587
|
+
|
|
588
|
+
def set_core_memory(
|
|
589
|
+
self,
|
|
590
|
+
scope_type: str,
|
|
591
|
+
scope_id: str,
|
|
592
|
+
block_label: str,
|
|
593
|
+
content: str,
|
|
594
|
+
) -> int:
|
|
595
|
+
"""Stores or updates a core memory block of the user/session.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
scope_type: 'user', 'session', 'agent', or 'org'.
|
|
599
|
+
scope_id: Unique identifier of the scope.
|
|
600
|
+
block_label: Label of the memory block.
|
|
601
|
+
content: Content to store.
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
ID of the inserted/updated record.
|
|
605
|
+
"""
|
|
606
|
+
return self._store.set_core_memory(scope_type, scope_id, block_label, content)
|
|
607
|
+
|
|
608
|
+
def get_core_memory_blocks(
|
|
609
|
+
self,
|
|
610
|
+
scope_type: str,
|
|
611
|
+
scope_id: str,
|
|
612
|
+
block_label: Optional[str] = None,
|
|
613
|
+
) -> list[dict]:
|
|
614
|
+
"""Returns core memory blocks for a scope.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
scope_type: Scope type.
|
|
618
|
+
scope_id: Unique identifier of the scope.
|
|
619
|
+
block_label: If provided, returns only the specific block
|
|
620
|
+
(list with 0 or 1 element). If absent, returns all.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
List of dicts with the user_core_memory records.
|
|
624
|
+
"""
|
|
625
|
+
if block_label:
|
|
626
|
+
record = self._store.get_core_memory(scope_type, scope_id, block_label)
|
|
627
|
+
return [record] if record else []
|
|
628
|
+
return self._store.list_core_memory_blocks(scope_type, scope_id)
|
|
629
|
+
|
|
630
|
+
# ===================================================================
|
|
631
|
+
# BAYESIAN FEEDBACK LOOP — Patch 3
|
|
632
|
+
# ===================================================================
|
|
633
|
+
|
|
634
|
+
def update_fact_utility(self, fact_id: int, was_useful: bool) -> None:
|
|
635
|
+
"""Updates the Bayesian utility of a semantic_fact.
|
|
636
|
+
|
|
637
|
+
Increments utility_alpha (success) or utility_beta (failure) of the fact,
|
|
638
|
+
feeding Thompson Sampling in HybridSearchEngine.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
fact_id: ID of the semantic fact.
|
|
642
|
+
was_useful: True if the fact was useful, False otherwise.
|
|
643
|
+
"""
|
|
644
|
+
from storage.semantic_logic import update_memory_utility
|
|
645
|
+
|
|
646
|
+
def _do_update(conn) -> None:
|
|
647
|
+
update_memory_utility(conn, fact_id, was_useful)
|
|
648
|
+
|
|
649
|
+
self._store.write_callback(_do_update)
|
|
650
|
+
logger.info(
|
|
651
|
+
"update_fact_utility: fact_id=%d, was_useful=%s → %s atualizado.",
|
|
652
|
+
fact_id, was_useful, "utility_alpha" if was_useful else "utility_beta",
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# ===================================================================
|
|
656
|
+
# MCP ARSENAL — Backend-6.1: Life Cycle + Telemetry + Vector
|
|
657
|
+
# ===================================================================
|
|
658
|
+
|
|
659
|
+
def update_project(self, project_uuid: str, **fields: Any) -> None:
|
|
660
|
+
"""Updates allowed fields of a project (registry).
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
project_uuid: Project UUID.
|
|
664
|
+
**fields: Fields to update (folder_name, primary_wing,
|
|
665
|
+
privacy_level, summary).
|
|
666
|
+
"""
|
|
667
|
+
self._store.update_project(project_uuid, **fields)
|
|
668
|
+
logger.info("update_project: %s → campos=%s", project_uuid, list(fields.keys()))
|
|
669
|
+
|
|
670
|
+
def add_reference_wing(self, project_uuid: str, wing_name: str) -> None:
|
|
671
|
+
"""Associates a Reference Wing to the project.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
project_uuid: Project UUID.
|
|
675
|
+
wing_name: Wing name to associate.
|
|
676
|
+
"""
|
|
677
|
+
self._store.add_reference_wing(project_uuid, wing_name)
|
|
678
|
+
logger.info("add_reference_wing: %s → wing=%s", project_uuid, wing_name)
|
|
679
|
+
|
|
680
|
+
def remove_reference_wing(self, project_uuid: str, wing_name: str) -> None:
|
|
681
|
+
"""Removes a Reference Wing from the project.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
project_uuid: Project UUID.
|
|
685
|
+
wing_name: Wing name to remove.
|
|
686
|
+
"""
|
|
687
|
+
self._store.remove_reference_wing(project_uuid, wing_name)
|
|
688
|
+
logger.info("remove_reference_wing: %s → wing=%s", project_uuid, wing_name)
|
|
689
|
+
|
|
690
|
+
def get_trajectories(self, project_uuid: str) -> list[dict]:
|
|
691
|
+
"""Retrieves the history of cognitive trajectories of the project.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
project_uuid: Project UUID.
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
List of dicts with the registered trajectories.
|
|
698
|
+
"""
|
|
699
|
+
return self._store.get_trajectories(project_uuid)
|
|
700
|
+
|
|
701
|
+
def count_embeddings(self, project_uuid: Optional[str] = None) -> int:
|
|
702
|
+
"""Returns the exact count of vectors in ChromaDB.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
project_uuid: If provided, counts only for this project.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Total number of stored embeddings.
|
|
709
|
+
"""
|
|
710
|
+
return self._vector.count(project_uuid)
|
|
711
|
+
|
|
712
|
+
def reset_collection(self) -> bool:
|
|
713
|
+
"""Destroys and recreates the vector collection (emergency repair).
|
|
714
|
+
|
|
715
|
+
CAUTION: Destructive and irreversible operation. Will require re-ingestion.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
True if successful, False otherwise.
|
|
719
|
+
"""
|
|
720
|
+
result = self._vector.reset_collection()
|
|
721
|
+
if result:
|
|
722
|
+
logger.warning("reset_collection: coleção vetorial destruída e recriada.")
|
|
723
|
+
return result
|