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
interface/mcp_server.py
ADDED
|
@@ -0,0 +1,1737 @@
|
|
|
1
|
+
"""
|
|
2
|
+
interface/mcp_server.py — Grafo Concierge v3.8.0 (Absolute Solidity)
|
|
3
|
+
|
|
4
|
+
MCP (Model Context Protocol) server exposing Grafo Concierge tools
|
|
5
|
+
to LLM agents via FastMCP.
|
|
6
|
+
|
|
7
|
+
v3.8 REFACTOR: Now consumes exclusively the Central Facade
|
|
8
|
+
(core.middleware.GrafoConcierge) instead of instantiating loose internal
|
|
9
|
+
dependencies. All business logic has been moved to core/.
|
|
10
|
+
|
|
11
|
+
Tools exposed (6 tools — aligned with Architecture v3.8):
|
|
12
|
+
concierge_mine → Project ingestion (crawl → parse → store)
|
|
13
|
+
concierge_search → Hybrid Search v4 with Strict Scoping
|
|
14
|
+
concierge_commit → Audited alterations registration
|
|
15
|
+
concierge_wakeup → Consciousness reactivation (Compass + Wings)
|
|
16
|
+
concierge_resume → Context Compass (concise summary)
|
|
17
|
+
concierge_load → Lazy Load of a node on demand
|
|
18
|
+
concierge_status → System health and statistics
|
|
19
|
+
|
|
20
|
+
Architecture:
|
|
21
|
+
This module is ONLY the MCP bridge ↔ Central Facade.
|
|
22
|
+
No business logic resides here. All operations are delegated
|
|
23
|
+
to the GrafoConcierge class (core/middleware.py).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import time
|
|
31
|
+
import traceback
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
from mcp.server.fastmcp import FastMCP
|
|
35
|
+
|
|
36
|
+
from core.middleware import GrafoConcierge
|
|
37
|
+
from services import JanitorService
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("grafo-concierge.mcp")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# GrafoConciergeServer — Encapsulation of FastMCP + Central Facade
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
class GrafoConciergeServer:
|
|
47
|
+
"""MCP Server of Grafo Concierge.
|
|
48
|
+
|
|
49
|
+
Encapsulates FastMCP and registers tools with access to the Central Facade.
|
|
50
|
+
Each tool is a closure that delegates to the GrafoConcierge instance.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
concierge: Instance of the GrafoConcierge Central Facade.
|
|
54
|
+
janitor: Instance of JanitorService (autonomous maintenance).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
concierge: GrafoConcierge,
|
|
60
|
+
janitor: Optional[JanitorService] = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
self._gc = concierge
|
|
63
|
+
self._janitor = janitor
|
|
64
|
+
|
|
65
|
+
# Read environment variables for host and port (if any)
|
|
66
|
+
host = os.environ.get("GRAFO_HOST", "127.0.0.1")
|
|
67
|
+
try:
|
|
68
|
+
port = int(os.environ.get("GRAFO_PORT", "8000"))
|
|
69
|
+
except ValueError:
|
|
70
|
+
port = 8000
|
|
71
|
+
|
|
72
|
+
# Creates the FastMCP server
|
|
73
|
+
self._mcp = FastMCP("Grafo Concierge", host=host, port=port)
|
|
74
|
+
|
|
75
|
+
# Enables CORS and optional API Key Authentication on Starlette sse_app
|
|
76
|
+
original_sse_app = self._mcp.sse_app
|
|
77
|
+
def custom_sse_app(*args, **kwargs):
|
|
78
|
+
app = original_sse_app(*args, **kwargs)
|
|
79
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
80
|
+
from starlette.responses import JSONResponse
|
|
81
|
+
|
|
82
|
+
api_key = self._gc.config.api_key or os.environ.get("GRAFO_API_KEY")
|
|
83
|
+
cors_origins_env = os.environ.get("GRAFO_CORS_ORIGINS")
|
|
84
|
+
if cors_origins_env:
|
|
85
|
+
origins = [o.strip() for o in cors_origins_env.split(",") if o.strip()]
|
|
86
|
+
else:
|
|
87
|
+
origins = list(self._gc.config.cors_origins)
|
|
88
|
+
|
|
89
|
+
# 1. API Key Authentication Middleware (if configured)
|
|
90
|
+
if api_key:
|
|
91
|
+
@app.middleware("http")
|
|
92
|
+
async def auth_middleware(request, call_next):
|
|
93
|
+
# Check Authorization header (Bearer token) or query param ?token=
|
|
94
|
+
auth_header = request.headers.get("Authorization", "")
|
|
95
|
+
token_param = request.query_params.get("token", "")
|
|
96
|
+
|
|
97
|
+
is_valid = False
|
|
98
|
+
if auth_header.startswith("Bearer "):
|
|
99
|
+
is_valid = (auth_header[7:].strip() == api_key)
|
|
100
|
+
elif token_param:
|
|
101
|
+
is_valid = (token_param.strip() == api_key)
|
|
102
|
+
|
|
103
|
+
if not is_valid:
|
|
104
|
+
return JSONResponse({"error": "Unauthorized access to Grafo Concierge MCP"}, status_code=401)
|
|
105
|
+
return await call_next(request)
|
|
106
|
+
|
|
107
|
+
# 2. CORS Middleware
|
|
108
|
+
allow_creds = True if origins != ["*"] else False
|
|
109
|
+
app.add_middleware(
|
|
110
|
+
CORSMiddleware,
|
|
111
|
+
allow_origins=origins,
|
|
112
|
+
allow_credentials=allow_creds,
|
|
113
|
+
allow_methods=["*"],
|
|
114
|
+
allow_headers=["*"],
|
|
115
|
+
)
|
|
116
|
+
return app
|
|
117
|
+
self._mcp.sse_app = custom_sse_app
|
|
118
|
+
|
|
119
|
+
# Registers the tools
|
|
120
|
+
self._register_tools()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
tool_count = len(self._mcp._tool_manager.list_tools())
|
|
124
|
+
logger.info("GrafoConciergeServer initialized — %d tools registered.", tool_count)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def mcp(self) -> FastMCP:
|
|
128
|
+
"""Direct access to FastMCP (for run/mount)."""
|
|
129
|
+
return self._mcp
|
|
130
|
+
|
|
131
|
+
# ===================================================================
|
|
132
|
+
# TOOL REGISTRATION
|
|
133
|
+
# ===================================================================
|
|
134
|
+
|
|
135
|
+
def _register_tools(self) -> None:
|
|
136
|
+
"""Registers all MCP tools as closures with access to self."""
|
|
137
|
+
|
|
138
|
+
server = self
|
|
139
|
+
|
|
140
|
+
# --- concierge_register ---
|
|
141
|
+
@self._mcp.tool(
|
|
142
|
+
name="concierge_register",
|
|
143
|
+
description=(
|
|
144
|
+
"Registers a new project and defines Privacy Level."
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
def concierge_register(
|
|
148
|
+
project_path: str,
|
|
149
|
+
wing: str = "geral",
|
|
150
|
+
privacy_level: str = "PUBLIC",
|
|
151
|
+
summary: Optional[str] = None,
|
|
152
|
+
) -> dict:
|
|
153
|
+
"""Registers a new project in Grafo Concierge.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
project_path: Path or directory name of the project.
|
|
157
|
+
wing: Main wing (Primary Wing). Default: "geral".
|
|
158
|
+
privacy_level: Privacy level (PUBLIC, INTERNAL, RESTRICTED).
|
|
159
|
+
summary: Optional description.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary with the generated UUID and status.
|
|
163
|
+
"""
|
|
164
|
+
return server._handle_register(project_path, wing, privacy_level, summary)
|
|
165
|
+
|
|
166
|
+
# --- concierge_list_projects ---
|
|
167
|
+
@self._mcp.tool(
|
|
168
|
+
name="concierge_list_projects",
|
|
169
|
+
description=(
|
|
170
|
+
"Returns the list of all registered projects in Grafo Concierge, "
|
|
171
|
+
"mapping Project Name -> UUID and Update Date."
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
def concierge_list_projects() -> dict:
|
|
175
|
+
"""Returns all Name -> UUID matches of the projects."""
|
|
176
|
+
return server._handle_list_projects()
|
|
177
|
+
|
|
178
|
+
# --- concierge_mine ---
|
|
179
|
+
@self._mcp.tool(
|
|
180
|
+
name="concierge_mine",
|
|
181
|
+
description=(
|
|
182
|
+
"Ingests a project from the filesystem into the Memory Graph. "
|
|
183
|
+
"Crawls the directory, parses files (AST/Semantic), "
|
|
184
|
+
"generates recursive summaries (L0/L1/L2), stores embeddings "
|
|
185
|
+
"and synchronizes SQLite + Qdrant. Returns complete report."
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
def concierge_mine(
|
|
189
|
+
path: str,
|
|
190
|
+
project_identifier: str,
|
|
191
|
+
auto_tag: bool = True,
|
|
192
|
+
) -> dict:
|
|
193
|
+
"""Ingests a project in the Memory Graph.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
path: Absolute path of the directory to ingest.
|
|
197
|
+
project_identifier: Readable name of the project or its UUID.
|
|
198
|
+
auto_tag: If True, extracts tags automatically from files.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary with ingestion report (MCP-compatible).
|
|
202
|
+
"""
|
|
203
|
+
return server._handle_mine(path, project_identifier, auto_tag)
|
|
204
|
+
|
|
205
|
+
# --- concierge_search ---
|
|
206
|
+
@self._mcp.tool(
|
|
207
|
+
name="concierge_search",
|
|
208
|
+
description=(
|
|
209
|
+
"Hybrid search in the Memory Graph combining vector similarity "
|
|
210
|
+
"(cosine), frequency (FTS5/BM25) and graph signals "
|
|
211
|
+
"(recency, centrality). Returns the most relevant chunks "
|
|
212
|
+
"ranked by hybrid score."
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
def concierge_search(
|
|
216
|
+
query: str,
|
|
217
|
+
project_identifier: str = "",
|
|
218
|
+
top_k: int = 10,
|
|
219
|
+
node_type: Optional[str] = None,
|
|
220
|
+
include_references: bool = False,
|
|
221
|
+
all_wings: bool = False,
|
|
222
|
+
) -> dict:
|
|
223
|
+
"""Hybrid search in the Memory Graph.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
query: Natural language query text.
|
|
227
|
+
project_identifier: UUID or name of the project for Strict Scoping.
|
|
228
|
+
Optional when all_wings=True.
|
|
229
|
+
top_k: Maximum number of results (default: 10).
|
|
230
|
+
node_type: Optional filter for node type (FACT, SKILL, etc.).
|
|
231
|
+
include_references: Include Reference Wings in scope.
|
|
232
|
+
all_wings: Search in all wings (ignores Strict Scoping).
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dictionary with ranked results and metadata.
|
|
236
|
+
"""
|
|
237
|
+
return server._handle_search(
|
|
238
|
+
query, project_identifier, top_k, node_type,
|
|
239
|
+
include_references, all_wings,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# --- concierge_commit ---
|
|
243
|
+
@self._mcp.tool(
|
|
244
|
+
name="concierge_commit",
|
|
245
|
+
description=(
|
|
246
|
+
"Registers consolidated changes in the Memory Graph. "
|
|
247
|
+
"Writes to the commit_log table, updates recency of affected "
|
|
248
|
+
"nodes, and audits via Critical Reviewer."
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
def concierge_commit(
|
|
252
|
+
project_uuid: str,
|
|
253
|
+
phase: str,
|
|
254
|
+
technical_changes: str,
|
|
255
|
+
updated_pointers: list[str],
|
|
256
|
+
node_ids: Optional[list[int]] = None,
|
|
257
|
+
) -> dict:
|
|
258
|
+
"""Registers an audited memory commit.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
project_uuid: UUID of the project.
|
|
262
|
+
phase: Current phase (planning, build, done, review).
|
|
263
|
+
technical_changes: Description of the technical changes.
|
|
264
|
+
updated_pointers: List of updated pointers.
|
|
265
|
+
node_ids: IDs of the affected nodes (updates recency).
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Dictionary with commit ID and status.
|
|
269
|
+
"""
|
|
270
|
+
return server._handle_commit(
|
|
271
|
+
project_uuid, phase, technical_changes,
|
|
272
|
+
updated_pointers, node_ids,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# --- concierge_wakeup ---
|
|
276
|
+
@self._mcp.tool(
|
|
277
|
+
name="concierge_wakeup",
|
|
278
|
+
description=(
|
|
279
|
+
"Reactivates agent consciousness for a project. "
|
|
280
|
+
"Returns the Context Compass, Reference Wings, "
|
|
281
|
+
"latest commits and statistics."
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
def concierge_wakeup(project_uuid: str) -> dict:
|
|
285
|
+
"""Agent consciousness reactivation.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
project_uuid: UUID of the project.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dictionary with Compass, Wings, commits and stats.
|
|
292
|
+
"""
|
|
293
|
+
return server._handle_wakeup(project_uuid)
|
|
294
|
+
|
|
295
|
+
# --- concierge_resume ---
|
|
296
|
+
@self._mcp.tool(
|
|
297
|
+
name="concierge_resume",
|
|
298
|
+
description=(
|
|
299
|
+
"Returns the Context Compass (concise summary) "
|
|
300
|
+
"of the project. Ideal for system prompt injection."
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
def concierge_resume(project_uuid: str) -> dict:
|
|
304
|
+
"""Context Compass of the project.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
project_uuid: UUID of the project.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Dictionary with summary and basic statistics.
|
|
311
|
+
"""
|
|
312
|
+
return server._handle_resume(project_uuid)
|
|
313
|
+
|
|
314
|
+
# --- concierge_load ---
|
|
315
|
+
@self._mcp.tool(
|
|
316
|
+
name="concierge_load",
|
|
317
|
+
description=(
|
|
318
|
+
"Loads complete node data on demand (Lazy Load). "
|
|
319
|
+
"Returns content, tags, edges and node metadata."
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
def concierge_load(node_id: int) -> dict:
|
|
323
|
+
"""Loads a complete node on demand.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
node_id: ID of the node to load.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Dictionary with all fields and edges of the node.
|
|
330
|
+
"""
|
|
331
|
+
return server._handle_load(node_id)
|
|
332
|
+
|
|
333
|
+
# --- concierge_status ---
|
|
334
|
+
@self._mcp.tool(
|
|
335
|
+
name="concierge_status",
|
|
336
|
+
description=(
|
|
337
|
+
"Returns health status of Grafo Concierge: "
|
|
338
|
+
"project statistics, Qdrant health, latest Janitor "
|
|
339
|
+
"report and pipeline metrics."
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
def concierge_status(
|
|
343
|
+
project_uuid: Optional[str] = None,
|
|
344
|
+
) -> dict:
|
|
345
|
+
"""System health status.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
project_uuid: UUID of the project (optional). If omitted,
|
|
349
|
+
returns global status.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Dictionary with health metrics and statistics.
|
|
353
|
+
"""
|
|
354
|
+
return server._handle_status(project_uuid)
|
|
355
|
+
|
|
356
|
+
# --- search_symbols ---
|
|
357
|
+
@self._mcp.tool(
|
|
358
|
+
name="search_symbols",
|
|
359
|
+
description=(
|
|
360
|
+
"Performs fast search of symbol signatures (classes/functions) "
|
|
361
|
+
"in the FTS5 index of Grafo Concierge."
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
def search_symbols(
|
|
365
|
+
query: str,
|
|
366
|
+
project_uuid: Optional[str] = None,
|
|
367
|
+
) -> dict:
|
|
368
|
+
"""Searches code symbols by name in the FTS5 index.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
query: Text or symbol name to search.
|
|
372
|
+
project_uuid: UUID of the project (optional).
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Dictionary with list of symbols and details.
|
|
376
|
+
"""
|
|
377
|
+
return server._handle_search_symbols(query, project_uuid)
|
|
378
|
+
|
|
379
|
+
# --- get_implementations ---
|
|
380
|
+
@self._mcp.tool(
|
|
381
|
+
name="get_implementations",
|
|
382
|
+
description=(
|
|
383
|
+
"Returns implementation (AST code block) corresponding to a symbol node."
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
def get_implementations(symbol_id: int) -> dict:
|
|
387
|
+
"""Returns the implementation of a symbol node.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
symbol_id: Numeric ID of the symbol node.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Dictionary with the symbol implementation (code).
|
|
394
|
+
"""
|
|
395
|
+
return server._handle_get_implementations(symbol_id)
|
|
396
|
+
|
|
397
|
+
# --- get_callers ---
|
|
398
|
+
@self._mcp.tool(
|
|
399
|
+
name="get_callers",
|
|
400
|
+
description=(
|
|
401
|
+
"Returns all callers of a symbol node by analyzing the Graph edges."
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
def get_callers(symbol_id: int) -> dict:
|
|
405
|
+
"""Returns callers of a symbol.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
symbol_id: Numeric ID of the symbol node.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Dictionary with callers and relationship details.
|
|
412
|
+
"""
|
|
413
|
+
return server._handle_get_callers(symbol_id)
|
|
414
|
+
|
|
415
|
+
# --- concierge_store_fact ---
|
|
416
|
+
@self._mcp.tool(
|
|
417
|
+
name="concierge_store_fact",
|
|
418
|
+
description=(
|
|
419
|
+
"Writes a semantic fact to the Memory Graph via SemanticExtractor. "
|
|
420
|
+
"Extractor evaluates fact against existing scope and decides: "
|
|
421
|
+
"ADD, UPDATE, DELETE or NOOP (bi-temporal)."
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
def concierge_store_fact(
|
|
425
|
+
scope_type: str,
|
|
426
|
+
scope_id: str,
|
|
427
|
+
fact_statement: str,
|
|
428
|
+
) -> dict:
|
|
429
|
+
"""Writes a semantic fact to the graph.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
scope_type: Scope type ('user', 'session', 'agent', 'org').
|
|
433
|
+
scope_id: Unique identifier of scope.
|
|
434
|
+
fact_statement: Fact/preference text to write.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Dictionary with decisions made by SemanticExtractor.
|
|
438
|
+
"""
|
|
439
|
+
return server._handle_store_fact(scope_type, scope_id, fact_statement)
|
|
440
|
+
|
|
441
|
+
# --- concierge_set_memory ---
|
|
442
|
+
@self._mcp.tool(
|
|
443
|
+
name="concierge_set_memory",
|
|
444
|
+
description=(
|
|
445
|
+
"Writes or updates a persistent core memory block (user_core_memory). "
|
|
446
|
+
"Use to store preferences, settings, and permanent context of the user/session."
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
def concierge_set_memory(
|
|
450
|
+
scope_type: str,
|
|
451
|
+
scope_id: str,
|
|
452
|
+
block_label: str,
|
|
453
|
+
content: str,
|
|
454
|
+
) -> dict:
|
|
455
|
+
"""Writes a persistent core memory block.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
scope_type: Scope type ('user', 'session', 'agent', 'org').
|
|
459
|
+
scope_id: Unique identifier of scope.
|
|
460
|
+
block_label: Block label (e.g. 'preferred_language', 'persona_name').
|
|
461
|
+
content: Content to store in the block.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Dictionary with success and memory_id of the record.
|
|
465
|
+
"""
|
|
466
|
+
return server._handle_set_memory(scope_type, scope_id, block_label, content)
|
|
467
|
+
|
|
468
|
+
# --- concierge_get_memory ---
|
|
469
|
+
@self._mcp.tool(
|
|
470
|
+
name="concierge_get_memory",
|
|
471
|
+
description=(
|
|
472
|
+
"Queries persistent core memory blocks. "
|
|
473
|
+
"If block_label is provided, returns only that block. "
|
|
474
|
+
"If omitted, returns all blocks of the scope."
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
def concierge_get_memory(
|
|
478
|
+
scope_type: str,
|
|
479
|
+
scope_id: str,
|
|
480
|
+
block_label: Optional[str] = None,
|
|
481
|
+
) -> dict:
|
|
482
|
+
"""Queries core memory blocks.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
scope_type: Scope type ('user', 'session', 'agent', 'org').
|
|
486
|
+
scope_id: Unique identifier of scope.
|
|
487
|
+
block_label: Specific label (optional). If absent, returns all.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Dictionary with success and list of blocks.
|
|
491
|
+
"""
|
|
492
|
+
return server._handle_get_memory(scope_type, scope_id, block_label)
|
|
493
|
+
|
|
494
|
+
# --- concierge_feedback ---
|
|
495
|
+
@self._mcp.tool(
|
|
496
|
+
name="concierge_feedback",
|
|
497
|
+
description=(
|
|
498
|
+
"Registers utility feedback on a semantic fact (semantic_fact). "
|
|
499
|
+
"Triggers Bayesian learning: increments utility_alpha (success) or "
|
|
500
|
+
"utility_beta (failure), feeding Thompson Sampling of hybrid search."
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
def concierge_feedback(
|
|
504
|
+
fact_id: int,
|
|
505
|
+
was_useful: bool,
|
|
506
|
+
) -> dict:
|
|
507
|
+
"""Registers utility feedback of a semantic fact.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
fact_id: ID of the semantic_fact to evaluate (id field returned by concierge_store_fact).
|
|
511
|
+
was_useful: True if the fact was useful in the response, False otherwise.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Dictionary with success, fact_id, was_useful and message.
|
|
515
|
+
"""
|
|
516
|
+
return server._handle_feedback(fact_id, was_useful)
|
|
517
|
+
|
|
518
|
+
# --- get_full_topology ---
|
|
519
|
+
@self._mcp.tool(
|
|
520
|
+
name="get_full_topology",
|
|
521
|
+
description=(
|
|
522
|
+
"Returns the complete topology of nodes and edges (call graph, "
|
|
523
|
+
"symbols, files and dependencies) in an ultra-lean way. "
|
|
524
|
+
"Used by the Web Dashboard for real-time 3D visualizations. "
|
|
525
|
+
"Does not include text summaries or embeddings."
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
def get_full_topology(
|
|
529
|
+
project_identifier: Optional[str] = None,
|
|
530
|
+
) -> dict:
|
|
531
|
+
"""Returns the complete topology of nodes and edges in the database.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
project_identifier: Optional project UUID or name to filter the data.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Dictionary containing success, list of nodes and list of edges.
|
|
538
|
+
"""
|
|
539
|
+
return server._handle_get_full_topology(project_identifier)
|
|
540
|
+
|
|
541
|
+
# --- delete_project ---
|
|
542
|
+
@self._mcp.tool(
|
|
543
|
+
name="delete_project",
|
|
544
|
+
description=(
|
|
545
|
+
"Physically removes a project and all linked records, "
|
|
546
|
+
"including nodes, edges, commits, trajectories and associated embeddings."
|
|
547
|
+
),
|
|
548
|
+
)
|
|
549
|
+
def delete_project(project_identifier: str) -> dict:
|
|
550
|
+
"""Removes a project and all its cascading data.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
project_identifier: UUID or directory name of the project.
|
|
554
|
+
"""
|
|
555
|
+
return server._handle_delete_project(project_identifier)
|
|
556
|
+
|
|
557
|
+
# --- update_project ---
|
|
558
|
+
@self._mcp.tool(
|
|
559
|
+
name="update_project",
|
|
560
|
+
description=(
|
|
561
|
+
"Updates permitted cadastral fields of a project "
|
|
562
|
+
"(folder_name, primary_wing, privacy_level, summary)."
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
def update_project(
|
|
566
|
+
project_identifier: str,
|
|
567
|
+
folder_name: Optional[str] = None,
|
|
568
|
+
primary_wing: Optional[str] = None,
|
|
569
|
+
privacy_level: Optional[str] = None,
|
|
570
|
+
summary: Optional[str] = None,
|
|
571
|
+
) -> dict:
|
|
572
|
+
"""Updates a project registration.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
project_identifier: UUID or directory name of the project.
|
|
576
|
+
folder_name: New directory name (optional).
|
|
577
|
+
primary_wing: New primary wing of the project (optional).
|
|
578
|
+
privacy_level: Privacy level (PUBLIC, INTERNAL, RESTRICTED) (optional).
|
|
579
|
+
summary: New descriptive summary of the project (optional).
|
|
580
|
+
"""
|
|
581
|
+
return server._handle_update_project(
|
|
582
|
+
project_identifier, folder_name, primary_wing, privacy_level, summary
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# --- add_reference_wing ---
|
|
586
|
+
@self._mcp.tool(
|
|
587
|
+
name="add_reference_wing",
|
|
588
|
+
description="Associates a recommended reference wing (Reference Wing) with a project.",
|
|
589
|
+
)
|
|
590
|
+
def add_reference_wing(project_identifier: str, wing_name: str) -> dict:
|
|
591
|
+
"""Associates a recommended wing with the project.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
project_identifier: UUID or directory name of the project.
|
|
595
|
+
wing_name: Name of the wing to associate.
|
|
596
|
+
"""
|
|
597
|
+
return server._handle_add_reference_wing(project_identifier, wing_name)
|
|
598
|
+
|
|
599
|
+
# --- remove_reference_wing ---
|
|
600
|
+
@self._mcp.tool(
|
|
601
|
+
name="remove_reference_wing",
|
|
602
|
+
description="Removes an associated reference wing (Reference Wing) from a project.",
|
|
603
|
+
)
|
|
604
|
+
def remove_reference_wing(project_identifier: str, wing_name: str) -> dict:
|
|
605
|
+
"""Removes an associated wing from the project.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
project_identifier: UUID or directory name of the project.
|
|
609
|
+
wing_name: Name of the wing to remove.
|
|
610
|
+
"""
|
|
611
|
+
return server._handle_remove_reference_wing(project_identifier, wing_name)
|
|
612
|
+
|
|
613
|
+
# --- find_similar ---
|
|
614
|
+
@self._mcp.tool(
|
|
615
|
+
name="find_similar",
|
|
616
|
+
description="Searches other registered projects that share the same domain of technical expertise.",
|
|
617
|
+
)
|
|
618
|
+
def find_similar(
|
|
619
|
+
project_identifier: str,
|
|
620
|
+
limit: int = 5,
|
|
621
|
+
include_references: bool = False,
|
|
622
|
+
all_wings: bool = False,
|
|
623
|
+
) -> dict:
|
|
624
|
+
"""Searches similar projects in the same technical wing.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
project_identifier: UUID or directory name of the anchor project.
|
|
628
|
+
limit: Maximum limit of projects returned (default: 5).
|
|
629
|
+
include_references: If True, includes reference wings in search.
|
|
630
|
+
all_wings: If True, searches in all wings indistinctly.
|
|
631
|
+
"""
|
|
632
|
+
return server._handle_find_similar(project_identifier, limit, include_references, all_wings)
|
|
633
|
+
|
|
634
|
+
# --- get_trajectories ---
|
|
635
|
+
@self._mcp.tool(
|
|
636
|
+
name="get_trajectories",
|
|
637
|
+
description="Retrieves the detailed history of cognitive trajectories and previous navigation steps of the project.",
|
|
638
|
+
)
|
|
639
|
+
def get_trajectories(project_identifier: str) -> dict:
|
|
640
|
+
"""Retrieves history of cognitive trajectories.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
project_identifier: UUID or directory name of the project.
|
|
644
|
+
"""
|
|
645
|
+
return server._handle_get_trajectories(project_identifier)
|
|
646
|
+
|
|
647
|
+
# --- count_embeddings ---
|
|
648
|
+
@self._mcp.tool(
|
|
649
|
+
name="count_embeddings",
|
|
650
|
+
description="Returns the exact count of vectors (embeddings) stored in the vector collection.",
|
|
651
|
+
)
|
|
652
|
+
def count_embeddings(project_identifier: Optional[str] = None) -> dict:
|
|
653
|
+
"""Counts collection vectors.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
project_identifier: If provided, filters and counts only vectors of this project.
|
|
657
|
+
"""
|
|
658
|
+
return server._handle_count_embeddings(project_identifier)
|
|
659
|
+
|
|
660
|
+
# --- reset_collection ---
|
|
661
|
+
@self._mcp.tool(
|
|
662
|
+
name="reset_collection",
|
|
663
|
+
description=(
|
|
664
|
+
"Destroys and recreates the physical vector collection (emergency repair). "
|
|
665
|
+
"WARNING: Irreversible operation that deletes ALL vectors!"
|
|
666
|
+
),
|
|
667
|
+
)
|
|
668
|
+
def reset_collection() -> dict:
|
|
669
|
+
"""Destroys and recreates the collection of vectors."""
|
|
670
|
+
return server._handle_reset_collection()
|
|
671
|
+
|
|
672
|
+
def _resolve_project_identifier(self, project_identifier: str) -> str:
|
|
673
|
+
"""Resolves project_identifier (UUID or folder_name) to project_uuid.
|
|
674
|
+
|
|
675
|
+
Raises ValueError if the name is not found in the database.
|
|
676
|
+
"""
|
|
677
|
+
import uuid
|
|
678
|
+
try:
|
|
679
|
+
uuid.UUID(project_identifier)
|
|
680
|
+
return project_identifier
|
|
681
|
+
except ValueError:
|
|
682
|
+
pass
|
|
683
|
+
|
|
684
|
+
try:
|
|
685
|
+
project = self._gc.store.get_project(project_identifier)
|
|
686
|
+
return project["uuid"]
|
|
687
|
+
except Exception:
|
|
688
|
+
raise ValueError(
|
|
689
|
+
f"Project '{project_identifier}' not found. "
|
|
690
|
+
"Please list available projects using concierge_list_projects."
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# ===================================================================
|
|
694
|
+
# HANDLER: concierge_list_projects
|
|
695
|
+
# ===================================================================
|
|
696
|
+
|
|
697
|
+
def _handle_list_projects(self) -> dict:
|
|
698
|
+
"""Handler for concierge_list_projects."""
|
|
699
|
+
t0 = time.perf_counter()
|
|
700
|
+
try:
|
|
701
|
+
projects = self._gc.store.list_projects()
|
|
702
|
+
formatted = {}
|
|
703
|
+
for p in projects:
|
|
704
|
+
name = p["folder_name"]
|
|
705
|
+
updated = p["updated_at"][:10] if p["updated_at"] else ""
|
|
706
|
+
formatted[name] = {"uuid": p["uuid"], "updated_at": updated}
|
|
707
|
+
|
|
708
|
+
elapsed = time.perf_counter() - t0
|
|
709
|
+
return {
|
|
710
|
+
"success": True,
|
|
711
|
+
"projects": formatted,
|
|
712
|
+
"duration_seconds": round(elapsed, 3),
|
|
713
|
+
}
|
|
714
|
+
except Exception as e:
|
|
715
|
+
elapsed = time.perf_counter() - t0
|
|
716
|
+
logger.error("concierge_list_projects FAILED: %s", e)
|
|
717
|
+
return {
|
|
718
|
+
"success": False,
|
|
719
|
+
"error": str(e),
|
|
720
|
+
"projects": {},
|
|
721
|
+
"duration_seconds": round(elapsed, 3),
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# ===================================================================
|
|
725
|
+
# HANDLER: concierge_register
|
|
726
|
+
# ===================================================================
|
|
727
|
+
|
|
728
|
+
def _handle_register(
|
|
729
|
+
self, project_path: str, wing: str, privacy_level: str, summary: Optional[str]
|
|
730
|
+
) -> dict:
|
|
731
|
+
"""Handler for concierge_register — delegates to Facade."""
|
|
732
|
+
t0 = time.perf_counter()
|
|
733
|
+
|
|
734
|
+
try:
|
|
735
|
+
folder_name = os.path.basename(project_path.strip(r"\/")) or project_path
|
|
736
|
+
|
|
737
|
+
project_uuid = self._gc.register_project(
|
|
738
|
+
folder_name=folder_name,
|
|
739
|
+
wing=wing,
|
|
740
|
+
privacy_level=privacy_level,
|
|
741
|
+
summary=summary or f"Project registered via MCP: {folder_name}",
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
elapsed = time.perf_counter() - t0
|
|
745
|
+
logger.info(
|
|
746
|
+
"concierge_register OK: %s → %s (wing=%s, privacy=%s), %.3fs",
|
|
747
|
+
folder_name, project_uuid, wing, privacy_level, elapsed,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
"success": True,
|
|
752
|
+
"project_uuid": project_uuid,
|
|
753
|
+
"folder_name": folder_name,
|
|
754
|
+
"wing": wing,
|
|
755
|
+
"privacy_level": privacy_level,
|
|
756
|
+
"duration_seconds": round(elapsed, 3),
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
except Exception as e:
|
|
760
|
+
elapsed = time.perf_counter() - t0
|
|
761
|
+
logger.error("concierge_register FAILED: %s", e)
|
|
762
|
+
return {
|
|
763
|
+
"success": False,
|
|
764
|
+
"error": str(e),
|
|
765
|
+
"duration_seconds": round(elapsed, 3),
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
# ===================================================================
|
|
769
|
+
# HANDLER: concierge_mine
|
|
770
|
+
# ===================================================================
|
|
771
|
+
|
|
772
|
+
def _handle_mine(
|
|
773
|
+
self, path: str, project_identifier: str, auto_tag: bool,
|
|
774
|
+
) -> dict:
|
|
775
|
+
"""Handler for concierge_mine — delegates to Facade."""
|
|
776
|
+
t0 = time.perf_counter()
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
import uuid
|
|
780
|
+
is_uuid = False
|
|
781
|
+
try:
|
|
782
|
+
uuid.UUID(project_identifier)
|
|
783
|
+
is_uuid = True
|
|
784
|
+
except ValueError:
|
|
785
|
+
pass
|
|
786
|
+
|
|
787
|
+
if is_uuid:
|
|
788
|
+
project_uuid = project_identifier
|
|
789
|
+
try:
|
|
790
|
+
project = self._gc.store.get_project(project_uuid)
|
|
791
|
+
project_name = project["folder_name"]
|
|
792
|
+
except Exception:
|
|
793
|
+
project_name = os.path.basename(path.rstrip(r"\/")) or project_uuid
|
|
794
|
+
# Registers if it does not exist
|
|
795
|
+
self._gc.register_project(
|
|
796
|
+
folder_name=project_name,
|
|
797
|
+
summary=f"Project ingested from: {path}",
|
|
798
|
+
)
|
|
799
|
+
else:
|
|
800
|
+
try:
|
|
801
|
+
project = self._gc.store.get_project(project_identifier)
|
|
802
|
+
project_uuid = project["uuid"]
|
|
803
|
+
project_name = project["folder_name"]
|
|
804
|
+
except Exception:
|
|
805
|
+
raise ValueError(
|
|
806
|
+
f"Project '{project_identifier}' not found. "
|
|
807
|
+
"Please list available projects using concierge_list_projects."
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Signals Idle-Lock for the Janitor
|
|
811
|
+
if self._janitor:
|
|
812
|
+
self._janitor.signal_mine_start()
|
|
813
|
+
|
|
814
|
+
try:
|
|
815
|
+
result = self._gc.mine(project_uuid, path, auto_tag=auto_tag)
|
|
816
|
+
finally:
|
|
817
|
+
if self._janitor:
|
|
818
|
+
self._janitor.signal_mine_end()
|
|
819
|
+
|
|
820
|
+
elapsed = time.perf_counter() - t0
|
|
821
|
+
result["project_uuid"] = project_uuid
|
|
822
|
+
result["project_name"] = project_name
|
|
823
|
+
result["path"] = path
|
|
824
|
+
result["duration_seconds"] = round(elapsed, 3)
|
|
825
|
+
result["success"] = True
|
|
826
|
+
|
|
827
|
+
logger.info(
|
|
828
|
+
"concierge_mine OK: %s → %d files, %d nodes, %.2fs",
|
|
829
|
+
project_name, result.get("files_processed", 0),
|
|
830
|
+
result.get("nodes_created", 0), elapsed,
|
|
831
|
+
)
|
|
832
|
+
return result
|
|
833
|
+
|
|
834
|
+
except Exception as e:
|
|
835
|
+
elapsed = time.perf_counter() - t0
|
|
836
|
+
logger.error("concierge_mine FAILED: %s — %s", project_identifier, e)
|
|
837
|
+
return {
|
|
838
|
+
"success": False,
|
|
839
|
+
"error": str(e),
|
|
840
|
+
"traceback": traceback.format_exc(),
|
|
841
|
+
"project_name": project_identifier,
|
|
842
|
+
"path": path,
|
|
843
|
+
"duration_seconds": round(elapsed, 3),
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
# ===================================================================
|
|
847
|
+
# HANDLER: concierge_search
|
|
848
|
+
# ===================================================================
|
|
849
|
+
|
|
850
|
+
def _handle_search(
|
|
851
|
+
self,
|
|
852
|
+
query: str,
|
|
853
|
+
project_identifier: str,
|
|
854
|
+
top_k: int,
|
|
855
|
+
node_type: Optional[str],
|
|
856
|
+
include_references: bool,
|
|
857
|
+
all_wings: bool,
|
|
858
|
+
) -> dict:
|
|
859
|
+
"""Handler for concierge_search — delegates to Facade."""
|
|
860
|
+
t0 = time.perf_counter()
|
|
861
|
+
|
|
862
|
+
logger.info(
|
|
863
|
+
"[concierge_search] query='%.60s' project_identifier=%r "
|
|
864
|
+
"top_k=%d all_wings=%s",
|
|
865
|
+
query, project_identifier, top_k, all_wings,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
try:
|
|
869
|
+
# When all_wings=True and no project_identifier is given, skip
|
|
870
|
+
# UUID resolution — the search spans every wing anyway.
|
|
871
|
+
if all_wings and not project_identifier:
|
|
872
|
+
project_uuid = ""
|
|
873
|
+
else:
|
|
874
|
+
# Transparent resolution of project_identifier
|
|
875
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
876
|
+
|
|
877
|
+
results = self._gc.hybrid_search(
|
|
878
|
+
query=query,
|
|
879
|
+
project_uuid=project_uuid,
|
|
880
|
+
top_k=top_k,
|
|
881
|
+
include_references=include_references,
|
|
882
|
+
all_wings=all_wings,
|
|
883
|
+
node_type=node_type,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Enriches with node data for MCP response
|
|
887
|
+
enriched = []
|
|
888
|
+
for item in results:
|
|
889
|
+
try:
|
|
890
|
+
node = self._gc.store.get_node(item["node_id"])
|
|
891
|
+
breakdown = item.get("score_breakdown", {})
|
|
892
|
+
enriched.append({
|
|
893
|
+
"node_id": item["node_id"],
|
|
894
|
+
"label": node.get("label", ""),
|
|
895
|
+
"summary": node.get("summary", ""),
|
|
896
|
+
"node_type": node.get("node_type", ""),
|
|
897
|
+
"tags": node.get("tags", []),
|
|
898
|
+
"hybrid_score": round(item.get("score_final", 0), 4),
|
|
899
|
+
"vector_score": round(breakdown.get("vetorial", 0), 4),
|
|
900
|
+
"fts_score": round(breakdown.get("frequencia", 0), 4),
|
|
901
|
+
"recency_score": round(breakdown.get("recencia", 0), 4),
|
|
902
|
+
"centrality_score": round(breakdown.get("centralidade", 0), 4),
|
|
903
|
+
"is_super_node": item.get("is_super_node", False),
|
|
904
|
+
})
|
|
905
|
+
except Exception:
|
|
906
|
+
logger.debug("Node %d not found in enrichment.", item.get("node_id"))
|
|
907
|
+
|
|
908
|
+
elapsed = time.perf_counter() - t0
|
|
909
|
+
|
|
910
|
+
logger.info(
|
|
911
|
+
"concierge_search OK: query='%.40s' → %d results, %.3fs",
|
|
912
|
+
query, len(enriched), elapsed,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
return {
|
|
916
|
+
"success": True,
|
|
917
|
+
"query": query,
|
|
918
|
+
"project_uuid": project_uuid,
|
|
919
|
+
"results_count": len(enriched),
|
|
920
|
+
"results": enriched,
|
|
921
|
+
"duration_seconds": round(elapsed, 3),
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
except Exception as e:
|
|
925
|
+
elapsed = time.perf_counter() - t0
|
|
926
|
+
logger.error("concierge_search FAILED: %s", e)
|
|
927
|
+
return {
|
|
928
|
+
"success": False,
|
|
929
|
+
"error": str(e),
|
|
930
|
+
"query": query,
|
|
931
|
+
"project_uuid": project_identifier,
|
|
932
|
+
"results": [],
|
|
933
|
+
"duration_seconds": round(elapsed, 3),
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
# ===================================================================
|
|
937
|
+
# HANDLER: concierge_commit
|
|
938
|
+
# ===================================================================
|
|
939
|
+
|
|
940
|
+
def _handle_commit(
|
|
941
|
+
self,
|
|
942
|
+
project_uuid: str,
|
|
943
|
+
phase: str,
|
|
944
|
+
technical_changes: str,
|
|
945
|
+
updated_pointers: list[str],
|
|
946
|
+
node_ids: Optional[list[int]],
|
|
947
|
+
) -> dict:
|
|
948
|
+
"""Handler for concierge_commit — delegates to Facade."""
|
|
949
|
+
t0 = time.perf_counter()
|
|
950
|
+
|
|
951
|
+
try:
|
|
952
|
+
commit_id = self._gc.commit_memory(
|
|
953
|
+
project_uuid=project_uuid,
|
|
954
|
+
phase=phase,
|
|
955
|
+
technical_changes=technical_changes,
|
|
956
|
+
updated_pointers=updated_pointers,
|
|
957
|
+
node_ids=node_ids,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
elapsed = time.perf_counter() - t0
|
|
961
|
+
|
|
962
|
+
logger.info(
|
|
963
|
+
"concierge_commit OK: id=%d, project=%s, phase='%s', %.3fs",
|
|
964
|
+
commit_id, project_uuid, phase, elapsed,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
return {
|
|
968
|
+
"success": True,
|
|
969
|
+
"commit_id": commit_id,
|
|
970
|
+
"project_uuid": project_uuid,
|
|
971
|
+
"phase": phase,
|
|
972
|
+
"duration_seconds": round(elapsed, 3),
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
except Exception as e:
|
|
976
|
+
elapsed = time.perf_counter() - t0
|
|
977
|
+
logger.error("concierge_commit FAILED: %s", e)
|
|
978
|
+
return {
|
|
979
|
+
"success": False,
|
|
980
|
+
"error": str(e),
|
|
981
|
+
"project_uuid": project_uuid,
|
|
982
|
+
"duration_seconds": round(elapsed, 3),
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
# ===================================================================
|
|
986
|
+
# HANDLER: concierge_wakeup
|
|
987
|
+
# ===================================================================
|
|
988
|
+
|
|
989
|
+
def _handle_wakeup(self, project_uuid: str) -> dict:
|
|
990
|
+
"""Handler for concierge_wakeup — delegates to Facade."""
|
|
991
|
+
t0 = time.perf_counter()
|
|
992
|
+
|
|
993
|
+
try:
|
|
994
|
+
result = self._gc.wake_up(project_uuid)
|
|
995
|
+
elapsed = time.perf_counter() - t0
|
|
996
|
+
|
|
997
|
+
result["success"] = True
|
|
998
|
+
result["duration_seconds"] = round(elapsed, 3)
|
|
999
|
+
|
|
1000
|
+
logger.info(
|
|
1001
|
+
"concierge_wakeup OK: project=%s, %.3fs", project_uuid, elapsed,
|
|
1002
|
+
)
|
|
1003
|
+
return result
|
|
1004
|
+
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
elapsed = time.perf_counter() - t0
|
|
1007
|
+
logger.error("concierge_wakeup FAILED: %s", e)
|
|
1008
|
+
return {
|
|
1009
|
+
"success": False,
|
|
1010
|
+
"error": str(e),
|
|
1011
|
+
"project_uuid": project_uuid,
|
|
1012
|
+
"duration_seconds": round(elapsed, 3),
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
# ===================================================================
|
|
1016
|
+
# HANDLER: concierge_resume
|
|
1017
|
+
# ===================================================================
|
|
1018
|
+
|
|
1019
|
+
def _handle_resume(self, project_uuid: str) -> dict:
|
|
1020
|
+
"""Handler for concierge_resume — delegates to Facade."""
|
|
1021
|
+
t0 = time.perf_counter()
|
|
1022
|
+
|
|
1023
|
+
try:
|
|
1024
|
+
resume = self._gc.get_resume(project_uuid)
|
|
1025
|
+
project = self._gc.store.get_project(project_uuid)
|
|
1026
|
+
stats = self._gc.store.get_project_stats(project_uuid)
|
|
1027
|
+
elapsed = time.perf_counter() - t0
|
|
1028
|
+
|
|
1029
|
+
logger.info(
|
|
1030
|
+
"concierge_resume OK: project=%s, %.3fs", project_uuid, elapsed,
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
return {
|
|
1034
|
+
"success": True,
|
|
1035
|
+
"project_uuid": project_uuid,
|
|
1036
|
+
"folder_name": project.get("folder_name", ""),
|
|
1037
|
+
"primary_wing": project.get("primary_wing", "geral"),
|
|
1038
|
+
"resume": resume,
|
|
1039
|
+
"stats": stats,
|
|
1040
|
+
"duration_seconds": round(elapsed, 3),
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
except Exception as e:
|
|
1044
|
+
elapsed = time.perf_counter() - t0
|
|
1045
|
+
logger.error("concierge_resume FAILED: %s", e)
|
|
1046
|
+
return {
|
|
1047
|
+
"success": False,
|
|
1048
|
+
"error": str(e),
|
|
1049
|
+
"project_uuid": project_uuid,
|
|
1050
|
+
"duration_seconds": round(elapsed, 3),
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
# ===================================================================
|
|
1054
|
+
# HANDLER: concierge_load
|
|
1055
|
+
# ===================================================================
|
|
1056
|
+
|
|
1057
|
+
def _handle_load(self, node_id: int) -> dict:
|
|
1058
|
+
"""Handler for concierge_load — delegates to Facade."""
|
|
1059
|
+
t0 = time.perf_counter()
|
|
1060
|
+
|
|
1061
|
+
try:
|
|
1062
|
+
result = self._gc.lazy_load(node_id)
|
|
1063
|
+
elapsed = time.perf_counter() - t0
|
|
1064
|
+
|
|
1065
|
+
logger.info(
|
|
1066
|
+
"concierge_load OK: node_id=%d, %.3fs", node_id, elapsed,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
"success": True,
|
|
1071
|
+
"node": result,
|
|
1072
|
+
"duration_seconds": round(elapsed, 3),
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
except Exception as e:
|
|
1076
|
+
elapsed = time.perf_counter() - t0
|
|
1077
|
+
logger.error("concierge_load FAILED: %s", e)
|
|
1078
|
+
return {
|
|
1079
|
+
"success": False,
|
|
1080
|
+
"error": str(e),
|
|
1081
|
+
"node_id": node_id,
|
|
1082
|
+
"duration_seconds": round(elapsed, 3),
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
# ===================================================================
|
|
1086
|
+
# HANDLER: concierge_status
|
|
1087
|
+
# ===================================================================
|
|
1088
|
+
|
|
1089
|
+
def _handle_status(self, project_uuid: Optional[str]) -> dict:
|
|
1090
|
+
"""Handler for concierge_status — delegates to Facade + components."""
|
|
1091
|
+
t0 = time.perf_counter()
|
|
1092
|
+
|
|
1093
|
+
try:
|
|
1094
|
+
status: dict = {
|
|
1095
|
+
"success": True,
|
|
1096
|
+
"system": "Grafo Concierge v3.8.0",
|
|
1097
|
+
"components": {},
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
# --- SQLite Health ---
|
|
1101
|
+
try:
|
|
1102
|
+
projects = self._gc.store.list_projects()
|
|
1103
|
+
status["components"]["sqlite"] = {
|
|
1104
|
+
"status": "healthy",
|
|
1105
|
+
"total_projects": len(projects),
|
|
1106
|
+
}
|
|
1107
|
+
except Exception as e:
|
|
1108
|
+
status["components"]["sqlite"] = {
|
|
1109
|
+
"status": "degraded",
|
|
1110
|
+
"error": str(e),
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
# --- Janitor ---
|
|
1114
|
+
if self._janitor:
|
|
1115
|
+
last = self._janitor.last_reports
|
|
1116
|
+
janitor_status = {
|
|
1117
|
+
"status": "active" if self._janitor.is_running else "idle",
|
|
1118
|
+
"total_runs": len(last),
|
|
1119
|
+
}
|
|
1120
|
+
if last:
|
|
1121
|
+
janitor_status["last_report"] = last[-1].to_dict()
|
|
1122
|
+
status["components"]["janitor"] = janitor_status
|
|
1123
|
+
else:
|
|
1124
|
+
status["components"]["janitor"] = {"status": "not_configured"}
|
|
1125
|
+
|
|
1126
|
+
# --- Project Stats (if UUID provided) ---
|
|
1127
|
+
if project_uuid:
|
|
1128
|
+
try:
|
|
1129
|
+
project_status = self._gc.status(project_uuid)
|
|
1130
|
+
status["project"] = project_status
|
|
1131
|
+
except Exception as e:
|
|
1132
|
+
status["project"] = {
|
|
1133
|
+
"uuid": project_uuid,
|
|
1134
|
+
"error": str(e),
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
elapsed = time.perf_counter() - t0
|
|
1138
|
+
status["duration_seconds"] = round(elapsed, 3)
|
|
1139
|
+
|
|
1140
|
+
logger.info("concierge_status OK in %.3fs", elapsed)
|
|
1141
|
+
return status
|
|
1142
|
+
|
|
1143
|
+
except Exception as e:
|
|
1144
|
+
elapsed = time.perf_counter() - t0
|
|
1145
|
+
logger.error("concierge_status FAILED: %s", e)
|
|
1146
|
+
return {
|
|
1147
|
+
"success": False,
|
|
1148
|
+
"error": str(e),
|
|
1149
|
+
"duration_seconds": round(elapsed, 3),
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
# ===================================================================
|
|
1153
|
+
# HANDLER: search_symbols
|
|
1154
|
+
# ===================================================================
|
|
1155
|
+
|
|
1156
|
+
def _handle_search_symbols(self, query: str, project_uuid: Optional[str] = None) -> dict:
|
|
1157
|
+
"""Handler for search_symbols."""
|
|
1158
|
+
t0 = time.perf_counter()
|
|
1159
|
+
try:
|
|
1160
|
+
results = self._gc.store.fts_search(query, project_uuid=project_uuid)
|
|
1161
|
+
formatted = []
|
|
1162
|
+
for r in results:
|
|
1163
|
+
formatted.append({
|
|
1164
|
+
"id": r["id"],
|
|
1165
|
+
"label": r["label"],
|
|
1166
|
+
"node_type": r["node_type"],
|
|
1167
|
+
"file_path": r.get("label", ""),
|
|
1168
|
+
"summary": r.get("summary", ""),
|
|
1169
|
+
})
|
|
1170
|
+
elapsed = time.perf_counter() - t0
|
|
1171
|
+
return {
|
|
1172
|
+
"success": True,
|
|
1173
|
+
"symbols": formatted,
|
|
1174
|
+
"duration_seconds": round(elapsed, 3),
|
|
1175
|
+
}
|
|
1176
|
+
except Exception as e:
|
|
1177
|
+
elapsed = time.perf_counter() - t0
|
|
1178
|
+
logger.error("search_symbols FAILED: %s", e)
|
|
1179
|
+
return {
|
|
1180
|
+
"success": False,
|
|
1181
|
+
"error": str(e),
|
|
1182
|
+
"duration_seconds": round(elapsed, 3),
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
# ===================================================================
|
|
1186
|
+
# HANDLER: get_implementations
|
|
1187
|
+
# ===================================================================
|
|
1188
|
+
|
|
1189
|
+
def _handle_get_implementations(self, symbol_id: int) -> dict:
|
|
1190
|
+
"""Handler for get_implementations — delegates to Central Facade."""
|
|
1191
|
+
t0 = time.perf_counter()
|
|
1192
|
+
try:
|
|
1193
|
+
impl = self._gc.get_implementations(symbol_id)
|
|
1194
|
+
elapsed = time.perf_counter() - t0
|
|
1195
|
+
return {
|
|
1196
|
+
"success": True,
|
|
1197
|
+
"symbol_id": symbol_id,
|
|
1198
|
+
"label": impl.get("label", ""),
|
|
1199
|
+
"type": impl.get("type", ""),
|
|
1200
|
+
"implementation": impl.get("content", ""),
|
|
1201
|
+
"project_uuid": impl.get("project_uuid", ""),
|
|
1202
|
+
"duration_seconds": round(elapsed, 3),
|
|
1203
|
+
}
|
|
1204
|
+
except Exception as e:
|
|
1205
|
+
elapsed = time.perf_counter() - t0
|
|
1206
|
+
logger.error("get_implementations FAILED: %s", e)
|
|
1207
|
+
return {
|
|
1208
|
+
"success": False,
|
|
1209
|
+
"error": str(e),
|
|
1210
|
+
"duration_seconds": round(elapsed, 3),
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
# ===================================================================
|
|
1214
|
+
# HANDLER: get_callers
|
|
1215
|
+
# ===================================================================
|
|
1216
|
+
|
|
1217
|
+
def _handle_get_callers(self, symbol_id: int) -> dict:
|
|
1218
|
+
"""Handler for get_callers."""
|
|
1219
|
+
t0 = time.perf_counter()
|
|
1220
|
+
try:
|
|
1221
|
+
edges = self._gc.store.get_edges_to(symbol_id)
|
|
1222
|
+
callers = []
|
|
1223
|
+
for edge in edges:
|
|
1224
|
+
try:
|
|
1225
|
+
source_node = self._gc.store.get_node(edge["source_id"])
|
|
1226
|
+
callers.append({
|
|
1227
|
+
"id": source_node["id"],
|
|
1228
|
+
"label": source_node["label"],
|
|
1229
|
+
"node_type": source_node["node_type"],
|
|
1230
|
+
"relation_type": edge["relation_type"],
|
|
1231
|
+
})
|
|
1232
|
+
except Exception:
|
|
1233
|
+
pass
|
|
1234
|
+
elapsed = time.perf_counter() - t0
|
|
1235
|
+
return {
|
|
1236
|
+
"success": True,
|
|
1237
|
+
"symbol_id": symbol_id,
|
|
1238
|
+
"callers": callers,
|
|
1239
|
+
"duration_seconds": round(elapsed, 3),
|
|
1240
|
+
}
|
|
1241
|
+
except Exception as e:
|
|
1242
|
+
elapsed = time.perf_counter() - t0
|
|
1243
|
+
logger.error("get_callers FAILED: %s", e)
|
|
1244
|
+
return {
|
|
1245
|
+
"success": False,
|
|
1246
|
+
"error": str(e),
|
|
1247
|
+
"duration_seconds": round(elapsed, 3),
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
# ===================================================================
|
|
1251
|
+
# HANDLER: concierge_store_fact
|
|
1252
|
+
# ===================================================================
|
|
1253
|
+
|
|
1254
|
+
def _handle_store_fact(
|
|
1255
|
+
self, scope_type: str, scope_id: str, fact_statement: str,
|
|
1256
|
+
) -> dict:
|
|
1257
|
+
"""Handler for concierge_store_fact — delegates to Facade with fail-fast validation."""
|
|
1258
|
+
t0 = time.perf_counter()
|
|
1259
|
+
try:
|
|
1260
|
+
valid_scopes = {"user", "session", "agent", "org"}
|
|
1261
|
+
if scope_type not in valid_scopes:
|
|
1262
|
+
raise ValueError(f"Invalid scope_type '{scope_type}'. Must be one of: {valid_scopes}")
|
|
1263
|
+
if not scope_id or not scope_id.strip():
|
|
1264
|
+
raise ValueError("scope_id cannot be empty.")
|
|
1265
|
+
if not fact_statement or not fact_statement.strip():
|
|
1266
|
+
raise ValueError("fact_statement cannot be empty.")
|
|
1267
|
+
|
|
1268
|
+
results = self._gc.store_fact(
|
|
1269
|
+
scope_type=scope_type,
|
|
1270
|
+
scope_id=scope_id,
|
|
1271
|
+
fact_statement=fact_statement,
|
|
1272
|
+
)
|
|
1273
|
+
elapsed = time.perf_counter() - t0
|
|
1274
|
+
|
|
1275
|
+
logger.info(
|
|
1276
|
+
"concierge_store_fact OK: scope=%s/%s, decisions=%d, %.3fs",
|
|
1277
|
+
scope_type, scope_id, len(results), elapsed,
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
return {
|
|
1281
|
+
"success": True,
|
|
1282
|
+
"scope_type": scope_type,
|
|
1283
|
+
"scope_id": scope_id,
|
|
1284
|
+
"decisions": results,
|
|
1285
|
+
"duration_seconds": round(elapsed, 3),
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
except Exception as e:
|
|
1289
|
+
elapsed = time.perf_counter() - t0
|
|
1290
|
+
logger.error("concierge_store_fact FAILED: %s", e)
|
|
1291
|
+
return {
|
|
1292
|
+
"success": False,
|
|
1293
|
+
"error": str(e),
|
|
1294
|
+
"scope_type": scope_type,
|
|
1295
|
+
"scope_id": scope_id,
|
|
1296
|
+
"duration_seconds": round(elapsed, 3),
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
# ===================================================================
|
|
1300
|
+
# RUN — Server initialization
|
|
1301
|
+
# ===================================================================
|
|
1302
|
+
|
|
1303
|
+
def run(self, transport: str = "stdio") -> None:
|
|
1304
|
+
"""Starts the MCP server.
|
|
1305
|
+
|
|
1306
|
+
Args:
|
|
1307
|
+
transport: Transport type ('stdio' or 'sse').
|
|
1308
|
+
"""
|
|
1309
|
+
import asyncio
|
|
1310
|
+
logger.info("Starting Grafo Concierge MCP Server (transport=%s)...", transport)
|
|
1311
|
+
|
|
1312
|
+
async def _run_server():
|
|
1313
|
+
if transport == "sse":
|
|
1314
|
+
await self._mcp.run_sse_async()
|
|
1315
|
+
else:
|
|
1316
|
+
await self._mcp.run_stdio_async()
|
|
1317
|
+
|
|
1318
|
+
try:
|
|
1319
|
+
asyncio.run(_run_server())
|
|
1320
|
+
except KeyboardInterrupt:
|
|
1321
|
+
logger.info("Server stopped by user interrupt.")
|
|
1322
|
+
|
|
1323
|
+
# ===================================================================
|
|
1324
|
+
# HANDLER: concierge_set_memory
|
|
1325
|
+
# ===================================================================
|
|
1326
|
+
|
|
1327
|
+
def _handle_set_memory(
|
|
1328
|
+
self,
|
|
1329
|
+
scope_type: str,
|
|
1330
|
+
scope_id: str,
|
|
1331
|
+
block_label: str,
|
|
1332
|
+
content: str,
|
|
1333
|
+
) -> dict:
|
|
1334
|
+
"""Handler for concierge_set_memory — delegates to Facade with fail-fast validation."""
|
|
1335
|
+
t0 = time.perf_counter()
|
|
1336
|
+
try:
|
|
1337
|
+
valid_scopes = {"user", "session", "agent", "org"}
|
|
1338
|
+
if scope_type not in valid_scopes:
|
|
1339
|
+
raise ValueError(f"Invalid scope_type '{scope_type}'. Must be one of: {valid_scopes}")
|
|
1340
|
+
if not scope_id or not scope_id.strip():
|
|
1341
|
+
raise ValueError("scope_id cannot be empty.")
|
|
1342
|
+
if not block_label or not block_label.strip():
|
|
1343
|
+
raise ValueError("block_label cannot be empty.")
|
|
1344
|
+
if not content or not content.strip():
|
|
1345
|
+
raise ValueError("content cannot be empty.")
|
|
1346
|
+
|
|
1347
|
+
memory_id = self._gc.set_core_memory(
|
|
1348
|
+
scope_type=scope_type,
|
|
1349
|
+
scope_id=scope_id,
|
|
1350
|
+
block_label=block_label,
|
|
1351
|
+
content=content,
|
|
1352
|
+
)
|
|
1353
|
+
elapsed = time.perf_counter() - t0
|
|
1354
|
+
logger.info(
|
|
1355
|
+
"concierge_set_memory OK: scope=%s/%s, label=%s, id=%s, %.3fs",
|
|
1356
|
+
scope_type, scope_id, block_label, memory_id, elapsed,
|
|
1357
|
+
)
|
|
1358
|
+
return {
|
|
1359
|
+
"success": True,
|
|
1360
|
+
"memory_id": memory_id,
|
|
1361
|
+
"scope_type": scope_type,
|
|
1362
|
+
"scope_id": scope_id,
|
|
1363
|
+
"block_label": block_label,
|
|
1364
|
+
"duration_seconds": round(elapsed, 3),
|
|
1365
|
+
}
|
|
1366
|
+
except Exception as e:
|
|
1367
|
+
elapsed = time.perf_counter() - t0
|
|
1368
|
+
logger.error("concierge_set_memory FAILED: %s", e)
|
|
1369
|
+
return {
|
|
1370
|
+
"success": False,
|
|
1371
|
+
"error": str(e),
|
|
1372
|
+
"scope_type": scope_type,
|
|
1373
|
+
"scope_id": scope_id,
|
|
1374
|
+
"duration_seconds": round(elapsed, 3),
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
# ===================================================================
|
|
1378
|
+
# HANDLER: concierge_get_memory
|
|
1379
|
+
# ===================================================================
|
|
1380
|
+
|
|
1381
|
+
def _handle_get_memory(
|
|
1382
|
+
self,
|
|
1383
|
+
scope_type: str,
|
|
1384
|
+
scope_id: str,
|
|
1385
|
+
block_label: Optional[str],
|
|
1386
|
+
) -> dict:
|
|
1387
|
+
"""Handler for concierge_get_memory — delegates to Facade with fail-fast validation."""
|
|
1388
|
+
t0 = time.perf_counter()
|
|
1389
|
+
try:
|
|
1390
|
+
valid_scopes = {"user", "session", "agent", "org"}
|
|
1391
|
+
if scope_type not in valid_scopes:
|
|
1392
|
+
raise ValueError(f"Invalid scope_type '{scope_type}'. Must be one of: {valid_scopes}")
|
|
1393
|
+
if not scope_id or not scope_id.strip():
|
|
1394
|
+
raise ValueError("scope_id cannot be empty.")
|
|
1395
|
+
|
|
1396
|
+
blocks = self._gc.get_core_memory_blocks(
|
|
1397
|
+
scope_type=scope_type,
|
|
1398
|
+
scope_id=scope_id,
|
|
1399
|
+
block_label=block_label,
|
|
1400
|
+
)
|
|
1401
|
+
elapsed = time.perf_counter() - t0
|
|
1402
|
+
logger.info(
|
|
1403
|
+
"concierge_get_memory OK: scope=%s/%s, label=%s, blocks=%d, %.3fs",
|
|
1404
|
+
scope_type, scope_id, block_label or '*', len(blocks), elapsed,
|
|
1405
|
+
)
|
|
1406
|
+
return {
|
|
1407
|
+
"success": True,
|
|
1408
|
+
"scope_type": scope_type,
|
|
1409
|
+
"scope_id": scope_id,
|
|
1410
|
+
"block_label": block_label,
|
|
1411
|
+
"blocks": blocks,
|
|
1412
|
+
"duration_seconds": round(elapsed, 3),
|
|
1413
|
+
}
|
|
1414
|
+
except Exception as e:
|
|
1415
|
+
elapsed = time.perf_counter() - t0
|
|
1416
|
+
logger.error("concierge_get_memory FAILED: %s", e)
|
|
1417
|
+
return {
|
|
1418
|
+
"success": False,
|
|
1419
|
+
"error": str(e),
|
|
1420
|
+
"scope_type": scope_type,
|
|
1421
|
+
"scope_id": scope_id,
|
|
1422
|
+
"duration_seconds": round(elapsed, 3),
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
# ===================================================================
|
|
1426
|
+
# HANDLER: concierge_feedback
|
|
1427
|
+
# ===================================================================
|
|
1428
|
+
|
|
1429
|
+
def _handle_feedback(self, fact_id: int, was_useful: bool) -> dict:
|
|
1430
|
+
"""Handler for concierge_feedback — triggers Bayesian learning."""
|
|
1431
|
+
t0 = time.perf_counter()
|
|
1432
|
+
try:
|
|
1433
|
+
self._gc.update_fact_utility(fact_id=fact_id, was_useful=was_useful)
|
|
1434
|
+
elapsed = time.perf_counter() - t0
|
|
1435
|
+
updated_field = "utility_alpha" if was_useful else "utility_beta"
|
|
1436
|
+
logger.info(
|
|
1437
|
+
"concierge_feedback OK: fact_id=%d, was_useful=%s, %s+1, %.3fs",
|
|
1438
|
+
fact_id, was_useful, updated_field, elapsed,
|
|
1439
|
+
)
|
|
1440
|
+
return {
|
|
1441
|
+
"success": True,
|
|
1442
|
+
"fact_id": fact_id,
|
|
1443
|
+
"was_useful": was_useful,
|
|
1444
|
+
"updated_field": updated_field,
|
|
1445
|
+
"message": f"{updated_field} incremented for fact {fact_id}.",
|
|
1446
|
+
"duration_seconds": round(elapsed, 3),
|
|
1447
|
+
}
|
|
1448
|
+
except Exception as e:
|
|
1449
|
+
elapsed = time.perf_counter() - t0
|
|
1450
|
+
logger.error("concierge_feedback FAILED: %s", e)
|
|
1451
|
+
return {
|
|
1452
|
+
"success": False,
|
|
1453
|
+
"fact_id": fact_id,
|
|
1454
|
+
"error": str(e),
|
|
1455
|
+
"duration_seconds": round(elapsed, 3),
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
# ===================================================================
|
|
1459
|
+
# HANDLER: get_full_topology
|
|
1460
|
+
# ===================================================================
|
|
1461
|
+
|
|
1462
|
+
def _handle_get_full_topology(self, project_identifier: Optional[str] = None) -> dict:
|
|
1463
|
+
"""Handler for get_full_topology."""
|
|
1464
|
+
t0 = time.perf_counter()
|
|
1465
|
+
try:
|
|
1466
|
+
project_uuid = None
|
|
1467
|
+
if project_identifier:
|
|
1468
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1469
|
+
|
|
1470
|
+
topology = self._gc.get_full_topology(project_uuid)
|
|
1471
|
+
elapsed = time.perf_counter() - t0
|
|
1472
|
+
logger.info(
|
|
1473
|
+
"get_full_topology OK: project=%s, nodes=%d, edges=%d, %.3fs",
|
|
1474
|
+
project_identifier or "ALL",
|
|
1475
|
+
len(topology.get("nodes", [])),
|
|
1476
|
+
len(topology.get("edges", [])),
|
|
1477
|
+
elapsed,
|
|
1478
|
+
)
|
|
1479
|
+
return {
|
|
1480
|
+
"success": True,
|
|
1481
|
+
"nodes": topology.get("nodes", []),
|
|
1482
|
+
"edges": topology.get("edges", []),
|
|
1483
|
+
"duration_seconds": round(elapsed, 3),
|
|
1484
|
+
}
|
|
1485
|
+
except Exception as e:
|
|
1486
|
+
elapsed = time.perf_counter() - t0
|
|
1487
|
+
logger.error("get_full_topology FAILED: %s", e)
|
|
1488
|
+
return {
|
|
1489
|
+
"success": False,
|
|
1490
|
+
"error": str(e),
|
|
1491
|
+
"nodes": [],
|
|
1492
|
+
"edges": [],
|
|
1493
|
+
"duration_seconds": round(elapsed, 3),
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
# ===================================================================
|
|
1497
|
+
# HANDLER: delete_project
|
|
1498
|
+
# ===================================================================
|
|
1499
|
+
|
|
1500
|
+
def _handle_delete_project(self, project_identifier: str) -> dict:
|
|
1501
|
+
"""Handler for delete_project."""
|
|
1502
|
+
t0 = time.perf_counter()
|
|
1503
|
+
try:
|
|
1504
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1505
|
+
self._gc.delete_project(project_uuid)
|
|
1506
|
+
elapsed = time.perf_counter() - t0
|
|
1507
|
+
logger.info("delete_project OK: %s in %.3fs", project_uuid, elapsed)
|
|
1508
|
+
return {
|
|
1509
|
+
"success": True,
|
|
1510
|
+
"project_uuid": project_uuid,
|
|
1511
|
+
"duration_seconds": round(elapsed, 3),
|
|
1512
|
+
}
|
|
1513
|
+
except Exception as e:
|
|
1514
|
+
elapsed = time.perf_counter() - t0
|
|
1515
|
+
logger.error("delete_project FAILED: %s — %s", project_identifier, e)
|
|
1516
|
+
return {
|
|
1517
|
+
"success": False,
|
|
1518
|
+
"error": str(e),
|
|
1519
|
+
"duration_seconds": round(elapsed, 3),
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
# ===================================================================
|
|
1523
|
+
# HANDLER: update_project
|
|
1524
|
+
# ===================================================================
|
|
1525
|
+
|
|
1526
|
+
def _handle_update_project(
|
|
1527
|
+
self,
|
|
1528
|
+
project_identifier: str,
|
|
1529
|
+
folder_name: Optional[str] = None,
|
|
1530
|
+
primary_wing: Optional[str] = None,
|
|
1531
|
+
privacy_level: Optional[str] = None,
|
|
1532
|
+
summary: Optional[str] = None,
|
|
1533
|
+
) -> dict:
|
|
1534
|
+
"""Handler for update_project."""
|
|
1535
|
+
t0 = time.perf_counter()
|
|
1536
|
+
try:
|
|
1537
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1538
|
+
fields = {}
|
|
1539
|
+
if folder_name is not None:
|
|
1540
|
+
fields["folder_name"] = folder_name
|
|
1541
|
+
if primary_wing is not None:
|
|
1542
|
+
fields["primary_wing"] = primary_wing
|
|
1543
|
+
if privacy_level is not None:
|
|
1544
|
+
fields["privacy_level"] = privacy_level
|
|
1545
|
+
if summary is not None:
|
|
1546
|
+
fields["summary"] = summary
|
|
1547
|
+
|
|
1548
|
+
self._gc.update_project(project_uuid, **fields)
|
|
1549
|
+
elapsed = time.perf_counter() - t0
|
|
1550
|
+
logger.info("update_project OK: %s with fields=%s in %.3fs", project_uuid, list(fields.keys()), elapsed)
|
|
1551
|
+
return {
|
|
1552
|
+
"success": True,
|
|
1553
|
+
"project_uuid": project_uuid,
|
|
1554
|
+
"updated_fields": list(fields.keys()),
|
|
1555
|
+
"duration_seconds": round(elapsed, 3),
|
|
1556
|
+
}
|
|
1557
|
+
except Exception as e:
|
|
1558
|
+
elapsed = time.perf_counter() - t0
|
|
1559
|
+
logger.error("update_project FAILED: %s — %s", project_identifier, e)
|
|
1560
|
+
return {
|
|
1561
|
+
"success": False,
|
|
1562
|
+
"error": str(e),
|
|
1563
|
+
"duration_seconds": round(elapsed, 3),
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
# ===================================================================
|
|
1567
|
+
# HANDLER: add_reference_wing
|
|
1568
|
+
# ===================================================================
|
|
1569
|
+
|
|
1570
|
+
def _handle_add_reference_wing(self, project_identifier: str, wing_name: str) -> dict:
|
|
1571
|
+
"""Handler for add_reference_wing."""
|
|
1572
|
+
t0 = time.perf_counter()
|
|
1573
|
+
try:
|
|
1574
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1575
|
+
self._gc.add_reference_wing(project_uuid, wing_name)
|
|
1576
|
+
elapsed = time.perf_counter() - t0
|
|
1577
|
+
logger.info("add_reference_wing OK: %s -> %s in %.3fs", project_uuid, wing_name, elapsed)
|
|
1578
|
+
return {
|
|
1579
|
+
"success": True,
|
|
1580
|
+
"project_uuid": project_uuid,
|
|
1581
|
+
"wing_name": wing_name,
|
|
1582
|
+
"duration_seconds": round(elapsed, 3),
|
|
1583
|
+
}
|
|
1584
|
+
except Exception as e:
|
|
1585
|
+
elapsed = time.perf_counter() - t0
|
|
1586
|
+
logger.error("add_reference_wing FAILED: %s — %s", project_identifier, e)
|
|
1587
|
+
return {
|
|
1588
|
+
"success": False,
|
|
1589
|
+
"error": str(e),
|
|
1590
|
+
"duration_seconds": round(elapsed, 3),
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
# ===================================================================
|
|
1594
|
+
# HANDLER: remove_reference_wing
|
|
1595
|
+
# ===================================================================
|
|
1596
|
+
|
|
1597
|
+
def _handle_remove_reference_wing(self, project_identifier: str, wing_name: str) -> dict:
|
|
1598
|
+
"""Handler for remove_reference_wing."""
|
|
1599
|
+
t0 = time.perf_counter()
|
|
1600
|
+
try:
|
|
1601
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1602
|
+
self._gc.remove_reference_wing(project_uuid, wing_name)
|
|
1603
|
+
elapsed = time.perf_counter() - t0
|
|
1604
|
+
logger.info("remove_reference_wing OK: %s -> %s in %.3fs", project_uuid, wing_name, elapsed)
|
|
1605
|
+
return {
|
|
1606
|
+
"success": True,
|
|
1607
|
+
"project_uuid": project_uuid,
|
|
1608
|
+
"wing_name": wing_name,
|
|
1609
|
+
"duration_seconds": round(elapsed, 3),
|
|
1610
|
+
}
|
|
1611
|
+
except Exception as e:
|
|
1612
|
+
elapsed = time.perf_counter() - t0
|
|
1613
|
+
logger.error("remove_reference_wing FAILED: %s — %s", project_identifier, e)
|
|
1614
|
+
return {
|
|
1615
|
+
"success": False,
|
|
1616
|
+
"error": str(e),
|
|
1617
|
+
"duration_seconds": round(elapsed, 3),
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
# ===================================================================
|
|
1621
|
+
# HANDLER: find_similar
|
|
1622
|
+
# ===================================================================
|
|
1623
|
+
|
|
1624
|
+
def _handle_find_similar(
|
|
1625
|
+
self,
|
|
1626
|
+
project_identifier: str,
|
|
1627
|
+
limit: int = 5,
|
|
1628
|
+
include_references: bool = False,
|
|
1629
|
+
all_wings: bool = False,
|
|
1630
|
+
) -> dict:
|
|
1631
|
+
"""Handler for find_similar."""
|
|
1632
|
+
t0 = time.perf_counter()
|
|
1633
|
+
try:
|
|
1634
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1635
|
+
similar = self._gc.find_similar(
|
|
1636
|
+
project_uuid=project_uuid,
|
|
1637
|
+
limit=limit,
|
|
1638
|
+
include_references=include_references,
|
|
1639
|
+
all_wings=all_wings,
|
|
1640
|
+
)
|
|
1641
|
+
elapsed = time.perf_counter() - t0
|
|
1642
|
+
logger.info("find_similar OK: %s (limit=%d) in %.3fs", project_uuid, limit, elapsed)
|
|
1643
|
+
return {
|
|
1644
|
+
"success": True,
|
|
1645
|
+
"project_uuid": project_uuid,
|
|
1646
|
+
"similar_projects": similar,
|
|
1647
|
+
"duration_seconds": round(elapsed, 3),
|
|
1648
|
+
}
|
|
1649
|
+
except Exception as e:
|
|
1650
|
+
elapsed = time.perf_counter() - t0
|
|
1651
|
+
logger.error("find_similar FAILED: %s — %s", project_identifier, e)
|
|
1652
|
+
return {
|
|
1653
|
+
"success": False,
|
|
1654
|
+
"error": str(e),
|
|
1655
|
+
"duration_seconds": round(elapsed, 3),
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
# ===================================================================
|
|
1659
|
+
# HANDLER: get_trajectories
|
|
1660
|
+
# ===================================================================
|
|
1661
|
+
|
|
1662
|
+
def _handle_get_trajectories(self, project_identifier: str) -> dict:
|
|
1663
|
+
"""Handler for get_trajectories."""
|
|
1664
|
+
t0 = time.perf_counter()
|
|
1665
|
+
try:
|
|
1666
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1667
|
+
trajectories = self._gc.get_trajectories(project_uuid)
|
|
1668
|
+
elapsed = time.perf_counter() - t0
|
|
1669
|
+
logger.info("get_trajectories OK: %s in %.3fs", project_uuid, elapsed)
|
|
1670
|
+
return {
|
|
1671
|
+
"success": True,
|
|
1672
|
+
"project_uuid": project_uuid,
|
|
1673
|
+
"trajectories": trajectories,
|
|
1674
|
+
"duration_seconds": round(elapsed, 3),
|
|
1675
|
+
}
|
|
1676
|
+
except Exception as e:
|
|
1677
|
+
elapsed = time.perf_counter() - t0
|
|
1678
|
+
logger.error("get_trajectories FAILED: %s — %s", project_identifier, e)
|
|
1679
|
+
return {
|
|
1680
|
+
"success": False,
|
|
1681
|
+
"error": str(e),
|
|
1682
|
+
"duration_seconds": round(elapsed, 3),
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
# ===================================================================
|
|
1686
|
+
# HANDLER: count_embeddings
|
|
1687
|
+
# ===================================================================
|
|
1688
|
+
|
|
1689
|
+
def _handle_count_embeddings(self, project_identifier: Optional[str] = None) -> dict:
|
|
1690
|
+
"""Handler for count_embeddings."""
|
|
1691
|
+
t0 = time.perf_counter()
|
|
1692
|
+
try:
|
|
1693
|
+
project_uuid = None
|
|
1694
|
+
if project_identifier:
|
|
1695
|
+
project_uuid = self._resolve_project_identifier(project_identifier)
|
|
1696
|
+
|
|
1697
|
+
count = self._gc.count_embeddings(project_uuid)
|
|
1698
|
+
elapsed = time.perf_counter() - t0
|
|
1699
|
+
logger.info("count_embeddings OK: project=%s, count=%d in %.3fs", project_uuid or "ALL", count, elapsed)
|
|
1700
|
+
return {
|
|
1701
|
+
"success": True,
|
|
1702
|
+
"project_uuid": project_uuid,
|
|
1703
|
+
"count": count,
|
|
1704
|
+
"duration_seconds": round(elapsed, 3),
|
|
1705
|
+
}
|
|
1706
|
+
except Exception as e:
|
|
1707
|
+
elapsed = time.perf_counter() - t0
|
|
1708
|
+
logger.error("count_embeddings FAILED: %s — %s", project_identifier, e)
|
|
1709
|
+
return {
|
|
1710
|
+
"success": False,
|
|
1711
|
+
"error": str(e),
|
|
1712
|
+
"duration_seconds": round(elapsed, 3),
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
# ===================================================================
|
|
1716
|
+
# HANDLER: reset_collection
|
|
1717
|
+
# ===================================================================
|
|
1718
|
+
|
|
1719
|
+
def _handle_reset_collection(self) -> dict:
|
|
1720
|
+
"""Handler for reset_collection."""
|
|
1721
|
+
t0 = time.perf_counter()
|
|
1722
|
+
try:
|
|
1723
|
+
success = self._gc.reset_collection()
|
|
1724
|
+
elapsed = time.perf_counter() - t0
|
|
1725
|
+
logger.info("reset_collection OK: %s in %.3fs", success, elapsed)
|
|
1726
|
+
return {
|
|
1727
|
+
"success": success,
|
|
1728
|
+
"duration_seconds": round(elapsed, 3),
|
|
1729
|
+
}
|
|
1730
|
+
except Exception as e:
|
|
1731
|
+
elapsed = time.perf_counter() - t0
|
|
1732
|
+
logger.error("reset_collection FAILED: %s", e)
|
|
1733
|
+
return {
|
|
1734
|
+
"success": False,
|
|
1735
|
+
"error": str(e),
|
|
1736
|
+
"duration_seconds": round(elapsed, 3),
|
|
1737
|
+
}
|