mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-08 04:00:15 +00:00
* chore: ignore .worktrees directory * feat(evals): add eval metric and result models * feat(evals): add evaluator registry * feat(evals): add exact/regex/range/length evaluators * feat(evals): add keyword/levenshtein/rouge/llm-as-judge evaluators * feat(evals): add decision_scores composite evaluator * feat(evals): add evaluation runner * feat(evals): expose public API and module proxy * fix(evals): resolve __all__ names and repair usage example * docs(evals): add usage docs and changelog entry * style(evals): tidy evaluator metadata and wiring comments * fix(evals): honor expected arg and classify error metrics * fix(evals): export get_evaluator and fix shared meta default * fix(evals): guard provenance check against non-dict metadata * docs: add objective layer design spec for semantica.evals * docs: refine objective spec for consistency with AIP Evals semantics * docs: add implementation plan for evals objective layer * docs: fix plan tests to use module-level pytest import * feat(evals): add per-metric objective support to runner * docs(evals): document per-metric objectives * docs(evals): fix minimize example threshold to demonstrate pass * fix(evals): validate objective config shape strictly * docs(evals): clarify objective examples and Boolean semantics * fix(evals): honor direction-only minimize, fail fast on objectives, deep-merge case config - minimize without threshold is now a no-op, matching maximize (issue #1091 requires thresholds to be optional for both directions) - objective config is parsed for every case before any target_fn/evaluator runs, so an invalid per-case objective rejects the run up front - per-case evaluator config deep-merges over the global config so a case that overrides one setting keeps the run-level objective - regression tests for all three, plus updated docs/CHANGELOG Addresses 3 of 4 Qodo findings on #1092 (the 4th, 'result models defined twice', is a false positive: types live in types.py) * fix: finalize eval objectives review --------- Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
"""Tests for the decision_scores composite evaluator."""
|
|
import pytest
|
|
from datetime import datetime
|
|
|
|
from semantica.context.decision_models import Decision
|
|
from semantica.evals import registry as reg
|
|
|
|
|
|
def _decision(**overrides):
|
|
base = dict(
|
|
decision_id="d1",
|
|
category="loan",
|
|
scenario="mortgage application",
|
|
reasoning="strong credit history",
|
|
outcome="approved",
|
|
confidence=0.95,
|
|
timestamp=datetime(2026, 1, 1),
|
|
decision_maker="loan_officer",
|
|
)
|
|
base.update(overrides)
|
|
return Decision(**base)
|
|
|
|
|
|
class TestDecisionScores:
|
|
def test_full_pass(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(
|
|
d, config={"expected_outcome": "approved"}
|
|
)
|
|
assert r.passed
|
|
assert r.meta["decision_outcome"] is True
|
|
assert r.meta["provenance"] is True
|
|
|
|
def test_outcome_mismatch(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(
|
|
d, config={"expected_outcome": "denied"}
|
|
)
|
|
assert not r.passed
|
|
assert r.meta["decision_outcome"] is False
|
|
|
|
def test_outcome_from_expected_argument(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(d, expected="approved")
|
|
assert r.passed
|
|
assert r.meta["decision_outcome"] is True
|
|
|
|
def test_outcome_mismatch_via_expected_argument(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(d, expected="denied")
|
|
assert not r.passed
|
|
assert r.meta["decision_outcome"] is False
|
|
assert "decision_outcome" in r.meta["reasons"]
|
|
|
|
def test_outcome_check_skipped_when_no_expected(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(d)
|
|
assert "decision_outcome" not in r.meta
|
|
|
|
def test_confidence_out_of_range(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}}, confidence=0.4)
|
|
r = reg.get_evaluator("decision_scores")(
|
|
d, config={"expected_outcome": "approved", "min_confidence": 0.8}
|
|
)
|
|
assert not r.passed
|
|
assert r.meta["decision_confidence"] is False
|
|
|
|
def test_missing_provenance_fails(self):
|
|
d = _decision(metadata={})
|
|
r = reg.get_evaluator("decision_scores")(d, config={"expected_outcome": "approved"})
|
|
assert not r.passed
|
|
assert r.meta["provenance"] is False
|
|
|
|
def test_missing_required_fields(self):
|
|
d = _decision(reasoning="")
|
|
r = reg.get_evaluator("decision_scores")(d, config={"expected_outcome": "approved"})
|
|
assert not r.passed
|
|
assert r.meta["reasoning"] is False
|
|
|
|
def test_dict_input_coerced(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
as_dict = d.to_dict()
|
|
r = reg.get_evaluator("decision_scores")(
|
|
as_dict, config={"expected_outcome": "approved"}
|
|
)
|
|
assert r.passed
|
|
|
|
def test_malformed_dict_is_error_not_crash(self):
|
|
r = reg.get_evaluator("decision_scores")({"foo": "bar"}, config={})
|
|
assert not r.passed
|
|
assert r.meta.get("error")
|
|
|
|
def test_non_dict_metadata_is_error_not_crash(self):
|
|
bad = _decision(metadata="not-a-dict")
|
|
r = reg.get_evaluator("decision_scores")(bad, config={})
|
|
assert not r.passed
|
|
assert r.meta["provenance"] is False
|
|
|
|
def test_policy_compliance_check(self):
|
|
class FakePolicyEngine:
|
|
def check_compliance(self, decision, policy_id):
|
|
return True
|
|
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(
|
|
d, config={
|
|
"expected_outcome": "approved",
|
|
"policy_engine": FakePolicyEngine(),
|
|
"policy_id": "p1",
|
|
"expected_policy_compliant": True,
|
|
}
|
|
)
|
|
assert r.meta["policy"] is True
|
|
|
|
def test_policy_mismatch_fails(self):
|
|
class FakePolicyEngine:
|
|
def check_compliance(self, decision, policy_id):
|
|
return False
|
|
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
|
|
r = reg.get_evaluator("decision_scores")(
|
|
d, config={
|
|
"policy_engine": FakePolicyEngine(),
|
|
"policy_id": "p1",
|
|
"expected_policy_compliant": True,
|
|
}
|
|
)
|
|
assert not r.passed
|
|
assert r.meta["policy"] is False
|
|
|
|
def test_causal_chain_gate(self):
|
|
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}}, decision_id="only-decision")
|
|
with pytest.raises(NotImplementedError):
|
|
reg.get_evaluator("decision_scores")(
|
|
d, config={"causal_chain_exists": True, "graph_store": object()}
|
|
)
|