File size: 4,284 Bytes
105f9ef
 
 
 
 
 
 
 
d144fee
 
 
 
105f9ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
"""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