Spaces:
Running on Zero
Running on Zero
| """Provenance-safe annotations and controlled evaluation cases.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| from enum import Enum | |
| from pathlib import Path | |
| from typing import Literal | |
| from pydantic import Field, model_validator | |
| from .schemas import ContractModel, FindingPolarity, Speaker | |
| EVALUATION_DATASET_ID = "banking-evaluator-v2" | |
| EVALUATION_DATASET_VERSION = "0.1.0" | |
| class EvaluationSourceType(str, Enum): | |
| EXISTING_CALL = "existing_call" | |
| CONTROLLED_CHALLENGE = "controlled_challenge" | |
| class AnnotatorType(str, Enum): | |
| HUMAN = "human" | |
| MODEL_ASSISTED_RESEARCHER_SEED = ( | |
| "model_assisted_researcher_seed" | |
| ) | |
| SYNTHETIC_AUTHOR = "synthetic_author" | |
| class AnnotationStatus(str, Enum): | |
| DRAFT = "draft" | |
| REVIEWED = "reviewed" | |
| ADJUDICATED = "adjudicated" | |
| class ExpectedAttention(str, Enum): | |
| REQUIRED = "required" | |
| NOT_REQUIRED = "not_required" | |
| UNCERTAIN = "uncertain" | |
| class CounterfactualAxis(str, Enum): | |
| TRANSCRIPT = "transcript" | |
| DELIVERY = "delivery" | |
| OUTCOME = "outcome" | |
| CONTROL = "control" | |
| RECOVERY = "recovery" | |
| DURATION = "duration" | |
| class ExpectedPairEffect(str, Enum): | |
| ATTENTION_CHANGES = "attention_changes" | |
| REASONS_CHANGE = "reasons_change" | |
| SUPPORT_CHANGES = "support_changes" | |
| RECOVERY_CHANGES = "recovery_changes" | |
| NO_DECISION_CHANGE = "no_decision_change" | |
| class AcousticCondition(str, Enum): | |
| NOT_MODELED = "not_modeled" | |
| CALM = "calm" | |
| PERSISTENT_ELEVATION = "persistent_elevation" | |
| RECOVERED_ELEVATION = "recovered_elevation" | |
| UNRESOLVED_ELEVATION = "unresolved_elevation" | |
| class AnnotationProvenance(ContractModel): | |
| annotator_id: str = Field(min_length=1) | |
| annotator_type: AnnotatorType | |
| status: AnnotationStatus | |
| method: str = Field(min_length=1) | |
| requires_human_review: bool | |
| ground_truth_basis: str | None = None | |
| def validate_claim_strength(self): | |
| if ( | |
| self.annotator_type | |
| == AnnotatorType.MODEL_ASSISTED_RESEARCHER_SEED | |
| ): | |
| if self.status != AnnotationStatus.DRAFT: | |
| raise ValueError( | |
| "model-assisted seed annotations must remain draft" | |
| ) | |
| if not self.requires_human_review: | |
| raise ValueError( | |
| "model-assisted seed annotations require human review" | |
| ) | |
| if self.ground_truth_basis: | |
| raise ValueError( | |
| "model-assisted seed annotations are not ground truth" | |
| ) | |
| if self.status == AnnotationStatus.ADJUDICATED: | |
| if ( | |
| self.annotator_type == AnnotatorType.HUMAN | |
| and self.requires_human_review | |
| ): | |
| raise ValueError( | |
| "human-adjudicated annotations cannot remain pending" | |
| ) | |
| if ( | |
| self.annotator_type == AnnotatorType.SYNTHETIC_AUTHOR | |
| and not self.ground_truth_basis | |
| ): | |
| raise ValueError( | |
| "synthetic known answers require a controlled basis" | |
| ) | |
| return self | |
| class AnnotationEvidence(ContractModel): | |
| evidence_id: str = Field( | |
| min_length=1, | |
| pattern=r"^[a-zA-Z0-9_.:-]+$", | |
| ) | |
| segment_id: str = Field(min_length=1) | |
| seq_id: int = Field(ge=0) | |
| speaker: Speaker | |
| start_seconds: float = Field(ge=0.0) | |
| end_seconds: float = Field(ge=0.0) | |
| quote: str = Field(min_length=1) | |
| purpose: str = Field(min_length=1) | |
| def validate_time_range(self): | |
| if self.end_seconds < self.start_seconds: | |
| raise ValueError( | |
| "annotation evidence end must not precede start" | |
| ) | |
| return self | |
| class ExpectedFindingLabel(ContractModel): | |
| finding_type: str = Field( | |
| min_length=1, | |
| pattern=r"^[a-z0-9_.]+$", | |
| ) | |
| polarity: FindingPolarity | |
| evidence_ids: list[str] = Field(default_factory=list) | |
| rationale: str = Field(min_length=1) | |
| class CallAnnotation(ContractModel): | |
| annotation_id: str = Field( | |
| min_length=1, | |
| pattern=r"^[a-zA-Z0-9_.:-]+$", | |
| ) | |
| call_id: str = Field(min_length=1) | |
| profile_id: str = Field(min_length=1) | |
| source_type: EvaluationSourceType | |
| expected_attention: ExpectedAttention | |
| expected_findings: list[ExpectedFindingLabel] = Field( | |
| default_factory=list | |
| ) | |
| evidence: list[AnnotationEvidence] = Field(default_factory=list) | |
| uncertainty_notes: list[str] = Field(default_factory=list) | |
| provenance: AnnotationProvenance | |
| def validate_annotation(self): | |
| evidence_ids = [item.evidence_id for item in self.evidence] | |
| if len(evidence_ids) != len(set(evidence_ids)): | |
| raise ValueError("annotation evidence ids must be unique") | |
| known_evidence = set(evidence_ids) | |
| finding_types = [ | |
| finding.finding_type for finding in self.expected_findings | |
| ] | |
| if len(finding_types) != len(set(finding_types)): | |
| raise ValueError("expected finding types must be unique") | |
| for finding in self.expected_findings: | |
| if not set(finding.evidence_ids).issubset(known_evidence): | |
| raise ValueError( | |
| "expected finding references unknown evidence" | |
| ) | |
| negative = [ | |
| item | |
| for item in self.expected_findings | |
| if item.polarity == FindingPolarity.NEGATIVE | |
| ] | |
| if ( | |
| self.expected_attention == ExpectedAttention.REQUIRED | |
| and not negative | |
| ): | |
| raise ValueError( | |
| "required attention needs an expected negative finding" | |
| ) | |
| if ( | |
| self.expected_attention == ExpectedAttention.NOT_REQUIRED | |
| and negative | |
| ): | |
| raise ValueError( | |
| "not-required attention cannot include negative findings" | |
| ) | |
| if ( | |
| self.expected_attention == ExpectedAttention.UNCERTAIN | |
| and not self.uncertainty_notes | |
| ): | |
| raise ValueError( | |
| "uncertain attention requires an uncertainty note" | |
| ) | |
| if ( | |
| self.source_type == EvaluationSourceType.EXISTING_CALL | |
| and self.provenance.annotator_type | |
| == AnnotatorType.SYNTHETIC_AUTHOR | |
| ): | |
| raise ValueError( | |
| "existing calls cannot use synthetic-author provenance" | |
| ) | |
| return self | |
| class ExistingCallRecord(ContractModel): | |
| call_id: str = Field(min_length=1) | |
| transcript_path: str = Field(min_length=1) | |
| transcript_sha256: str = Field(pattern=r"^[0-9a-f]{64}$") | |
| annotation: CallAnnotation | |
| def validate_call_id(self): | |
| if self.annotation.call_id != self.call_id: | |
| raise ValueError("existing annotation call id mismatch") | |
| if ( | |
| self.annotation.source_type | |
| != EvaluationSourceType.EXISTING_CALL | |
| ): | |
| raise ValueError( | |
| "existing record requires existing-call annotation" | |
| ) | |
| return self | |
| class ChallengeSegment(ContractModel): | |
| segment_id: str = Field(min_length=1) | |
| seq_id: int = Field(ge=0) | |
| speaker: Speaker | |
| start_seconds: float = Field(ge=0.0) | |
| end_seconds: float = Field(ge=0.0) | |
| text: str = Field(min_length=1) | |
| def validate_time_range(self): | |
| if self.end_seconds < self.start_seconds: | |
| raise ValueError( | |
| "challenge segment end must not precede start" | |
| ) | |
| return self | |
| class ChallengeCall(ContractModel): | |
| call_id: str = Field(min_length=1) | |
| title: str = Field(min_length=1) | |
| intent_ids: list[str] = Field(min_length=1) | |
| facts: dict[str, bool] | |
| acoustic_condition: AcousticCondition | |
| segments: list[ChallengeSegment] = Field(min_length=1) | |
| annotation: CallAnnotation | |
| def validate_challenge(self): | |
| if self.annotation.call_id != self.call_id: | |
| raise ValueError("challenge annotation call id mismatch") | |
| if ( | |
| self.annotation.source_type | |
| != EvaluationSourceType.CONTROLLED_CHALLENGE | |
| ): | |
| raise ValueError( | |
| "challenge requires controlled-challenge annotation" | |
| ) | |
| segment_ids = [item.segment_id for item in self.segments] | |
| seq_ids = [item.seq_id for item in self.segments] | |
| if len(segment_ids) != len(set(segment_ids)): | |
| raise ValueError("challenge segment ids must be unique") | |
| if len(seq_ids) != len(set(seq_ids)): | |
| raise ValueError("challenge seq ids must be unique") | |
| return self | |
| class CounterfactualPair(ContractModel): | |
| pair_id: str = Field( | |
| min_length=1, | |
| pattern=r"^[a-zA-Z0-9_.:-]+$", | |
| ) | |
| axis: CounterfactualAxis | |
| baseline_call_id: str = Field(min_length=1) | |
| variant_call_id: str = Field(min_length=1) | |
| controlled_change: str = Field(min_length=1) | |
| invariant_dimensions: list[ | |
| Literal[ | |
| "intent_and_facts", | |
| "transcript", | |
| "acoustic_condition", | |
| "attention", | |
| ] | |
| ] = Field(min_length=1) | |
| expected_effect: ExpectedPairEffect | |
| def validate_distinct_calls(self): | |
| if self.baseline_call_id == self.variant_call_id: | |
| raise ValueError( | |
| "counterfactual pair requires two distinct calls" | |
| ) | |
| if len(self.invariant_dimensions) != len( | |
| set(self.invariant_dimensions) | |
| ): | |
| raise ValueError( | |
| "counterfactual invariant dimensions must be unique" | |
| ) | |
| return self | |
| class EvaluationDataset(ContractModel): | |
| schema_version: Literal["1.0"] = "1.0" | |
| dataset_id: Literal["banking-evaluator-v2"] = ( | |
| EVALUATION_DATASET_ID | |
| ) | |
| dataset_version: str = Field(min_length=1) | |
| profile_id: str = Field(min_length=1) | |
| existing_calls: list[ExistingCallRecord] = Field( | |
| default_factory=list | |
| ) | |
| challenge_calls: list[ChallengeCall] = Field( | |
| default_factory=list | |
| ) | |
| counterfactual_pairs: list[CounterfactualPair] = Field( | |
| default_factory=list | |
| ) | |
| def validate_dataset_references(self): | |
| all_calls = [ | |
| item.call_id | |
| for item in self.existing_calls | |
| ] + [ | |
| item.call_id | |
| for item in self.challenge_calls | |
| ] | |
| if len(all_calls) != len(set(all_calls)): | |
| raise ValueError("evaluation call ids must be unique") | |
| pair_ids = [item.pair_id for item in self.counterfactual_pairs] | |
| if len(pair_ids) != len(set(pair_ids)): | |
| raise ValueError("counterfactual pair ids must be unique") | |
| challenge_ids = { | |
| item.call_id for item in self.challenge_calls | |
| } | |
| for pair in self.counterfactual_pairs: | |
| if ( | |
| pair.baseline_call_id not in challenge_ids | |
| or pair.variant_call_id not in challenge_ids | |
| ): | |
| raise ValueError( | |
| "counterfactual pair references unknown challenge" | |
| ) | |
| represented = { | |
| item.axis for item in self.counterfactual_pairs | |
| } | |
| if self.counterfactual_pairs and represented != set( | |
| CounterfactualAxis | |
| ): | |
| missing = sorted( | |
| item.value | |
| for item in set(CounterfactualAxis) - represented | |
| ) | |
| raise ValueError( | |
| f"counterfactual axes are incomplete: {missing}" | |
| ) | |
| return self | |
| def file_sha256(path: Path) -> str: | |
| return hashlib.sha256(path.read_bytes()).hexdigest() | |
| def _transcript_segments(path: Path) -> tuple[str, dict[str, dict]]: | |
| payload = json.loads(path.read_text(encoding="utf-8")) | |
| return payload["call_id"], { | |
| item["id"]: item for item in payload["sentences"] | |
| } | |
| def _validate_evidence( | |
| annotation: CallAnnotation, | |
| segments: dict[str, dict], | |
| ) -> None: | |
| for evidence in annotation.evidence: | |
| try: | |
| segment = segments[evidence.segment_id] | |
| except KeyError as exc: | |
| raise ValueError( | |
| f"{annotation.call_id}: unknown evidence segment " | |
| f"{evidence.segment_id!r}" | |
| ) from exc | |
| expected = { | |
| "seq_id": evidence.seq_id, | |
| "speaker": evidence.speaker.value.upper(), | |
| "start": evidence.start_seconds, | |
| "end": evidence.end_seconds, | |
| "text": evidence.quote, | |
| } | |
| actual = { | |
| "seq_id": segment["seq_id"], | |
| "speaker": segment["speaker"], | |
| "start": segment["start"], | |
| "end": segment["end"], | |
| "text": segment["text"], | |
| } | |
| if actual != expected: | |
| raise ValueError( | |
| f"{annotation.call_id}: evidence " | |
| f"{evidence.evidence_id!r} does not exactly match " | |
| "the transcript" | |
| ) | |
| def _challenge_segments(call: ChallengeCall) -> dict[str, dict]: | |
| return { | |
| item.segment_id: { | |
| "seq_id": item.seq_id, | |
| "speaker": item.speaker.value.upper(), | |
| "start": item.start_seconds, | |
| "end": item.end_seconds, | |
| "text": item.text, | |
| } | |
| for item in call.segments | |
| } | |
| def _transcript_content(call: ChallengeCall) -> list[tuple[str, str]]: | |
| return [ | |
| (item.speaker.value, item.text) | |
| for item in call.segments | |
| ] | |
| def validate_evaluation_dataset( | |
| dataset: EvaluationDataset, | |
| repo_root: Path, | |
| ) -> None: | |
| """Validate source fidelity and declared pair invariants.""" | |
| for record in dataset.existing_calls: | |
| path = repo_root / record.transcript_path | |
| if file_sha256(path) != record.transcript_sha256: | |
| raise ValueError( | |
| f"{record.call_id}: transcript sha256 changed" | |
| ) | |
| call_id, segments = _transcript_segments(path) | |
| if call_id != record.call_id: | |
| raise ValueError( | |
| f"{record.call_id}: transcript call id mismatch" | |
| ) | |
| _validate_evidence(record.annotation, segments) | |
| challenges = { | |
| item.call_id: item for item in dataset.challenge_calls | |
| } | |
| for call in challenges.values(): | |
| _validate_evidence( | |
| call.annotation, | |
| _challenge_segments(call), | |
| ) | |
| for pair in dataset.counterfactual_pairs: | |
| baseline = challenges[pair.baseline_call_id] | |
| variant = challenges[pair.variant_call_id] | |
| invariants = set(pair.invariant_dimensions) | |
| if "intent_and_facts" in invariants and ( | |
| baseline.intent_ids != variant.intent_ids | |
| or baseline.facts != variant.facts | |
| ): | |
| raise ValueError( | |
| f"{pair.pair_id}: intent_and_facts invariant failed" | |
| ) | |
| if ( | |
| "transcript" in invariants | |
| and _transcript_content(baseline) | |
| != _transcript_content(variant) | |
| ): | |
| raise ValueError( | |
| f"{pair.pair_id}: transcript invariant failed" | |
| ) | |
| if ( | |
| "acoustic_condition" in invariants | |
| and baseline.acoustic_condition | |
| != variant.acoustic_condition | |
| ): | |
| raise ValueError( | |
| f"{pair.pair_id}: acoustic invariant failed" | |
| ) | |
| if ( | |
| "attention" in invariants | |
| and baseline.annotation.expected_attention | |
| != variant.annotation.expected_attention | |
| ): | |
| raise ValueError( | |
| f"{pair.pair_id}: attention invariant failed" | |
| ) | |
| def load_evaluation_dataset(path: Path) -> EvaluationDataset: | |
| return EvaluationDataset.model_validate_json( | |
| path.read_text(encoding="utf-8") | |
| ) | |