#!/usr/bin/env python3 """Contracts VM server — thin HTTP router. All logic in db/ and services/.""" from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs import json, re # ── DB auto-seed ────────────────────────────────────────────────────────── from db import prompts as db_prompts from db.connection import DB_CONFIG db_prompts.seed_defaults() # ── Services ────────────────────────────────────────────────────────────── from services.upload import handle_upload from services.unzip import handle_unzip from services.process import run_pipeline from llm_prompt import build_prompt class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True class Handler(BaseHTTPRequestHandler): def do_OPTIONS(self): self.send_response(200) self._send_cors() self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def do_GET(self): parsed = urlparse(self.path) if parsed.path == "/process-v2": self._handle_process_v2(parsed) elif parsed.path == "/health": self._json({"ok": True, "db": DB_CONFIG["dbname"]}) else: self.send_error(404) def do_POST(self): if self.path == "/upload": self._handle_upload() elif self.path == "/unzip-upload": self._handle_unzip() elif self.path == "/llm-ops": self._handle_llm_ops() else: self.send_error(404) # ── /process-v2 (SSE) ───────────────────────────────────────────────── def _handle_process_v2(self, parsed): params = parse_qs(parsed.query) cid = params.get("contract_id", [None])[0] if not cid: self.send_error(400, "contract_id required") return if not re.fullmatch(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', cid, re.I): self.send_error(400, "invalid contract_id format") return self.send_response(200) self.send_header("Content-Type", "text/event-stream; charset=utf-8") self.send_header("Cache-Control", "no-cache") self.send_header("Connection", "keep-alive") self.end_headers() self.wfile.write(b": ok\n\n") self.wfile.flush() order_ids = params.get("order", [None])[0] or "" try: run_pipeline(cid, order_ids, self._sse, build_prompt) except Exception as e: self._sse({"type": "error", "message": str(e)}) def _sse(self, data): self.wfile.write(f"data: {json.dumps(data, ensure_ascii=False)}\n\n".encode()) self.wfile.flush() # ── /upload ─────────────────────────────────────────────────────────── def _handle_upload(self): try: content_type = self.headers.get("Content-Type", "") content_length = int(self.headers.get("Content-Length", 0)) result = handle_upload(self.rfile, content_type, content_length) self._json(result) except Exception as e: self._json({"ok": False, "error": str(e)}, 500) # ── /unzip-upload ───────────────────────────────────────────────────── def _handle_unzip(self): try: content_length = int(self.headers.get("Content-Length", 0)) result = handle_unzip(self.rfile, content_length) self._json(result) except Exception as e: self._json({"ok": False, "error": str(e)}, 500) # ── /llm-ops (debug) ────────────────────────────────────────────────── def _handle_llm_ops(self): from services.llm import call_llm length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) try: result, prompt_id = call_llm( body.get("current_spec", []), body.get("doc_text", ""), build_prompt, ) self._json(result) except Exception as e: self._json({"error": str(e)}, 502) # ── Helpers ─────────────────────────────────────────────────────────── def _json(self, data, status=200): self.send_response(status) self._send_cors() self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() self.wfile.write(json.dumps(data, ensure_ascii=False).encode()) def _send_cors(self): self.send_header("Access-Control-Allow-Origin", "*") def log_message(self, format, *args): pass if __name__ == "__main__": import os port = int(os.environ.get("PORT", "8766")) server = ThreadingHTTPServer(("0.0.0.0", port), Handler) print(f"Contracts VM server on :{port}, db={DB_CONFIG['dbname']}") try: server.serve_forever() except KeyboardInterrupt: server.shutdown()