fix: support Qdrant backend on non-AVX CPUs

- doctor: skip LanceDB check when qdrant_storage/ dir detected
- topology: use daemon socket instead of local store (avoids lancedb crash)
- qdrant_store: add records_as_dataframe() + wire into _TableShim
  so build_runtime_graph() works with Qdrant (was returning empty)
This commit is contained in:
Apunkt 2026-05-12 18:14:03 +02:00
parent 01e5903035
commit 8492719735
No known key found for this signature in database
3 changed files with 110 additions and 8 deletions

View file

@ -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:

View file

@ -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}",
)

View file

@ -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()