igerasimov's picture
MVP Milestone 8
19d779e
Raw
History Blame Contribute Delete
7.95 kB
"""Candidate construction for constrained classification prompts."""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from gcmd_classifier.llm.prompts import PromptCandidate
from gcmd_classifier.models import CanonicalConceptRecord
from gcmd_classifier.vocabulary.index import VocabularyIndex
class TopicCandidate(BaseModel):
"""Application candidate for one UUID-bearing GCMD Topic."""
model_config = ConfigDict(extra="forbid", frozen=True)
candidate_id: str = Field(min_length=1)
topic_uuid: str = Field(min_length=1)
prompt_candidate: PromptCandidate
@property
def record_uuid(self) -> str:
"""Authoritative UUID for the candidate's Topic record."""
return self.topic_uuid
class TermCandidate(BaseModel):
"""Application candidate for one direct Term beneath a selected Topic."""
model_config = ConfigDict(extra="forbid", frozen=True)
candidate_id: str = Field(min_length=1)
topic_uuid: str = Field(min_length=1)
term_uuid: str = Field(min_length=1)
prompt_candidate: PromptCandidate
@property
def record_uuid(self) -> str:
"""Authoritative UUID for the candidate's Term record."""
return self.term_uuid
class VariableCandidate(BaseModel):
"""Application candidate for one direct Variable child beneath a selected parent."""
model_config = ConfigDict(extra="forbid", frozen=True)
candidate_id: str = Field(min_length=1)
parent_uuid: str = Field(min_length=1)
variable_uuid: str = Field(min_length=1)
prompt_candidate: PromptCandidate
@property
def record_uuid(self) -> str:
"""Authoritative UUID for the candidate's Variable record."""
return self.variable_uuid
def build_topic_candidates(index: VocabularyIndex) -> tuple[TopicCandidate, ...]:
"""Build deterministic prompt candidates from UUID-bearing Topic records only."""
candidates: list[TopicCandidate] = []
for position, topic in enumerate(index.topics(), start=1):
_validate_topic_record(topic)
candidate_id = f"topic_{position:04d}"
candidates.append(
TopicCandidate(
candidate_id=candidate_id,
topic_uuid=topic.UUID,
prompt_candidate=PromptCandidate(
candidate_id=candidate_id,
name=topic.name,
level=topic.level,
definition=topic.definition,
canonical_path=topic.canonical_path,
),
)
)
return tuple(candidates)
def build_term_candidates(
index: VocabularyIndex,
*,
topic_uuid: str,
) -> tuple[TermCandidate, ...]:
"""Build deterministic prompt candidates from direct Term children of one Topic."""
topic = index.get(topic_uuid)
_validate_topic_record(topic)
candidates: list[TermCandidate] = []
for position, term in enumerate(index.terms_for_topic(topic_uuid), start=1):
_validate_term_record(term, topic_uuid=topic_uuid, index=index)
candidate_id = f"term_{position:04d}"
candidates.append(
TermCandidate(
candidate_id=candidate_id,
topic_uuid=topic_uuid,
term_uuid=term.UUID,
prompt_candidate=PromptCandidate(
candidate_id=candidate_id,
name=term.name,
level=term.level,
definition=term.definition,
canonical_path=term.canonical_path,
parent_context=topic.canonical_path,
),
)
)
return tuple(candidates)
def build_variable_candidates(
index: VocabularyIndex,
*,
parent_uuid: str,
) -> tuple[VariableCandidate, ...]:
"""Build deterministic prompt candidates from direct Variable children of one parent."""
parent = index.get(parent_uuid)
_validate_variable_parent_record(parent)
candidates: list[VariableCandidate] = []
for position, variable in enumerate(index.variables_for_parent(parent_uuid), start=1):
_validate_variable_child_record(variable, parent_uuid=parent_uuid, index=index)
candidate_id = f"variable_{position:04d}"
candidates.append(
VariableCandidate(
candidate_id=candidate_id,
parent_uuid=parent_uuid,
variable_uuid=variable.UUID,
prompt_candidate=PromptCandidate(
candidate_id=candidate_id,
name=variable.name,
level=variable.level,
definition=variable.definition,
canonical_path=variable.canonical_path,
parent_context=parent.canonical_path,
),
)
)
return tuple(candidates)
def validate_variable_candidate_relationship(
candidate: VariableCandidate,
*,
selected_parent_uuid: str,
index: VocabularyIndex,
) -> None:
"""Validate that a Variable candidate is a direct child of the selected parent."""
if candidate.parent_uuid != selected_parent_uuid:
raise ValueError(
f"Variable candidate {candidate.candidate_id!r} was built for parent "
f"{candidate.parent_uuid!r}, not selected parent {selected_parent_uuid!r}."
)
variable = index.get(candidate.variable_uuid)
_validate_variable_child_record(variable, parent_uuid=selected_parent_uuid, index=index)
def validate_term_candidate_relationship(
candidate: TermCandidate,
*,
selected_topic_uuid: str,
index: VocabularyIndex,
) -> None:
"""Validate that a Term candidate is a direct child of the selected Topic."""
if candidate.topic_uuid != selected_topic_uuid:
raise ValueError(
f"Term candidate {candidate.candidate_id!r} was built for Topic "
f"{candidate.topic_uuid!r}, not selected Topic {selected_topic_uuid!r}."
)
term = index.get(candidate.term_uuid)
_validate_term_record(term, topic_uuid=selected_topic_uuid, index=index)
def _validate_topic_record(record: CanonicalConceptRecord) -> None:
if record.level != "Topic":
raise ValueError(f"Expected Topic record, got {record.level!r} for {record.UUID!r}.")
if not record.UUID:
raise ValueError(f"Topic record {record.name!r} does not have a UUID.")
def _validate_term_record(
record: CanonicalConceptRecord,
*,
topic_uuid: str,
index: VocabularyIndex,
) -> None:
if record.level != "Term":
raise ValueError(f"Expected Term record, got {record.level!r} for {record.UUID!r}.")
if not record.UUID:
raise ValueError(f"Term record {record.name!r} does not have a UUID.")
if index.parent_of(record.UUID) != topic_uuid:
raise ValueError(f"Term {record.UUID!r} is not a direct child of Topic {topic_uuid!r}.")
def _validate_variable_parent_record(record: CanonicalConceptRecord) -> None:
if record.level not in {"Term", "Variable_Level_1", "Variable_Level_2"}:
raise ValueError(
f"Expected Term or Variable parent, got {record.level!r} for {record.UUID!r}."
)
if not record.UUID:
raise ValueError(f"Variable parent record {record.name!r} does not have a UUID.")
def _validate_variable_child_record(
record: CanonicalConceptRecord,
*,
parent_uuid: str,
index: VocabularyIndex,
) -> None:
if record.level not in {"Variable_Level_1", "Variable_Level_2", "Variable_Level_3"}:
raise ValueError(f"Expected Variable record, got {record.level!r} for {record.UUID!r}.")
if not record.UUID:
raise ValueError(f"Variable record {record.name!r} does not have a UUID.")
if index.parent_of(record.UUID) != parent_uuid:
raise ValueError(
f"Variable {record.UUID!r} is not a direct child of parent {parent_uuid!r}."
)