"""Solomon HTTP surface for the Solomon layer (solomon/service.py). Named api.py, not http.py, so that running a script from inside Solomon/ can never shadow the standard-library http package. GET /health contract, readout mode, serving binding, runtime identity POST /states {"state": text | object | parts} -> {"state_id", ...} POST /v1/decide {"state" | "state_id", "questions", ...} -> {"answers", "usage", ...} POST /states//decide {"questions", ...} -> same, reusing the saved state v1.1: every question is answered. A decide response carries probabilities (one per candidate for multi-label questions) and an `ordering_score`. The entity answer type was removed: ask one yes/no question per candidate, or send the candidates as labels; an old entity request is answered 400 with that instruction. Evidence spans name the selector that produced them (`evidence_method`: 'trained_relevance_head_ranked' -- experimental ranked pointers with scores -- or the labelled 'lexical_overlap_fallback'); it never carries `abstain`, `confidence`, a threshold or anything that reads as a certified error rate -- `solomon.service.decide` refuses to emit such a field. `ordering_score` is uncalibrated and rank-only; `ordering_score_semantics` says so in every response and on /health. """ import json from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer DECIDE_KEYS = {'state', 'state_id', 'questions', 'population', 'evidence', 'evidence_max_calls', 'evidence_detail', 'budget', 'model'} def handler(service): class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def reply(self, status, data): raw = json.dumps(data).encode() self.send_response(status) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(raw))) self.end_headers() self.wfile.write(raw) def do_GET(self): if self.path != '/health': return self.reply(404, {'error': 'not found'}) return self.reply(200, service.health()) def do_POST(self): try: if self.headers.get('Origin'): return self.reply(403, {'error': 'cross-origin requests unsupported'}) length = int(self.headers.get('Content-Length', '0')) if not 0 < length <= 8_000_000: raise ValueError('request body must be 1 to 8000000 bytes') body = json.loads(self.rfile.read(length)) if not isinstance(body, dict): raise ValueError('request body must be a JSON object') path = self.path.strip('/').split('/') if path == ['states']: return self.reply(201, service.create(body.get('state', body.get('document')))) if path == ['v1', 'decide'] or (len(path) == 3 and path[0] == 'states' and path[2] == 'decide'): unknown = set(body) - DECIDE_KEYS if unknown: raise ValueError('unknown request fields: ' + ', '.join(sorted(unknown))) body.pop('model', None) # some clients send a model name; one model is served here, so it is accepted and ignored if len(path) == 3: if 'state' in body or 'state_id' in body: raise ValueError('state is given by the URL') body['state_id'] = path[1] return self.reply(200, service.decide(**body)) return self.reply(404, {'error': 'not found'}) except (ValueError, KeyError, TypeError) as exc: return self.reply(400, {'error': str(exc)}) except FileNotFoundError: return self.reply(404, {'error': 'state or input file not found'}) return Handler def serve(service, host='127.0.0.1', port=0): """Start a ThreadingHTTPServer (caller runs serve_forever / shutdown).""" server = ThreadingHTTPServer((host, port), handler(service)) server.service = service return server