Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- oev/__init__.py +1 -1
- oev/infer.py +33 -1
- oev/serve.py +25 -0
oev/__init__.py
CHANGED
|
@@ -4,4 +4,4 @@ State + typed questions in, calibrated probability distributions out,
|
|
| 4 |
one forward pass.
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
__version__ = "0.
|
|
|
|
| 4 |
one forward pass.
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
__version__ = "0.3.0"
|
oev/infer.py
CHANGED
|
@@ -8,6 +8,36 @@ from oev.evaluate import load_model
|
|
| 8 |
from oev.tokenizer_hf import HFTokenPacker
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
class OEV:
|
| 12 |
def __init__(self, checkpoint="checkpoints/oev-tiny.pt", device="cpu", temperature=1.0):
|
| 13 |
self.model = load_model(checkpoint, device)
|
|
@@ -21,6 +51,7 @@ class OEV:
|
|
| 21 |
self.packer = HFTokenPacker(self.model.cfg["backbone"])
|
| 22 |
|
| 23 |
def _question(self, name, q):
|
|
|
|
| 24 |
pq = {"name": name, "type": q["type"], "instructions": q.get("instructions", q["type"])}
|
| 25 |
if q["type"] == "choice":
|
| 26 |
pq["options"] = list(q["options"])
|
|
@@ -48,7 +79,8 @@ class OEV:
|
|
| 48 |
|
| 49 |
def decide(self, state, questions, temperature=None):
|
| 50 |
out = {}
|
| 51 |
-
for name,
|
|
|
|
| 52 |
pq = self._question(name, q)
|
| 53 |
probs = self._probs(state, pq, temperature=temperature)
|
| 54 |
best = max(range(len(probs)), key=probs.__getitem__)
|
|
|
|
| 8 |
from oev.tokenizer_hf import HFTokenPacker
|
| 9 |
|
| 10 |
|
| 11 |
+
def normalize_question(name, q):
|
| 12 |
+
"""Accept either question schema and return an OEV-schema copy.
|
| 13 |
+
|
| 14 |
+
Native schema: choice uses a bare "options" list, score uses "levels".
|
| 15 |
+
Jev / TypeSafe schema: choice and noul carry "criteria" as a map of
|
| 16 |
+
option name -> description, score carries "criteria" as a list of level
|
| 17 |
+
labels. Descriptions are ignored (OEV was trained on bare options); the
|
| 18 |
+
option names become the options. Native keys win when both are present.
|
| 19 |
+
"""
|
| 20 |
+
q = dict(q)
|
| 21 |
+
criteria = q.get("criteria")
|
| 22 |
+
if q.get("type") == "choice" and "options" not in q:
|
| 23 |
+
if isinstance(criteria, dict) and criteria:
|
| 24 |
+
q["options"] = [str(option) for option in criteria]
|
| 25 |
+
else:
|
| 26 |
+
raise ValueError(
|
| 27 |
+
f"question {name!r} (choice) needs an 'options' list "
|
| 28 |
+
"or a 'criteria' map of option name -> description"
|
| 29 |
+
)
|
| 30 |
+
elif q.get("type") == "score" and "levels" not in q:
|
| 31 |
+
if isinstance(criteria, list) and criteria:
|
| 32 |
+
q["levels"] = list(criteria)
|
| 33 |
+
else:
|
| 34 |
+
raise ValueError(
|
| 35 |
+
f"question {name!r} (score) needs a 'levels' list "
|
| 36 |
+
"or a 'criteria' list of level labels"
|
| 37 |
+
)
|
| 38 |
+
return q
|
| 39 |
+
|
| 40 |
+
|
| 41 |
class OEV:
|
| 42 |
def __init__(self, checkpoint="checkpoints/oev-tiny.pt", device="cpu", temperature=1.0):
|
| 43 |
self.model = load_model(checkpoint, device)
|
|
|
|
| 51 |
self.packer = HFTokenPacker(self.model.cfg["backbone"])
|
| 52 |
|
| 53 |
def _question(self, name, q):
|
| 54 |
+
q = normalize_question(name, q)
|
| 55 |
pq = {"name": name, "type": q["type"], "instructions": q.get("instructions", q["type"])}
|
| 56 |
if q["type"] == "choice":
|
| 57 |
pq["options"] = list(q["options"])
|
|
|
|
| 79 |
|
| 80 |
def decide(self, state, questions, temperature=None):
|
| 81 |
out = {}
|
| 82 |
+
for name, raw_question in questions.items():
|
| 83 |
+
q = normalize_question(name, raw_question)
|
| 84 |
pq = self._question(name, q)
|
| 85 |
probs = self._probs(state, pq, temperature=temperature)
|
| 86 |
best = max(range(len(probs)), key=probs.__getitem__)
|
oev/serve.py
CHANGED
|
@@ -15,10 +15,13 @@ Usage:
|
|
| 15 |
}'
|
| 16 |
|
| 17 |
# Jev-compatible schema (drop-in for existing TypeSafe clients):
|
|
|
|
| 18 |
curl -X POST localhost:8000/v1/systemone -H "Content-Type: application/json" -d '{
|
| 19 |
"model": "oev",
|
| 20 |
"state": "We were charged twice for the same order.",
|
| 21 |
"questions": {
|
|
|
|
|
|
|
| 22 |
"refund_requested": {"type": "noul", "instructions": "Does the user request a refund?"}
|
| 23 |
}
|
| 24 |
}'
|
|
@@ -91,11 +94,33 @@ def _jevify(answers: dict) -> dict:
|
|
| 91 |
return out
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
@app.post("/v1/systemone")
|
| 95 |
def systemone(req: SystemOneRequest):
|
| 96 |
"""Jev-compatible endpoint: existing TypeSafe clients work by changing baseUrl."""
|
| 97 |
if agent is None:
|
| 98 |
raise HTTPException(status_code=503, detail="no checkpoint loaded - run oev-serve --checkpoint ...")
|
|
|
|
|
|
|
|
|
|
| 99 |
answers = agent.decide(req.state, req.questions)
|
| 100 |
return {
|
| 101 |
"model": req.model,
|
|
|
|
| 15 |
}'
|
| 16 |
|
| 17 |
# Jev-compatible schema (drop-in for existing TypeSafe clients):
|
| 18 |
+
# questions accept the same "criteria" shape laya and Kev serve.
|
| 19 |
curl -X POST localhost:8000/v1/systemone -H "Content-Type: application/json" -d '{
|
| 20 |
"model": "oev",
|
| 21 |
"state": "We were charged twice for the same order.",
|
| 22 |
"questions": {
|
| 23 |
+
"department": {"type": "choice", "instructions": "Which department?",
|
| 24 |
+
"criteria": {"billing": "payments and refunds", "technical": "bugs and outages"}},
|
| 25 |
"refund_requested": {"type": "noul", "instructions": "Does the user request a refund?"}
|
| 26 |
}
|
| 27 |
}'
|
|
|
|
| 94 |
return out
|
| 95 |
|
| 96 |
|
| 97 |
+
def _validate_jev_questions(questions: dict) -> str | None:
|
| 98 |
+
"""Return an error message for questions that do not match the Jev schema."""
|
| 99 |
+
for name, question in questions.items():
|
| 100 |
+
if not isinstance(question, dict):
|
| 101 |
+
return f"question {name} must be an object"
|
| 102 |
+
qtype = question.get("type")
|
| 103 |
+
if qtype not in {"choice", "noul", "score"}:
|
| 104 |
+
return f"question {name} has unsupported type"
|
| 105 |
+
if qtype in {"choice", "noul"}:
|
| 106 |
+
criteria = question.get("criteria")
|
| 107 |
+
if criteria is not None and (not isinstance(criteria, dict) or not criteria):
|
| 108 |
+
return f"question {name} ({qtype}) criteria must be a non-empty object"
|
| 109 |
+
else:
|
| 110 |
+
criteria = question.get("criteria")
|
| 111 |
+
if not isinstance(criteria, list) or not criteria:
|
| 112 |
+
return f"question {name} (score) needs a non-empty criteria list of level labels"
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
|
| 116 |
@app.post("/v1/systemone")
|
| 117 |
def systemone(req: SystemOneRequest):
|
| 118 |
"""Jev-compatible endpoint: existing TypeSafe clients work by changing baseUrl."""
|
| 119 |
if agent is None:
|
| 120 |
raise HTTPException(status_code=503, detail="no checkpoint loaded - run oev-serve --checkpoint ...")
|
| 121 |
+
error = _validate_jev_questions(req.questions)
|
| 122 |
+
if error:
|
| 123 |
+
raise HTTPException(status_code=422, detail=error)
|
| 124 |
answers = agent.decide(req.state, req.questions)
|
| 125 |
return {
|
| 126 |
"model": req.model,
|