HumboldtJoker commited on
Commit
e2921fc
·
verified ·
1 Parent(s): 4a7cf3e

Move scaffold/scaffold/rivet_serve.py -> scaffold/rivet_serve.py

Browse files
Files changed (1) hide show
  1. scaffold/rivet_serve.py +184 -0
scaffold/rivet_serve.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rivet Server — the code assistant endpoint.
2
+
3
+ Wraps Ollama with the discipline gate and context loader.
4
+ Faculty hit this endpoint; Rivet responds with architecture-aware,
5
+ discipline-gated suggestions.
6
+
7
+ Usage:
8
+ python rivet_serve.py # Start server on port 8100
9
+ python rivet_serve.py --port 8200 # Custom port
10
+ python rivet_serve.py --model qwen3.5:27b # Use fallback model
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import time
16
+ from http.server import HTTPServer, BaseHTTPRequestHandler
17
+
18
+ import requests
19
+
20
+ from context_loader import load_context
21
+ from discipline_gate import run_gate, GateResult
22
+
23
+
24
+ # Config
25
+ OLLAMA_URL = "http://localhost:11434"
26
+ DEFAULT_MODEL = "rivet"
27
+ FALLBACK_MODEL = "qwen3.5:27b"
28
+
29
+ # Load context once at startup
30
+ print("Loading Rivet context...", flush=True)
31
+ CONTEXT = load_context()
32
+ SYSTEM_PROMPT = CONTEXT.to_system_context() + """
33
+
34
+ ---
35
+
36
+ # YOUR ROLE
37
+
38
+ You are Rivet, a senior engineer embedded with the Multiverse Campus team.
39
+
40
+ ## Rules
41
+ 1. Never suggest a destructive migration. Staging and prod share the database.
42
+ 2. Every code suggestion includes: what it changes, what it could break, and what tests verify it.
43
+ 3. State your confidence level: HIGH (traced the full path), MEDIUM (read the code), LOW (reasoning from architecture).
44
+ 4. Auth changes require explicit callout: "This touches authentication. Review with security before merging."
45
+ 5. Flag known vulnerability patterns proactively.
46
+ 6. You are a colleague, not the lead. Suggest, don't decree.
47
+ 7. If you cannot verify your suggestion compiles, say so.
48
+ """
49
+
50
+ print(f"Context loaded: ~{CONTEXT.token_estimate} tokens", flush=True)
51
+
52
+
53
+ def query_ollama(prompt: str, model: str = DEFAULT_MODEL) -> str:
54
+ """Send a prompt to Ollama and get the response."""
55
+ try:
56
+ resp = requests.post(
57
+ f"{OLLAMA_URL}/api/generate",
58
+ json={
59
+ "model": model,
60
+ "prompt": prompt,
61
+ "system": SYSTEM_PROMPT,
62
+ "stream": False,
63
+ "options": {
64
+ "temperature": 0.3,
65
+ "top_p": 0.9,
66
+ "num_ctx": 32768,
67
+ },
68
+ },
69
+ timeout=120,
70
+ )
71
+ resp.raise_for_status()
72
+ return resp.json().get("response", "")
73
+ except requests.exceptions.ConnectionError:
74
+ return f"ERROR: Cannot connect to Ollama at {OLLAMA_URL}. Is it running?"
75
+ except requests.exceptions.Timeout:
76
+ return "ERROR: Ollama request timed out (120s). Try a shorter question or check the model."
77
+ except Exception as e:
78
+ return f"ERROR: {e}"
79
+
80
+
81
+ class RivetHandler(BaseHTTPRequestHandler):
82
+ model = DEFAULT_MODEL
83
+
84
+ def do_POST(self):
85
+ if self.path == "/ask":
86
+ content_length = int(self.headers.get("Content-Length", 0))
87
+ body = json.loads(self.rfile.read(content_length))
88
+ question = body.get("question", "")
89
+ user = body.get("user", "anonymous")
90
+
91
+ t0 = time.time()
92
+
93
+ # Get response from model
94
+ response = query_ollama(question, model=self.model)
95
+
96
+ # Run discipline gate on the response
97
+ gate_result = run_gate(response, context=question)
98
+ gate_warnings = gate_result.format_warnings()
99
+
100
+ # Compose final response
101
+ final_response = response
102
+ if gate_warnings:
103
+ final_response = gate_warnings + "\n\n---\n\n" + response
104
+ if not gate_result.passed:
105
+ final_response = (
106
+ "🛑 **BLOCKED by Discipline Gate**\n\n"
107
+ "The suggested approach was blocked for safety reasons:\n"
108
+ + gate_warnings
109
+ + "\n\nPlease rephrase your request or ask for a safe alternative."
110
+ )
111
+
112
+ elapsed = time.time() - t0
113
+
114
+ result = {
115
+ "response": final_response,
116
+ "gate_passed": gate_result.passed,
117
+ "flags": gate_result.flags,
118
+ "confidence": gate_result.confidence.value,
119
+ "user": user,
120
+ "elapsed_seconds": round(elapsed, 1),
121
+ }
122
+
123
+ self.send_response(200)
124
+ self.send_header("Content-Type", "application/json")
125
+ self.end_headers()
126
+ self.wfile.write(json.dumps(result).encode())
127
+
128
+ elif self.path == "/health":
129
+ self.send_response(200)
130
+ self.send_header("Content-Type", "application/json")
131
+ self.end_headers()
132
+ self.wfile.write(json.dumps({
133
+ "status": "ok",
134
+ "model": self.model,
135
+ "context_tokens": CONTEXT.token_estimate,
136
+ }).encode())
137
+
138
+ else:
139
+ self.send_response(404)
140
+ self.end_headers()
141
+
142
+ def do_GET(self):
143
+ if self.path == "/health":
144
+ self.send_response(200)
145
+ self.send_header("Content-Type", "application/json")
146
+ self.end_headers()
147
+ self.wfile.write(json.dumps({
148
+ "status": "ok",
149
+ "model": self.model,
150
+ "context_tokens": CONTEXT.token_estimate,
151
+ }).encode())
152
+ else:
153
+ self.send_response(404)
154
+ self.end_headers()
155
+
156
+ def log_message(self, format, *args):
157
+ print(f"[rivet] {args[0]}", flush=True)
158
+
159
+
160
+ def main():
161
+ parser = argparse.ArgumentParser(description="Rivet Code Assistant Server")
162
+ parser.add_argument("--port", type=int, default=8100)
163
+ parser.add_argument("--model", default=DEFAULT_MODEL)
164
+ args = parser.parse_args()
165
+
166
+ RivetHandler.model = args.model
167
+
168
+ server = HTTPServer(("0.0.0.0", args.port), RivetHandler)
169
+ print(f"\n🔩 Rivet listening on port {args.port}", flush=True)
170
+ print(f" Model: {args.model}", flush=True)
171
+ print(f" Context: ~{CONTEXT.token_estimate} tokens loaded", flush=True)
172
+ print(f" POST /ask {{\"question\": \"...\", \"user\": \"...\"}}", flush=True)
173
+ print(f" GET /health", flush=True)
174
+ print(flush=True)
175
+
176
+ try:
177
+ server.serve_forever()
178
+ except KeyboardInterrupt:
179
+ print("\n🔩 Rivet shutting down.", flush=True)
180
+ server.shutdown()
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()