#!/usr/bin/env python3 """ fleet_api.py — Lightweight HTTP API for the shared fleet palace. Exposes fleet memory search over HTTP so that Alpha servers and other wizard deployments can query the palace without direct filesystem access. Endpoints: GET /health Returns {"status": "ok", "palace": ""} GET /search?q=[&room=][&n=] Returns {"results": [...], "query": "...", "room": "...", "count": N} Each result: {"text": "...", "room": "...", "wing": "...", "score": 0.9} GET /wings Returns {"wings": ["bezalel", ...]} — distinct wizard wings present Error responses use {"error": ""} with appropriate HTTP status codes. Usage: # Default: localhost:7771, fleet palace at /var/lib/mempalace/fleet python mempalace/fleet_api.py # Custom host/port/palace: FLEET_PALACE_PATH=/data/fleet python mempalace/fleet_api.py --host 0.0.0.0 --port 8080 Refs: #1078, #1075 """ from __future__ import annotations import argparse import json import os import sys from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from urllib.parse import parse_qs, urlparse # Add repo root to path so we can import nexus.mempalace _HERE = Path(__file__).resolve().parent _REPO_ROOT = _HERE.parent if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 7771 MAX_RESULTS = 50 def _get_palace_path() -> Path: return Path(os.environ.get("FLEET_PALACE_PATH", "/var/lib/mempalace/fleet")) def _json_response(handler: BaseHTTPRequestHandler, status: int, body: dict) -> None: payload = json.dumps(body).encode() handler.send_response(status) handler.send_header("Content-Type", "application/json") handler.send_header("Content-Length", str(len(payload))) handler.end_headers() handler.wfile.write(payload) def _handle_health(handler: BaseHTTPRequestHandler) -> None: palace = _get_palace_path() _json_response(handler, 200, { "status": "ok", "palace": str(palace), "palace_exists": palace.exists(), }) def _handle_search(handler: BaseHTTPRequestHandler, qs: dict) -> None: query_terms = qs.get("q", [""]) q = query_terms[0].strip() if query_terms else "" if not q: _json_response(handler, 400, {"error": "Missing required parameter: q"}) return room_terms = qs.get("room", []) room = room_terms[0].strip() if room_terms else None n_terms = qs.get("n", []) try: n = max(1, min(int(n_terms[0]), MAX_RESULTS)) if n_terms else 10 except (ValueError, IndexError): _json_response(handler, 400, {"error": "Invalid parameter: n must be an integer"}) return try: from nexus.mempalace.searcher import search_fleet, MemPalaceUnavailable except ImportError as exc: _json_response(handler, 503, {"error": f"MemPalace module not available: {exc}"}) return try: results = search_fleet(q, room=room, n_results=n) except Exception as exc: # noqa: BLE001 _json_response(handler, 503, {"error": str(exc)}) return _json_response(handler, 200, { "query": q, "room": room, "count": len(results), "results": [ { "text": r.text, "room": r.room, "wing": r.wing, "score": round(r.score, 4), } for r in results ], }) def _handle_wings(handler: BaseHTTPRequestHandler) -> None: """Return distinct wizard wing names found in the fleet palace directory.""" palace = _get_palace_path() if not palace.exists(): _json_response(handler, 503, { "error": f"Fleet palace not found: {palace}", }) return wings = sorted({ p.name for p in palace.iterdir() if p.is_dir() }) _json_response(handler, 200, {"wings": wings}) class FleetAPIHandler(BaseHTTPRequestHandler): """Request handler for the fleet memory API.""" def log_message(self, fmt: str, *args) -> None: # noqa: ANN001 # Prefix with tag for easier log filtering sys.stderr.write(f"[fleet_api] {fmt % args}\n") def do_GET(self) -> None: # noqa: N802 parsed = urlparse(self.path) path = parsed.path.rstrip("/") or "/" qs = parse_qs(parsed.query) if path == "/health": _handle_health(self) elif path == "/search": _handle_search(self, qs) elif path == "/wings": _handle_wings(self) else: _json_response(self, 404, { "error": f"Unknown endpoint: {path}", "endpoints": ["/health", "/search", "/wings"], }) def make_server(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> HTTPServer: return HTTPServer((host, port), FleetAPIHandler) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description="Fleet palace HTTP API server." ) parser.add_argument("--host", default=DEFAULT_HOST, help=f"Bind host (default: {DEFAULT_HOST})") parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Bind port (default: {DEFAULT_PORT})") args = parser.parse_args(argv) palace = _get_palace_path() print(f"[fleet_api] Palace: {palace}") if not palace.exists(): print(f"[fleet_api] WARNING: palace path does not exist yet: {palace}", file=sys.stderr) server = make_server(args.host, args.port) print(f"[fleet_api] Listening on http://{args.host}:{args.port}") try: server.serve_forever() except KeyboardInterrupt: print("\n[fleet_api] Shutting down.") return 0 if __name__ == "__main__": sys.exit(main())