| """Rivet Server — the code assistant endpoint. |
| |
| Wraps Ollama with the discipline gate and context loader. |
| Faculty hit this endpoint; Rivet responds with architecture-aware, |
| discipline-gated suggestions. |
| |
| Usage: |
| python rivet_serve.py # Start server on port 8100 |
| python rivet_serve.py --port 8200 # Custom port |
| python rivet_serve.py --model qwen3.5:27b # Use fallback model |
| """ |
|
|
| import argparse |
| import json |
| import time |
| from http.server import HTTPServer, BaseHTTPRequestHandler |
|
|
| import requests |
|
|
| from context_loader import load_context |
| from discipline_gate import run_gate, GateResult |
|
|
|
|
| |
| OLLAMA_URL = "http://localhost:11434" |
| DEFAULT_MODEL = "rivet" |
| FALLBACK_MODEL = "qwen3.5:27b" |
|
|
| |
| print("Loading Rivet context...", flush=True) |
| CONTEXT = load_context() |
| SYSTEM_PROMPT = CONTEXT.to_system_context() + """ |
| |
| --- |
| |
| # YOUR ROLE |
| |
| You are Rivet, a senior engineer embedded with the Multiverse Campus team. |
| |
| ## Rules |
| 1. Never suggest a destructive migration. Staging and prod share the database. |
| 2. Every code suggestion includes: what it changes, what it could break, and what tests verify it. |
| 3. State your confidence level: HIGH (traced the full path), MEDIUM (read the code), LOW (reasoning from architecture). |
| 4. Auth changes require explicit callout: "This touches authentication. Review with security before merging." |
| 5. Flag known vulnerability patterns proactively. |
| 6. You are a colleague, not the lead. Suggest, don't decree. |
| 7. If you cannot verify your suggestion compiles, say so. |
| """ |
|
|
| print(f"Context loaded: ~{CONTEXT.token_estimate} tokens", flush=True) |
|
|
|
|
| def query_ollama(prompt: str, model: str = DEFAULT_MODEL) -> str: |
| """Send a prompt to Ollama and get the response.""" |
| try: |
| resp = requests.post( |
| f"{OLLAMA_URL}/api/generate", |
| json={ |
| "model": model, |
| "prompt": prompt, |
| "system": SYSTEM_PROMPT, |
| "stream": False, |
| "options": { |
| "temperature": 0.3, |
| "top_p": 0.9, |
| "num_ctx": 32768, |
| }, |
| }, |
| timeout=120, |
| ) |
| resp.raise_for_status() |
| return resp.json().get("response", "") |
| except requests.exceptions.ConnectionError: |
| return f"ERROR: Cannot connect to Ollama at {OLLAMA_URL}. Is it running?" |
| except requests.exceptions.Timeout: |
| return "ERROR: Ollama request timed out (120s). Try a shorter question or check the model." |
| except Exception as e: |
| return f"ERROR: {e}" |
|
|
|
|
| class RivetHandler(BaseHTTPRequestHandler): |
| model = DEFAULT_MODEL |
|
|
| def do_POST(self): |
| if self.path == "/ask": |
| content_length = int(self.headers.get("Content-Length", 0)) |
| body = json.loads(self.rfile.read(content_length)) |
| question = body.get("question", "") |
| user = body.get("user", "anonymous") |
|
|
| t0 = time.time() |
|
|
| |
| response = query_ollama(question, model=self.model) |
|
|
| |
| gate_result = run_gate(response, context=question) |
| gate_warnings = gate_result.format_warnings() |
|
|
| |
| final_response = response |
| if gate_warnings: |
| final_response = gate_warnings + "\n\n---\n\n" + response |
| if not gate_result.passed: |
| final_response = ( |
| "🛑 **BLOCKED by Discipline Gate**\n\n" |
| "The suggested approach was blocked for safety reasons:\n" |
| + gate_warnings |
| + "\n\nPlease rephrase your request or ask for a safe alternative." |
| ) |
|
|
| elapsed = time.time() - t0 |
|
|
| result = { |
| "response": final_response, |
| "gate_passed": gate_result.passed, |
| "flags": gate_result.flags, |
| "confidence": gate_result.confidence.value, |
| "user": user, |
| "elapsed_seconds": round(elapsed, 1), |
| } |
|
|
| self.send_response(200) |
| self.send_header("Content-Type", "application/json") |
| self.end_headers() |
| self.wfile.write(json.dumps(result).encode()) |
|
|
| elif self.path == "/health": |
| self.send_response(200) |
| self.send_header("Content-Type", "application/json") |
| self.end_headers() |
| self.wfile.write(json.dumps({ |
| "status": "ok", |
| "model": self.model, |
| "context_tokens": CONTEXT.token_estimate, |
| }).encode()) |
|
|
| else: |
| self.send_response(404) |
| self.end_headers() |
|
|
| def do_GET(self): |
| if self.path == "/health": |
| self.send_response(200) |
| self.send_header("Content-Type", "application/json") |
| self.end_headers() |
| self.wfile.write(json.dumps({ |
| "status": "ok", |
| "model": self.model, |
| "context_tokens": CONTEXT.token_estimate, |
| }).encode()) |
| else: |
| self.send_response(404) |
| self.end_headers() |
|
|
| def log_message(self, format, *args): |
| print(f"[rivet] {args[0]}", flush=True) |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Rivet Code Assistant Server") |
| parser.add_argument("--port", type=int, default=8100) |
| parser.add_argument("--model", default=DEFAULT_MODEL) |
| args = parser.parse_args() |
|
|
| RivetHandler.model = args.model |
|
|
| server = HTTPServer(("0.0.0.0", args.port), RivetHandler) |
| print(f"\n🔩 Rivet listening on port {args.port}", flush=True) |
| print(f" Model: {args.model}", flush=True) |
| print(f" Context: ~{CONTEXT.token_estimate} tokens loaded", flush=True) |
| print(f" POST /ask {{\"question\": \"...\", \"user\": \"...\"}}", flush=True) |
| print(f" GET /health", flush=True) |
| print(flush=True) |
|
|
| try: |
| server.serve_forever() |
| except KeyboardInterrupt: |
| print("\n🔩 Rivet shutting down.", flush=True) |
| server.shutdown() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|