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.
@@ -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
+ }