fev-4b / fev4b /decisions.py
murabcd's picture
feat: expose typed fev decisions and system one api
f843101 verified
Raw History Blame Contribute Delete
5.46 kB
"""Text-only typed decisions. No model imports or customer defaults."""
import copy
import math
MODEL_ID = 'fev-4b'
def _json(value, depth=0):
if depth > 32:
raise ValueError('JSON nesting exceeds 32 levels')
if value is None or isinstance(value, (str, bool, int)):
return
if isinstance(value, float) and math.isfinite(value):
return
if isinstance(value, list):
for item in value:
_json(item, depth + 1)
return
if isinstance(value, dict) and all(isinstance(key, str) for key in value):
for item in value.values():
_json(item, depth + 1)
return
raise ValueError('Content must be finite JSON')
def _content(value, *, nullable=False):
if value is None and nullable:
return
if not isinstance(value, (str, dict, list)):
raise ValueError('Content must be a string, object or array')
_json(value)
def validate_request(request):
"""Validate without changing instructions, state, keys or option order."""
if not isinstance(request, dict) or set(request) != {'state', 'questions'}:
raise ValueError('Expected exactly state and questions; images and assets are unsupported')
_content(request['state'])
questions = request['questions']
if not isinstance(questions, dict) or not 1 <= len(questions) <= 64:
raise ValueError('Supply 1 to 64 questions')
for key, question in questions.items():
if not isinstance(key, str) or not key:
raise ValueError('Question IDs must be nonempty strings')
if not isinstance(question, dict) or set(question) - {'type', 'instructions', 'criteria'}:
raise ValueError('Unsupported question fields')
kind = question.get('type')
if kind not in ('choice', 'noul', 'score'):
raise ValueError('Supported question types: choice, noul, score')
_content(question.get('instructions'), nullable=True)
criteria = question.get('criteria')
if kind == 'choice':
if not isinstance(criteria, dict) or not 1 <= len(criteria) <= 255:
raise ValueError('Choice needs 1 to 255 options')
if any(not isinstance(option, str) or not option for option in criteria):
raise ValueError('Choice option keys must be nonempty strings')
elif kind == 'noul':
if criteria is not None and (not isinstance(criteria, dict) or set(criteria) - {'true', 'false'}):
raise ValueError('Noul criteria may contain only true and false')
elif not isinstance(criteria, list) or not 2 <= len(criteria) <= 255:
raise ValueError('Score needs 2 to 255 ordered levels')
descriptions = criteria.values() if isinstance(criteria, dict) else (criteria or [])
for description in descriptions:
_content(description, nullable=kind != 'score')
return copy.deepcopy(request)
def _number(value, lower, upper):
return (type(value) in (int, float) and math.isfinite(value) and lower <= value <= upper)
def validate_response(request, response):
"""Reject incomplete/non-finite native outputs rather than inventing answers."""
answers = response.get('answers')
if not isinstance(answers, dict) or set(answers) != set(request['questions']):
raise RuntimeError('Incomplete typed decision response')
for key, question in request['questions'].items():
answer = answers[key]
kind = question['type']
if answer.get('type') != kind:
raise RuntimeError('Native answer type mismatch')
if kind == 'noul':
if not _number(answer.get('noul'), 0, 1):
raise RuntimeError('Invalid Noul probability')
continue
keys = list(question['criteria']) if kind == 'choice' else [str(i) for i in range(len(question['criteria']))]
probabilities = answer.get('probabilities')
if (not isinstance(probabilities, dict) or set(probabilities) != set(keys)
or any(not _number(p, 0, 1) for p in probabilities.values())
or not math.isclose(sum(probabilities.values()), 1, abs_tol=1e-5)
or not _number(answer.get('confidence'), 0, 1)):
raise RuntimeError('Invalid typed probability distribution')
if kind == 'choice':
if answer.get('choice') not in keys or probabilities[answer['choice']] != max(probabilities.values()):
raise RuntimeError('Choice does not match distribution')
else:
expected_score = sum(int(k) * p for k, p in probabilities.items())
if (not _number(answer.get('score'), 0, len(keys) - 1)
or not math.isclose(answer['score'], expected_score, abs_tol=1e-5)
or answer.get('legend') != dict(zip(keys, question['criteria']))):
raise RuntimeError('Score does not match distribution or legend')
usage = response.get('usage', {})
if (type(usage.get('input_tokens')) is not int or usage['input_tokens'] < 1
or usage.get('output_tokens') != 0 or usage.get('sequences') != 1):
raise RuntimeError('Typed decisions must use one sequence and generate zero tokens')
return response
def decide_with_predict(request, *, predict):
request = validate_request(request)
response = validate_response(request, predict(request))
return {'model': MODEL_ID, **response}