Spaces:
Running on Zero
Running on Zero
| """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 | |
| def valid_count(self) -> int: | |
| """Number of records that passed validation and duplicate checks.""" | |
| return len(self.articles) | |
| 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) | |
| 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) | |
| 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 | |
| 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) | |