igerasimov's picture
Fix Hugging Face runtime compatibility with Python 3.10
1163af8
Raw
History Blame Contribute Delete
12.9 kB
"""Shared typed models for the GCMD classifier MVP."""
from enum import Enum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
HierarchyLevel = Literal[
"Topic",
"Term",
"Variable_Level_1",
"Variable_Level_2",
"Variable_Level_3",
]
SourceHierarchyLevel = Literal[
"Category",
"Topic",
"Term",
"Variable_Level_1",
"Variable_Level_2",
"Variable_Level_3",
]
class CanonicalConceptRecord(BaseModel):
"""Canonical UUID-bearing GCMD concept record derived from the hierarchy."""
model_config = ConfigDict(frozen=True)
UUID: str
name: str
level: HierarchyLevel
topic: str
term: str | None = None
path_components: tuple[str, ...]
canonical_path: str
parent_uuid: str | None = None
parent_name: str | None = None
child_uuids: tuple[str, ...] = Field(default_factory=tuple)
has_children: bool
assignable: bool
definition: str | None = None
vocabulary_version: str
class ArticleRecord(BaseModel):
"""Validated source article record with exact serialized field names."""
model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
DOI: str
Title: str
Year: int
Abstract: str
class ArticleValidationIssue(BaseModel):
"""Structured validation issue for one article source record."""
model_config = ConfigDict(frozen=True)
index: int | None
code: str
message: str
field: str | None = None
DOI: str | None = None
class ArticleLoadResult(BaseModel):
"""Aggregate article loading result preserving valid records and all issues."""
model_config = ConfigDict(frozen=True)
articles: tuple[ArticleRecord, ...]
errors: tuple[ArticleValidationIssue, ...] = Field(default_factory=tuple)
source_count: int
@property
def valid_count(self) -> int:
"""Number of records that passed validation and duplicate checks."""
return len(self.articles)
@property
def has_errors(self) -> bool:
"""Whether any source records failed validation."""
return bool(self.errors)
class SupportType(str, Enum): # noqa: UP042
"""Uncalibrated model support category for a candidate selection."""
EXPLICIT = "explicit"
INFERRED = "inferred"
MIXED = "mixed"
class ArticleProcessingStatus(str, Enum): # noqa: UP042
"""Workflow execution state for one article."""
COMPLETED = "completed"
PARTIAL = "partial"
FAILED = "failed"
SKIPPED = "skipped"
class ArticleClassificationOutcome(str, Enum): # noqa: UP042
"""Semantic article-level outcome when processing completes."""
CLASSIFIED = "classified"
PENDING_REVIEW = "pending_review"
NOT_CLASSIFIED = "not_classified"
class ClassificationFinalStatus(str, Enum): # noqa: UP042
"""Final automated status for one classification candidate."""
ACCEPTED = "accepted"
REDUCED_TO_ANCESTOR = "reduced_to_ancestor"
REVIEW_REQUIRED = "review_required"
REJECTED = "rejected"
class ReviewStatus(str, Enum): # noqa: UP042
"""Human-review workflow state."""
NOT_REQUIRED = "not_required"
PENDING = "pending"
COMPLETED = "completed"
class StructuredMessage(BaseModel):
"""Structured warning or error message for output records."""
model_config = ConfigDict(extra="forbid", frozen=True)
code: str = Field(min_length=1)
message: str = Field(min_length=1)
stage: str | None = None
details: dict[str, Any] | None = None
index: int | None = None
DOI: str | None = None
class OutputWarning(StructuredMessage):
"""Non-fatal warning associated with an article or classification."""
class OutputError(StructuredMessage):
"""Structured failure associated with an article or classification."""
retry_count: int | None = Field(default=None, ge=0)
class DeterministicValidationResult(BaseModel):
"""Vocabulary validation result for a proposed final classification."""
model_config = ConfigDict(extra="forbid", frozen=True)
valid: bool
errors: tuple[OutputError, ...] = Field(default_factory=tuple)
warnings: tuple[OutputWarning, ...] = Field(default_factory=tuple)
class ConfidenceMetadata(BaseModel):
"""Optional, uncalibrated confidence signals from routing stages."""
model_config = ConfigDict(extra="forbid", frozen=True)
topic: float | None = Field(default=None, ge=0.0, le=1.0)
term: float | None = Field(default=None, ge=0.0, le=1.0)
variable_level_1: float | None = Field(default=None, ge=0.0, le=1.0)
variable_level_2: float | None = Field(default=None, ge=0.0, le=1.0)
variable_level_3: float | None = Field(default=None, ge=0.0, le=1.0)
final: float | None = Field(default=None, ge=0.0, le=1.0)
class OriginalCandidateReference(BaseModel):
"""Reference to an earlier candidate retained for reductions or diagnostics."""
model_config = ConfigDict(extra="forbid", frozen=True)
UUID: str = Field(min_length=1)
name: str | None = None
level: HierarchyLevel | None = None
canonical_path: str = Field(min_length=1)
path_components: tuple[str, ...] = Field(default_factory=tuple)
class SemanticValidationResult(BaseModel):
"""Draft semantic-validation result shape for future pipeline stages."""
model_config = ConfigDict(extra="forbid", frozen=True)
decision: str | None = None
support_level: str | None = None
deepest_supported_uuid: str | None = None
deepest_supported_path: str | None = None
unsupported_components: tuple[str, ...] = Field(default_factory=tuple)
evidence: str | None = None
errors: tuple[OutputError, ...] = Field(default_factory=tuple)
class ClassificationRecord(BaseModel):
"""Article output record for one GCMD classification or retained diagnostic candidate."""
model_config = ConfigDict(extra="forbid", frozen=True)
UUID: str = Field(min_length=1)
name: str = Field(min_length=1)
level: HierarchyLevel
canonical_path: str = Field(min_length=1)
path_components: tuple[str, ...] = Field(min_length=1)
topic: str = Field(min_length=1)
term: str | None = None
parent_uuid: str | None = None
branch_id: str | None = None
confidence: ConfidenceMetadata | None = None
classifier_evidence: str | None = None
support_type: SupportType | None = None
reason_for_stopping: str | None = None
deterministic_validation: DeterministicValidationResult
semantic_validation: SemanticValidationResult | None = None
final_status: ClassificationFinalStatus
review_required: bool = False
review_status: ReviewStatus | None = None
original_candidate: OriginalCandidateReference | None = None
warnings: tuple[OutputWarning, ...] = Field(default_factory=tuple)
errors: tuple[OutputError, ...] = Field(default_factory=tuple)
@model_validator(mode="after")
def accepted_results_require_valid_deterministic_validation(self) -> "ClassificationRecord":
"""Prevent accepted final statuses from carrying failed vocabulary validation."""
final_statuses_requiring_validity = {
ClassificationFinalStatus.ACCEPTED,
ClassificationFinalStatus.REDUCED_TO_ANCESTOR,
}
if (
self.final_status in final_statuses_requiring_validity
and not self.deterministic_validation.valid
):
raise ValueError(
"accepted or reduced classifications require deterministic_validation.valid"
)
return self
class ProcessingMetadata(BaseModel):
"""Reproducibility and runtime metadata for an article result."""
model_config = ConfigDict(extra="forbid", frozen=True)
run_id: str | None = None
processed_at: str | None = None
started_at: str | None = None
completed_at: str | None = None
model_provider: str | None = None
model_name: str | None = None
model_parameters: dict[str, Any] = Field(default_factory=dict)
prompt_versions: dict[str, str] = Field(default_factory=dict)
vocabulary_version: str | None = None
vocabulary_hash: str | None = None
application_version: str | None = None
configuration_hash: str | None = None
article_fingerprint: str | None = None
cache_used: bool = False
processing_time_seconds: float | None = Field(default=None, ge=0.0)
model_calls: int | None = Field(default=None, ge=0)
input_tokens: int | None = Field(default=None, ge=0)
output_tokens: int | None = Field(default=None, ge=0)
estimated_cost: float | None = Field(default=None, ge=0.0)
title_available: bool | None = None
abstract_available: bool | None = None
class ArticleResult(BaseModel):
"""Structured output for one article, preserving source fields exactly."""
model_config = ConfigDict(extra="forbid", frozen=True)
DOI: str = Field(strict=True)
Title: str = Field(strict=True)
Year: int = Field(strict=True)
Abstract: str = Field(strict=True)
processing_status: ArticleProcessingStatus
classification_outcome: ArticleClassificationOutcome | None = None
classifications: tuple[ClassificationRecord, ...] = Field(default_factory=tuple)
no_classification_reason: str | None = None
review_status: ReviewStatus = ReviewStatus.NOT_REQUIRED
warnings: tuple[OutputWarning, ...] = Field(default_factory=tuple)
errors: tuple[OutputError, ...] = Field(default_factory=tuple)
processing_metadata: ProcessingMetadata = Field(default_factory=ProcessingMetadata)
@field_validator("Year")
@classmethod
def reject_boolean_year(cls, value: int) -> int:
"""Keep article-output validation consistent with ArticleRecord."""
if isinstance(value, bool):
raise ValueError("Year must be a strict integer and not a Boolean.")
return value
@model_validator(mode="after")
def validate_article_result_consistency(self) -> "ArticleResult":
"""Apply article-level status-scope consistency rules."""
if self.processing_status is ArticleProcessingStatus.COMPLETED:
if self.classification_outcome is None:
raise ValueError("completed article results require classification_outcome")
if (
self.classification_outcome is ArticleClassificationOutcome.NOT_CLASSIFIED
and not self.no_classification_reason
):
raise ValueError("not_classified article results require no_classification_reason")
if (
self.classification_outcome is ArticleClassificationOutcome.NOT_CLASSIFIED
and self.classifications
):
raise ValueError("not_classified article results cannot include classifications")
if (
self.classification_outcome is ArticleClassificationOutcome.CLASSIFIED
and not self.classifications
):
raise ValueError("classified article results require at least one classification")
return self
class RunSummary(BaseModel):
"""Draft aggregate run-summary output schema."""
model_config = ConfigDict(extra="forbid", frozen=True)
run_id: str = Field(min_length=1)
started_at: str | None = None
completed_at: str | None = None
articles_received: int = Field(ge=0)
valid_article_records: int = Field(default=0, ge=0)
invalid_source_records: int = Field(default=0, ge=0)
processed_articles: int = Field(default=0, ge=0)
cache_hits: int = Field(default=0, ge=0)
cache_misses: int = Field(default=0, ge=0)
total_warnings: int = Field(default=0, ge=0)
total_errors: int = Field(default=0, ge=0)
duration_seconds: float | None = Field(default=None, ge=0.0)
articles_completed: int = Field(default=0, ge=0)
articles_partial: int = Field(default=0, ge=0)
articles_failed: int = Field(default=0, ge=0)
articles_skipped: int = Field(default=0, ge=0)
articles_not_classified: int = Field(default=0, ge=0)
articles_requiring_review: int = Field(default=0, ge=0)
accepted_classifications: int = Field(default=0, ge=0)
reduced_classifications: int = Field(default=0, ge=0)
rejected_candidates: int = Field(default=0, ge=0)
average_classifications_per_article: float | None = Field(default=None, ge=0.0)
average_processing_time_seconds: float | None = Field(default=None, ge=0.0)
total_model_calls: int | None = Field(default=None, ge=0)
input_tokens: int | None = Field(default=None, ge=0)
output_tokens: int | None = Field(default=None, ge=0)
estimated_total_cost: float | None = Field(default=None, ge=0.0)
warnings: tuple[OutputWarning, ...] = Field(default_factory=tuple)
errors: tuple[OutputError, ...] = Field(default_factory=tuple)