botp
/

Solomon / src /solomon /api.py
orz99's picture ArcherHume's picture
Duplicate from DoccyHealth/Solomon
1d2de8a
Raw
History Blame Contribute Delete
4.28 kB
"""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/<state_id>/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