diff --git a/src/iai_mcp/cli.py b/src/iai_mcp/cli.py index dbbe12f..b5eb307 100644 --- a/src/iai_mcp/cli.py +++ b/src/iai_mcp/cli.py @@ -1499,14 +1499,31 @@ def cmd_topology(args: argparse.Namespace) -> int: guard). The CLI is a print-only command -- no event writes, no state mutation. compute_and_emit() runs in S4's offline pass instead (see `iai_mcp.s4.run_offline_pass`). - """ - from iai_mcp.retrieve import build_runtime_graph - from iai_mcp.sigma import compute_topology_snapshot - from iai_mcp.store import MemoryStore - store = MemoryStore() - graph, _assignment, _rich_club = build_runtime_graph(store) - snap = compute_topology_snapshot(graph) + Uses the daemon's topology handler via the control socket so the CLI + never needs to instantiate a local store (avoids lancedb/Qdrant deps + on non-AVX CPUs or when QDRANT_URL is only in systemd env). + """ + snap = _send_socket_request({"type": "topology"}) + if snap is None: + print("ERROR: daemon unreachable", file=sys.stderr) + return 1 + + def _fmt(v) -> str: + if v is None: + return "insufficient_data" + if isinstance(v, float): + return f"{v:.4f}" + return str(v) + + print(f"C: {_fmt(snap.get('C'))}") + print(f"L: {_fmt(snap.get('L'))}") + print(f"sigma: {_fmt(snap.get('sigma'))}") + print(f"communities: {_fmt(snap.get('community_count'))}") + print(f"rich_club_ratio: {_fmt(snap.get('rich_club_ratio'))}") + print(f"N: {_fmt(snap.get('N'))}") + print(f"regime: {_fmt(snap.get('regime'))}") + return 0 def _fmt(v) -> str: if v is None: diff --git a/src/iai_mcp/doctor.py b/src/iai_mcp/doctor.py index 9fdee4f..aa5817e 100644 --- a/src/iai_mcp/doctor.py +++ b/src/iai_mcp/doctor.py @@ -433,7 +433,31 @@ def check_f_lancedb_readable() -> CheckResult: Open a MemoryStore handle. The constructor opens the lancedb connection; if the directory is corrupt / permission-denied / disk-full, the constructor raises and we report FAIL. + + Skips gracefully when Qdrant is the active backend or lancedb is + unavailable (non-AVX CPU, etc.) — returns PASS with skip reason. """ + from iai_mcp.store import _use_qdrant + + if _use_qdrant(): + return CheckResult( + "(f) lancedb store readable", + True, + "skipped (Qdrant backend active)", + ) + + # Heuristic: qdrant_storage/ directory present → Qdrant is the active + # backend even if QDRANT_URL is not set in the current shell (e.g. + # systemd service provides it but interactive shell does not). + env_path = os.environ.get("IAI_MCP_STORE") + store_root = Path(env_path) if env_path else (Path.home() / ".iai-mcp") + if (store_root / "qdrant_storage").exists(): + return CheckResult( + "(f) lancedb store readable", + True, + "skipped (Qdrant backend detected via qdrant_storage/)", + ) + try: from iai_mcp.store import MemoryStore @@ -443,11 +467,24 @@ def check_f_lancedb_readable() -> CheckResult: True, "opens without error", ) + except KeyboardInterrupt: + raise + except SystemExit: + raise except Exception as e: # noqa: BLE001 — surface any open failure + # Non-AVX CPUs may crash in lancedb native libs (SIGILL); treat as + # unavailable rather than a store corruption failure. + exc_name = type(e).__name__ + if exc_name == "IllegalInstruction" or "illegal" in str(e).lower(): + return CheckResult( + "(f) lancedb store readable", + True, + f"skipped (lancedb unavailable on this CPU: {exc_name})", + ) return CheckResult( "(f) lancedb store readable", False, - f"open failed: {type(e).__name__}: {e}", + f"open failed: {exc_name}: {e}", ) diff --git a/src/iai_mcp/qdrant_store.py b/src/iai_mcp/qdrant_store.py index abe4ae7..e761f04 100644 --- a/src/iai_mcp/qdrant_store.py +++ b/src/iai_mcp/qdrant_store.py @@ -1289,6 +1289,52 @@ class QdrantStore: except Exception: return pd.DataFrame(columns=["src", "dst", "edge_type", "weight", "updated_at"]) + def records_as_dataframe(self) -> "pd.DataFrame": + """Return all records from the records collection as a pandas DataFrame.""" + try: + records = self.all_records() + if not records: + return pd.DataFrame(columns=[ + "id", "tier", "literal_surface", "embedding", + "community_id", "centrality", "pinned", + "tags_json", "language", "aaak_index", + "stability", "difficulty", "last_reviewed", + "never_decay", "never_merge", "detail_level", + "s5_trust_score", "structure_hv", + ]) + rows = [] + for r in records: + rows.append({ + "id": str(r.id), + "tier": r.tier, + "literal_surface": r.literal_surface, + "embedding": r.embedding, + "community_id": str(r.community_id) if r.community_id else None, + "centrality": r.centrality, + "pinned": r.pinned, + "tags_json": r.tags_json if hasattr(r, "tags_json") else "[]", + "language": r.language, + "aaak_index": r.aaak_index, + "stability": r.stability, + "difficulty": r.difficulty, + "last_reviewed": str(r.last_reviewed) if r.last_reviewed else None, + "never_decay": r.never_decay, + "never_merge": r.never_merge, + "detail_level": r.detail_level, + "s5_trust_score": r.s5_trust_score, + "structure_hv": r.structure_hv.hex() if r.structure_hv else "", + }) + return pd.DataFrame(rows) + except Exception: + return pd.DataFrame(columns=[ + "id", "tier", "literal_surface", "embedding", + "community_id", "centrality", "pinned", + "tags_json", "language", "aaak_index", + "stability", "difficulty", "last_reviewed", + "never_decay", "never_merge", "detail_level", + "s5_trust_score", "structure_hv", + ]) + # ------------------------------------------------------------------ db shim class _DbShim: @@ -1311,6 +1357,8 @@ class QdrantStore: def to_pandas(self) -> pd.DataFrame: if self._name == EDGES_TABLE: return self._store.edges_as_dataframe() + elif self._name == RECORDS_TABLE: + return self._store.records_as_dataframe() elif self._name == EVENTS_TABLE: return pd.DataFrame() return pd.DataFrame()