From d20a3a13548b5dc2746ad6110b76ac68c470e144 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 7 Apr 2026 08:52:50 +0200 Subject: [PATCH] chore(db): remove Qdrant semantic search infrastructure (#816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Qdrant index (commonwealth collection, 475 points) was stale — pointing at old worktree paths from previous sprints with no maintenance. Grep covers all current search needs. Removed: qdrant_connector.py, wrapper scripts (qdrant-search, qdrant-index, qdrant-health, qdrant-count), /docs-search skill, Qdrant/Ollama config entries, and all active references in agents, rules, briefings, DEVOPS, CLAUDE.md, and TEAM.md. The commonwealth collection was dropped from tower-of-joy:6333. Historical references in discussion archives and sprint briefings are preserved as-is. Co-Authored-By: Claude Opus 4.6 --- .claude/agents/README.md | 3 +- .claude/agents/qatux.md | 5 +- .claude/rules/local-services.md | 3 - .claude/rules/project-structure.md | 3 +- .claude/settings.json | 4 - .claude/skills/docs-search/SKILL.md | 55 ---- CLAUDE.md | 2 - TEAM.md | 2 +- docs/DEVOPS.md | 9 - docs/briefings/qatux.md | 5 +- tooling/db/config.json | 7 +- tooling/db/qdrant-count | 3 - tooling/db/qdrant-health | 3 - tooling/db/qdrant-index | 4 - tooling/db/qdrant-search | 4 - tooling/db/qdrant_connector.py | 437 ---------------------------- 16 files changed, 8 insertions(+), 541 deletions(-) delete mode 100644 .claude/skills/docs-search/SKILL.md delete mode 100755 tooling/db/qdrant-count delete mode 100755 tooling/db/qdrant-health delete mode 100755 tooling/db/qdrant-index delete mode 100755 tooling/db/qdrant-search delete mode 100755 tooling/db/qdrant_connector.py diff --git a/.claude/agents/README.md b/.claude/agents/README.md index e280f2ad9..655cc432c 100644 --- a/.claude/agents/README.md +++ b/.claude/agents/README.md @@ -115,8 +115,7 @@ Synthesize findings. ### Qatux (Documenter & Librarian) - Core team member — participates in discussion rounds as documenter -- Manages document search via `/docs-search` skill -- Maintains DECISIONS.md, DISCUSSION.md, briefings, and Qdrant search index +- Maintains DECISIONS.md, DISCUSSION.md, and briefings - Answers "did we discuss this?" with citations ## Extending the team diff --git a/.claude/agents/qatux.md b/.claude/agents/qatux.md index ea817d5d2..c0c8fa50a 100644 --- a/.claude/agents/qatux.md +++ b/.claude/agents/qatux.md @@ -1,6 +1,6 @@ --- name: qatux -description: Documenter and Librarian for the Settled Reach game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains decisions/ domain files, DISCUSSION.md, briefings, and the Qdrant search index. +description: Documenter and Librarian for the Settled Reach game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains decisions/ domain files, DISCUSSION.md, and briefings. tools: Read, Glob, Grep, Edit, Write, Bash model: sonnet memory: project @@ -28,7 +28,6 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli - Provide "state of the project" summaries when asked ### Knowledge management -- Maintain the Qdrant document index via /docs-search skill - Update briefing files when decisions change - Answer retrieval questions: "did we discuss X?", "what did we decide about Y?" - Catch staleness in briefings and flag for update @@ -44,7 +43,7 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli - **Work in dedicated round files:** All new rounds happen in `docs/discussions/round-NN-topic.md` from the start. DISCUSSION.md is retired for new content. - **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link). - **Update briefings:** After a round produces new decisions, update the relevant agent briefing files in `docs/briefings/`. -- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `tooling/db/qdrant-index `. +- **Update briefings:** After archiving or updating documents, update the relevant agent briefing files in `docs/briefings/`. ## Team workflow (mandatory) diff --git a/.claude/rules/local-services.md b/.claude/rules/local-services.md index df1a95c22..0a5e76931 100644 --- a/.claude/rules/local-services.md +++ b/.claude/rules/local-services.md @@ -3,6 +3,3 @@ Endpoints are also preconfigured in `tooling/db/config.json`. - **Gitea:** `http://git.schweitz.internal` (login: `schweitz`) -- **Qdrant:** `http://tower-of-joy:6333/` -- **Ollama:** `http://tower-of-joy:11434/` (nomic-embed-text) -- **Collection:** `commonwealth` (768 dimensions, cosine distance) diff --git a/.claude/rules/project-structure.md b/.claude/rules/project-structure.md index 3033be7ab..92634314a 100644 --- a/.claude/rules/project-structure.md +++ b/.claude/rules/project-structure.md @@ -26,12 +26,11 @@ docs/ db/ schema.sql # Database schema tooling/ - db/ # Connector scripts for SQLite, Qdrant, and audio + db/ # Connector scripts for SQLite and audio config.json # Endpoint configuration ticket # Ticket CLI sprint # Sprint lifecycle CLI sqlite_connector.py # SQLite mini MCP - qdrant_connector.py # Qdrant + ollama mini MCP audio_connector.py # Stable Audio Open connector .claude/ agents/ # Agent personality files diff --git a/.claude/settings.json b/.claude/settings.json index 726e21aaf..ef204f3b3 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -31,10 +31,6 @@ "Bash(tooling/db/sprint *)", "Bash(tooling/db/sqlite-query *)", "Bash(tooling/db/sqlite-exec *)", - "Bash(tooling/db/qdrant-search *)", - "Bash(tooling/db/qdrant-index *)", - "Bash(tooling/db/qdrant-health)", - "Bash(tooling/db/qdrant-count)", "Bash(tooling/db/sqlite-init)", "Bash(tooling/db/decisions-sync)", "Bash(tooling/db/decision *)", diff --git a/.claude/skills/docs-search/SKILL.md b/.claude/skills/docs-search/SKILL.md deleted file mode 100644 index fb20cc01e..000000000 --- a/.claude/skills/docs-search/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: docs-search -description: > - Search project documents using semantic search (Qdrant + ollama) or grep fallback. - Use when the user asks "did we discuss X?", "find references to Y", "search docs", - or invokes /docs-search. Wraps the qdrant_connector.py for semantic document search. -user-invocable: true -allowed-tools: Bash, Read, Grep, Glob ---- - -# Search Docs Skill - -Semantic search across project documents. Endpoints are in -`.claude/rules/local-services.md`. This skill covers advanced operations -and workflows. - -## Advanced Commands - -### Index a single chunk - -For precise indexing of specific content: -```bash -python3 tooling/db/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading" -``` - -### Create collection - -Initialize the Qdrant collection (run once during setup): -```bash -python3 tooling/db/qdrant_connector.py create-collection -``` - -## Bulk Indexing - -Index all project documents at once: -```bash -for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do - tooling/db/qdrant-index "$f" -done -``` - -## Fallback - -If Qdrant or ollama is unreachable, fall back to grep-based search: -```bash -grep -r -i "search term" decisions/ DISCUSSION.md docs/ --include="*.md" -``` - -## Workflow - -1. **Qatux (Librarian)** is the primary user of this skill -2. After each discussion round, index the archived round file -3. After briefing updates, re-index affected briefings -4. After decision changes, re-index the relevant decisions/*.md domain files -5. Use search to answer "did we discuss this?" questions with citations diff --git a/CLAUDE.md b/CLAUDE.md index d63d7b657..14727560d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,8 +73,6 @@ The ticketing database (`settledreach.db`) is accessed via `SR_DB_PATH` env var | SQL queries | `tooling/db/sqlite-query "SELECT ..."` | — | | SQL writes | `tooling/db/sqlite-exec "UPDATE ..."` | — | | Decisions | `tooling/db/decision next`, `claim`, `check-dupes` | — | -| Doc search | `tooling/db/qdrant-search "query"` | `/docs-search` skill | -| Doc index | `tooling/db/qdrant-index path/to/file.md` | `/docs-search` skill | ### Testing preferences diff --git a/TEAM.md b/TEAM.md index 1aeeaa9b6..bac6a879e 100644 --- a/TEAM.md +++ b/TEAM.md @@ -15,7 +15,7 @@ | **NIGEL** | Sandbox & Replayability | Emergent stories, multiple viable strategies, alt-history potential. | | **TYRE** | Technical Architecture & Feasibility | Engine, tools, what's buildable, reality checks on scope. | | **BURNELLI-SHELDON** | Economist & Simulation Modeler | Market models, price formation, production functions, stability analysis. "Is this economically credible?" | -| **QATUX** | Documenter & Librarian | Maintains decisions, discussions, briefings, Qdrant search index. Archives rounds, updates docs. | +| **QATUX** | Documenter & Librarian | Maintains decisions, discussions, briefings. Archives rounds, updates docs. | ## Specialist Team (task-focused, not in regular discussions) diff --git a/docs/DEVOPS.md b/docs/DEVOPS.md index 6a0a58a5e..383aa2e7c 100644 --- a/docs/DEVOPS.md +++ b/docs/DEVOPS.md @@ -280,15 +280,6 @@ tooling/db/sqlite-query "SELECT * FROM tickets WHERE status='open'" tooling/db/sqlite-exec "UPDATE tickets SET status='done' WHERE id=1" ``` -## Qdrant / Document Search - -```bash -tooling/db/qdrant-search "asymmetric information design" -tooling/db/qdrant-index docs/briefings/tyre.md -tooling/db/qdrant-health -tooling/db/qdrant-count -``` - ## Decisions System Decisions are split into domain files under `decisions/` (see `decisions/README.md` for the full index). A SQLite index table syncs metadata for cross-referencing and querying. diff --git a/docs/briefings/qatux.md b/docs/briefings/qatux.md index 891729c8a..1881effd5 100644 --- a/docs/briefings/qatux.md +++ b/docs/briefings/qatux.md @@ -1,6 +1,6 @@ --- title: "Qatux - Project Briefing" -description: "Decision archivist and documentation maintainer — owns all D/Q/R-records in decisions/, agent briefings, discussion rounds, diagrams, and Qdrant indexing" +description: "Decision archivist and documentation maintainer — owns all D/Q/R-records in decisions/, agent briefings, discussion rounds, and diagrams" type: briefing status: active agent: Qatux @@ -51,8 +51,7 @@ None assigned directly. Track all Q-NNN and Q-WTF-* records. 3. Update `docs/discussions/README.md` with new round entries after formal closure 4. Update relevant agent briefing files with new decision references 5. **Create and update diagrams** whenever D-records are added or modified — use the `/d2-diagram` skill to generate d2 source + PNG. Existing diagrams in `docs/diagrams/{category}/` must be updated when their source decisions change. Categories: architecture, data-flow, entity, state, ui. -6. Re-index changed documents in Qdrant after updates -7. Verify briefing freshness against decision domain files +6. Verify briefing freshness against decision domain files ## Key Documents - `decisions/` — domain-split decision files (see `decisions/README.md` for index) diff --git a/tooling/db/config.json b/tooling/db/config.json index 6d0eab87e..cdfb077e8 100644 --- a/tooling/db/config.json +++ b/tooling/db/config.json @@ -1,9 +1,4 @@ { - "qdrant_url": "http://tower-of-joy:6333", - "ollama_url": "http://tower-of-joy:11434", "stable_audio_url": "http://tower-of-joy:11500", - "trellis_url": "http://tower-of-joy:11510", - "collection": "commonwealth", - "embed_model": "nomic-embed-text", - "embed_dimensions": 768 + "trellis_url": "http://tower-of-joy:11510" } diff --git a/tooling/db/qdrant-count b/tooling/db/qdrant-count deleted file mode 100755 index 2c0b46e84..000000000 --- a/tooling/db/qdrant-count +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# Count indexed documents in Qdrant. Whitelistable command. -exec python3 "$(dirname "$0")/qdrant_connector.py" count diff --git a/tooling/db/qdrant-health b/tooling/db/qdrant-health deleted file mode 100755 index 98b1c61ea..000000000 --- a/tooling/db/qdrant-health +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# Check Qdrant and ollama connectivity. Whitelistable command. -exec python3 "$(dirname "$0")/qdrant_connector.py" health diff --git a/tooling/db/qdrant-index b/tooling/db/qdrant-index deleted file mode 100755 index bf704db9d..000000000 --- a/tooling/db/qdrant-index +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -# Index a file into Qdrant. Whitelistable command. -# Usage: qdrant-index -exec python3 "$(dirname "$0")/qdrant_connector.py" index-file "$@" diff --git a/tooling/db/qdrant-search b/tooling/db/qdrant-search deleted file mode 100755 index 7d0b4b60c..000000000 --- a/tooling/db/qdrant-search +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -# Search the Qdrant document index. Whitelistable command. -# Usage: qdrant-search "query text" -exec python3 "$(dirname "$0")/qdrant_connector.py" search "$@" diff --git a/tooling/db/qdrant_connector.py b/tooling/db/qdrant_connector.py deleted file mode 100755 index e0559cb96..000000000 --- a/tooling/db/qdrant_connector.py +++ /dev/null @@ -1,437 +0,0 @@ -#!/usr/bin/env python3 -""" -Settled Reach Qdrant + Ollama Connector — mini MCP for vector search. - -Usage: - python3 qdrant_connector.py health - python3 qdrant_connector.py create-collection - python3 qdrant_connector.py search "some query text" - python3 qdrant_connector.py index "text to embed" [--metadata key=value ...] - python3 qdrant_connector.py index-file - python3 qdrant_connector.py count - python3 qdrant_connector.py --help - -Requires only Python 3 stdlib (no pip dependencies). -""" - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import ensure_venv # noqa: E402 - -ensure_venv() - -import hashlib -import json -import re -import urllib.error -import urllib.request - -# --------------------------------------------------------------------------- -# Paths / Config -# --------------------------------------------------------------------------- - -SCRIPT_DIR = Path(__file__).resolve().parent -CONFIG_PATH = SCRIPT_DIR / "config.json" - - -def load_config(): - """Load config.json.""" - with open(CONFIG_PATH, "r") as f: - return json.load(f) - - -# --------------------------------------------------------------------------- -# HTTP helpers (stdlib only) -# --------------------------------------------------------------------------- - -def http_request(url, method="GET", data=None, headers=None, timeout=30): - """ - Perform an HTTP request using urllib. Returns (status_code, parsed_json | raw_text). - """ - hdrs = {"Content-Type": "application/json"} - if headers: - hdrs.update(headers) - - body = None - if data is not None: - body = json.dumps(data).encode("utf-8") - - req = urllib.request.Request(url, data=body, headers=hdrs, method=method) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - raw = resp.read().decode("utf-8") - try: - return resp.status, json.loads(raw) - except json.JSONDecodeError: - return resp.status, raw - except urllib.error.HTTPError as exc: - raw = exc.read().decode("utf-8") if exc.fp else "" - try: - return exc.code, json.loads(raw) - except json.JSONDecodeError: - return exc.code, raw - except urllib.error.URLError as exc: - raise ConnectionError(f"Cannot reach {url}: {exc.reason}") from exc - - -# --------------------------------------------------------------------------- -# Embedding helper -# --------------------------------------------------------------------------- - -def embed_text(cfg, text): - """ - Call ollama /api/embed to get an embedding vector for the given text. - Returns a list of floats. - """ - url = f"{cfg['ollama_url']}/api/embed" - payload = {"model": cfg["embed_model"], "input": text} - status, resp = http_request(url, method="POST", data=payload) - if status != 200: - raise RuntimeError(f"Ollama embed failed (HTTP {status}): {resp}") - # ollama returns {"embeddings": [[...]]} - embeddings = resp.get("embeddings") - if not embeddings or not embeddings[0]: - raise RuntimeError(f"Ollama returned empty embeddings: {resp}") - return embeddings[0] - - -# --------------------------------------------------------------------------- -# Qdrant helpers -# --------------------------------------------------------------------------- - -def qdrant_create_collection(cfg): - """Create (or recreate) the Qdrant collection.""" - url = f"{cfg['qdrant_url']}/collections/{cfg['collection']}" - payload = { - "vectors": { - "size": cfg["embed_dimensions"], - "distance": "Cosine", - } - } - status, resp = http_request(url, method="PUT", data=payload) - return status, resp - - -def qdrant_upsert(cfg, points): - """Upsert a list of points into Qdrant.""" - url = f"{cfg['qdrant_url']}/collections/{cfg['collection']}/points" - payload = {"points": points} - status, resp = http_request(url, method="PUT", data=payload) - return status, resp - - -def qdrant_search(cfg, vector, limit=5): - """Search Qdrant by vector.""" - url = f"{cfg['qdrant_url']}/collections/{cfg['collection']}/points/query" - payload = {"query": vector, "limit": limit, "with_payload": True} - status, resp = http_request(url, method="POST", data=payload) - return status, resp - - -def qdrant_collection_info(cfg): - """Get collection info (includes point count).""" - url = f"{cfg['qdrant_url']}/collections/{cfg['collection']}" - status, resp = http_request(url, method="GET") - return status, resp - - -# --------------------------------------------------------------------------- -# Chunking helper -# --------------------------------------------------------------------------- - -def chunk_markdown(text, source_file=""): - """ - Split markdown by headings (# or ##). Returns a list of dicts: - {"heading": str, "text": str, "chunk_index": int, "source_file": str} - """ - # Split on lines that start with one or two hashes - pattern = re.compile(r"^(#{1,2})\s+(.+)$", re.MULTILINE) - matches = list(pattern.finditer(text)) - - chunks = [] - - if not matches: - # No headings — treat entire file as one chunk - stripped = text.strip() - if stripped: - chunks.append({ - "heading": Path(source_file).stem if source_file else "untitled", - "text": stripped, - "chunk_index": 0, - "source_file": source_file, - }) - return chunks - - # Text before the first heading - preamble = text[: matches[0].start()].strip() - if preamble: - chunks.append({ - "heading": "(preamble)", - "text": preamble, - "chunk_index": 0, - "source_file": source_file, - }) - - for i, match in enumerate(matches): - heading = match.group(2).strip() - start = match.end() - end = matches[i + 1].start() if i + 1 < len(matches) else len(text) - body = text[start:end].strip() - if body: - chunks.append({ - "heading": heading, - "text": body, - "chunk_index": len(chunks), - "source_file": source_file, - }) - - return chunks - - -def text_to_point_id(text): - """Deterministic integer ID from a string (unsigned 64-bit range for Qdrant).""" - h = hashlib.sha256(text.encode("utf-8")).hexdigest() - # Qdrant accepts unsigned 64-bit integer IDs - return int(h[:16], 16) - - -# --------------------------------------------------------------------------- -# Commands -# --------------------------------------------------------------------------- - -def cmd_health(cfg): - """Check connectivity to Qdrant and Ollama.""" - results = {} - - # Qdrant health - try: - status, resp = http_request(f"{cfg['qdrant_url']}/healthz", method="GET", timeout=5) - results["qdrant"] = {"reachable": True, "status": status, "response": resp} - except ConnectionError as exc: - results["qdrant"] = {"reachable": False, "error": str(exc)} - - # Ollama health - try: - status, resp = http_request(f"{cfg['ollama_url']}/api/tags", method="GET", timeout=5) - results["ollama"] = {"reachable": True, "status": status} - # List available models for convenience - if isinstance(resp, dict) and "models" in resp: - results["ollama"]["models"] = [m.get("name", "?") for m in resp["models"]] - except ConnectionError as exc: - results["ollama"] = {"reachable": False, "error": str(exc)} - - all_ok = all(v.get("reachable", False) for v in results.values()) - return {"ok": all_ok, "services": results} - - -def cmd_create_collection(cfg): - """Create the Qdrant collection.""" - try: - status, resp = qdrant_create_collection(cfg) - success = status in (200, 201) - return {"ok": success, "status": status, "response": resp} - except ConnectionError as exc: - return {"ok": False, "error": str(exc)} - - -def cmd_search(cfg, query_text): - """Embed query text and search Qdrant.""" - try: - vector = embed_text(cfg, query_text) - status, resp = qdrant_search(cfg, vector) - if status != 200: - return {"ok": False, "status": status, "error": resp} - - # Extract the points from the response - points = resp.get("result", {}).get("points", resp.get("result", [])) - results = [] - if isinstance(points, list): - for pt in points: - results.append({ - "id": pt.get("id"), - "score": pt.get("score"), - "payload": pt.get("payload", {}), - }) - return {"ok": True, "query": query_text, "count": len(results), "results": results} - except (ConnectionError, RuntimeError) as exc: - return {"ok": False, "error": str(exc)} - - -def cmd_index(cfg, point_id_str, text, metadata=None): - """Embed text and upsert a single point.""" - try: - vector = embed_text(cfg, text) - - # Build a numeric ID from the provided string - try: - point_id = int(point_id_str) - except ValueError: - point_id = text_to_point_id(point_id_str) - - payload = metadata or {} - payload["text"] = text - - point = {"id": point_id, "vector": vector, "payload": payload} - status, resp = qdrant_upsert(cfg, [point]) - success = status in (200, 201) - return {"ok": success, "status": status, "point_id": point_id, "response": resp} - except (ConnectionError, RuntimeError) as exc: - return {"ok": False, "error": str(exc)} - - -def cmd_index_file(cfg, filepath): - """Read a markdown file, chunk it, embed each chunk, and upsert all to Qdrant.""" - fpath = Path(filepath).resolve() - if not fpath.exists(): - return {"ok": False, "error": f"File not found: {fpath}"} - - text = fpath.read_text(encoding="utf-8") - source = str(fpath) - chunks = chunk_markdown(text, source_file=source) - - if not chunks: - return {"ok": False, "error": "No content chunks extracted from file"} - - points = [] - errors = [] - for chunk in chunks: - chunk_key = f"{source}::{chunk['heading']}::{chunk['chunk_index']}" - point_id = text_to_point_id(chunk_key) - try: - vector = embed_text(cfg, chunk["text"]) - except (ConnectionError, RuntimeError) as exc: - errors.append({"chunk": chunk["heading"], "error": str(exc)}) - continue - - points.append({ - "id": point_id, - "vector": vector, - "payload": { - "source_file": chunk["source_file"], - "heading": chunk["heading"], - "chunk_index": chunk["chunk_index"], - "text": chunk["text"], - }, - }) - - if not points: - return {"ok": False, "error": "All chunks failed to embed", "details": errors} - - try: - status, resp = qdrant_upsert(cfg, points) - success = status in (200, 201) - result = { - "ok": success, - "status": status, - "file": source, - "chunks_indexed": len(points), - "chunks_failed": len(errors), - "response": resp, - } - if errors: - result["errors"] = errors - return result - except ConnectionError as exc: - return {"ok": False, "error": str(exc)} - - -def cmd_count(cfg): - """Return the point count in the collection.""" - try: - status, resp = qdrant_collection_info(cfg) - if status != 200: - return {"ok": False, "status": status, "error": resp} - # Qdrant returns {"result": {"points_count": N, ...}} - result_data = resp.get("result", {}) - count = result_data.get("points_count", result_data.get("vectors_count", "unknown")) - return {"ok": True, "collection": cfg["collection"], "points_count": count} - except ConnectionError as exc: - return {"ok": False, "error": str(exc)} - - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - -HELP_TEXT = """\ -Settled Reach Qdrant + Ollama Connector - -Usage: - qdrant_connector.py health Check Qdrant & Ollama connectivity - qdrant_connector.py create-collection Create the vector collection - qdrant_connector.py search "" Embed query and search Qdrant - qdrant_connector.py index "" [--metadata k=v ...] - Embed text and upsert one point - qdrant_connector.py index-file Chunk a markdown file and index all chunks - qdrant_connector.py count Show point count in collection - qdrant_connector.py --help Show this help message - -All output is JSON on stdout. Uses only Python stdlib (no pip install needed). - -Config: {config} -""".format(config=CONFIG_PATH) - - -def parse_metadata(args): - """Parse --metadata key=value pairs from argument list.""" - metadata = {} - i = 0 - while i < len(args): - if args[i] == "--metadata" and i + 1 < len(args): - i += 1 - while i < len(args) and "=" in args[i] and not args[i].startswith("--"): - key, _, value = args[i].partition("=") - metadata[key] = value - i += 1 - else: - i += 1 - return metadata - - -def main(): - if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h", "help"): - print(HELP_TEXT) - sys.exit(0) - - cmd = sys.argv[1] - - try: - cfg = load_config() - except (FileNotFoundError, json.JSONDecodeError) as exc: - print(json.dumps({"ok": False, "error": f"Config error: {exc}"}, indent=2)) - sys.exit(1) - - if cmd == "health": - result = cmd_health(cfg) - elif cmd == "create-collection": - result = cmd_create_collection(cfg) - elif cmd == "search": - if len(sys.argv) < 3: - result = {"ok": False, "error": "search requires a query text argument"} - else: - result = cmd_search(cfg, sys.argv[2]) - elif cmd == "index": - if len(sys.argv) < 4: - result = {"ok": False, "error": "index requires and arguments"} - else: - metadata = parse_metadata(sys.argv[4:]) - result = cmd_index(cfg, sys.argv[2], sys.argv[3], metadata) - elif cmd == "index-file": - if len(sys.argv) < 3: - result = {"ok": False, "error": "index-file requires a argument"} - else: - result = cmd_index_file(cfg, sys.argv[2]) - elif cmd == "count": - result = cmd_count(cfg) - else: - result = {"ok": False, "error": f"Unknown command: {cmd}. Use --help for usage."} - - print(json.dumps(result, indent=2)) - sys.exit(0 if result.get("ok") else 1) - - -if __name__ == "__main__": - main()