mirror of
https://github.com/mcloh/decision-models-benchmark.git
synced 2026-11-05 22:04:12 +00:00
setup models
This commit is contained in:
71
tests/test_canonical.py
Normal file
71
tests/test_canonical.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from dmb.canonical import fit_state, serialize_state, to_jev_request
|
||||
|
||||
STATE = {
|
||||
"active_agent": "agente_a",
|
||||
"suspended_sessions": ["agente_b"],
|
||||
"recent_turns": [
|
||||
{"role": "user", "text": "primeiro turno"},
|
||||
{"role": "agent", "agent_id": "agente_a", "text": "resposta do agente"},
|
||||
{"role": "user", "text": "segundo turno"},
|
||||
],
|
||||
}
|
||||
EXAMPLE = {
|
||||
"id": "conv-1-t3-D2", "group_id": "conv-1", "task": "D2", "state": STATE,
|
||||
"utterance": "pode ser o segundo",
|
||||
"question": "Como o orquestrador deve tratar este turno?", "question_type": "choice",
|
||||
"options": {"continuar_agente_ativo": "Continua a tarefa em curso",
|
||||
"retomar_sessao:agente_b": "Volta ao assunto do agente_b",
|
||||
"iniciar_novo_agente": "Intenção nova",
|
||||
"desambiguar": "Não dá para decidir"},
|
||||
"gold_label": "continuar_agente_ativo",
|
||||
}
|
||||
|
||||
|
||||
def words(text):
|
||||
return len(text.split())
|
||||
|
||||
|
||||
def test_serialization_is_deterministic_and_ends_with_utterance():
|
||||
text = serialize_state(STATE, "pode ser o segundo", 2)
|
||||
assert text == serialize_state(STATE, "pode ser o segundo", 2)
|
||||
assert text.endswith("Mensagem: pode ser o segundo")
|
||||
assert "primeiro turno" not in text and "segundo turno" in text
|
||||
assert "Agente ativo: agente_a" in text
|
||||
|
||||
|
||||
def test_zero_turns_has_no_history():
|
||||
assert "Turnos anteriores" not in serialize_state(STATE, "x", 0)
|
||||
|
||||
|
||||
def test_fit_drops_oldest_turns_first():
|
||||
full = words(serialize_state(STATE, "pode ser o segundo", 3))
|
||||
text, info = fit_state(STATE, "pode ser o segundo", 3, full - 1, words)
|
||||
assert info["turns_used"] < 3 and info["truncated"]
|
||||
assert text.endswith("Mensagem: pode ser o segundo")
|
||||
|
||||
|
||||
def test_fit_cuts_utterance_tail_only_as_last_resort():
|
||||
long = "palavra " * 200
|
||||
text, info = fit_state(STATE, long, 2, 30, words)
|
||||
assert info["turns_used"] == 0 and info["utterance_chars_kept"] < len(long)
|
||||
assert words(text) <= 30
|
||||
|
||||
|
||||
def test_to_jev_request_keeps_option_order():
|
||||
request, info = to_jev_request(EXAMPLE, "m", n_turns=2)
|
||||
q = request["questions"]["D2"]
|
||||
assert list(q["criteria"]) == list(EXAMPLE["options"])
|
||||
assert q["type"] == "choice" and info["serialization"] == "state-text-v1"
|
||||
|
||||
|
||||
def test_multi_question_example():
|
||||
example = dict(EXAMPLE)
|
||||
for key in ("question", "question_type", "options", "gold_label"):
|
||||
example.pop(key)
|
||||
clara = {"true": "sim", "false": "não"}
|
||||
example["questions"] = [
|
||||
{"name": "pede_x", "question": "Pede X?", "question_type": "noul", "options": clara, "gold_label": "true"},
|
||||
{"name": "pede_y", "question": "Pede Y?", "question_type": "noul", "options": clara, "gold_label": "false"},
|
||||
]
|
||||
request, _ = to_jev_request(example, "m", n_turns=0)
|
||||
assert list(request["questions"]) == ["pede_x", "pede_y"]
|
||||
53
tests/test_contract.py
Normal file
53
tests/test_contract.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dmb import jev
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parents[1] / "contracts" / "jev" / "fixtures"
|
||||
CASES = json.loads((FIXTURES / "conformance.json").read_text())["cases"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES])
|
||||
def test_request_fixtures(case):
|
||||
if case["expect"] == "ok":
|
||||
jev.validate_request(case["request"])
|
||||
else:
|
||||
with pytest.raises(jev.InvalidRequest):
|
||||
jev.validate_request(case["request"])
|
||||
|
||||
|
||||
def test_normalize_keeps_declared_order_and_floor():
|
||||
p = jev.normalize({"b": 0.0, "a": 1.0}, ["a", "b"])
|
||||
assert list(p) == ["a", "b"]
|
||||
assert p["b"] > 0 and abs(sum(p.values()) - 1) < 1e-12
|
||||
|
||||
|
||||
def test_normalize_rejects_unknown_option():
|
||||
with pytest.raises(jev.UnknownOption):
|
||||
jev.normalize({"a": 0.5, "z": 0.5}, ["a", "b"])
|
||||
|
||||
|
||||
def test_answers_match_response_schema():
|
||||
choice_q = {"type": "choice", "instructions": "?", "criteria": {"a": "A", "b": "B"}}
|
||||
score_q = {"type": "score", "instructions": "?", "criteria": ["baixa", "alta"]}
|
||||
response = {
|
||||
"model": "m",
|
||||
"answers": {
|
||||
"c": jev.choice_answer({"a": 0.2, "b": 0.8}, choice_q),
|
||||
"n": jev.noul_answer(1.0),
|
||||
"s": jev.score_answer({"0": 0.25, "1": 0.75}, score_q),
|
||||
},
|
||||
"usage": {"input_tokens": 3, "output_tokens": 0},
|
||||
}
|
||||
assert jev.response_errors(response) == []
|
||||
assert response["answers"]["c"]["choice"] == "b"
|
||||
assert response["answers"]["s"]["score"] == pytest.approx(0.75)
|
||||
assert response["answers"]["n"]["noul"] < 1
|
||||
|
||||
|
||||
def test_output_tokens_must_be_zero():
|
||||
response = {"model": "m", "answers": {"n": jev.noul_answer(0.5)},
|
||||
"usage": {"input_tokens": 1, "output_tokens": 5}}
|
||||
assert jev.response_errors(response)
|
||||
35
tests/test_offline.py
Normal file
35
tests/test_offline.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from dmb.offline import NetworkAccessBlocked, enforce_offline, self_test
|
||||
|
||||
|
||||
def test_external_connection_is_blocked():
|
||||
enforce_offline()
|
||||
assert os.environ["HF_HUB_OFFLINE"] == "1"
|
||||
s = socket.socket()
|
||||
try:
|
||||
with pytest.raises(NetworkAccessBlocked):
|
||||
s.connect(("93.184.216.34", 443))
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def test_loopback_is_allowed():
|
||||
enforce_offline()
|
||||
server = socket.socket()
|
||||
server.bind(("127.0.0.1", 0))
|
||||
server.listen(1)
|
||||
client = socket.socket()
|
||||
try:
|
||||
client.connect(server.getsockname())
|
||||
finally:
|
||||
client.close()
|
||||
server.close()
|
||||
|
||||
|
||||
def test_self_test_reports_guard():
|
||||
enforce_offline()
|
||||
assert self_test()["process_guard"].startswith("ok")
|
||||
85
tests/test_server.py
Normal file
85
tests/test_server.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from dmb.adapter import Adapter
|
||||
from dmb.jev import answer_from_distribution, option_descriptions, response_errors
|
||||
from dmb.server import create_app
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parents[1] / "contracts" / "jev" / "fixtures"
|
||||
CASES = json.loads((FIXTURES / "conformance.json").read_text())["cases"]
|
||||
|
||||
|
||||
class FakeAdapter(Adapter):
|
||||
"""Escolhe a opção cuja descrição compartilha mais palavras com o estado."""
|
||||
name = "fake"
|
||||
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def native_context_limit(self):
|
||||
return 10_000
|
||||
|
||||
def count_tokens(self, text):
|
||||
return len(text.split())
|
||||
|
||||
def decide(self, state, questions):
|
||||
words = set(str(state).lower().split())
|
||||
answers = {}
|
||||
for name, q in questions.items():
|
||||
scores = {k: 1 + len(words & set(d.lower().split())) for k, d in option_descriptions(q).items()}
|
||||
total = sum(scores.values())
|
||||
answers[name] = answer_from_distribution(q, {k: v / total for k, v in scores.items()})
|
||||
return answers, self.count_tokens(str(state))
|
||||
|
||||
|
||||
class BrokenAdapter(FakeAdapter):
|
||||
def decide(self, state, questions):
|
||||
answers, n = super().decide(state, questions)
|
||||
for a in answers.values():
|
||||
if "probabilities" in a:
|
||||
a["probabilities"] = dict(reversed(list(a["probabilities"].items())))
|
||||
return answers, n
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
with TestClient(create_app(FakeAdapter("unused", "0" * 40))) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES])
|
||||
def test_conformance(client, case):
|
||||
r = client.post("/v1/systemone", json=case["request"])
|
||||
if case["expect"] == "ok":
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert response_errors(body) == []
|
||||
assert list(body["answers"]) == list(case["request"]["questions"])
|
||||
for name, q in case["request"]["questions"].items():
|
||||
if q["type"] == "choice":
|
||||
assert list(body["answers"][name]["probabilities"]) == list(q["criteria"])
|
||||
assert body["usage"]["output_tokens"] == 0
|
||||
assert {"latency_ms", "model_id", "run_id", "adapter_version"} <= body["metadata"].keys()
|
||||
else:
|
||||
assert r.status_code == 422
|
||||
assert r.json()["detail"]["error_type"] == "invalid_request"
|
||||
|
||||
|
||||
def test_malformed_json(client):
|
||||
r = client.post("/v1/systemone", content=b"{nope", headers={"content-type": "application/json"})
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_reordered_options_are_rejected():
|
||||
with TestClient(create_app(BrokenAdapter("unused", "0" * 40))) as c:
|
||||
r = c.post("/v1/systemone", json=CASES[0]["request"])
|
||||
assert r.status_code == 502
|
||||
assert r.json()["detail"]["error_type"] == "unknown_option"
|
||||
|
||||
|
||||
def test_health(client):
|
||||
body = client.get("/health").json()
|
||||
assert body["status"] == "ready" and body["name"] == "fake"
|
||||
Reference in New Issue
Block a user