Skip to content

lpgcast.validate

Validate a reply against the slice it was asked for.

Complete on purpose: graflo's writer silently drops an edge whose key no edge declares and a vertex with no identity value, which is right for a writer and wrong for an extraction. Every such case is a finding here.

Deterministic fixes run first and are recorded: values are coerced to their field type (declared units stripped), an edge given in the reverse of its declared direction is flipped, and quotes are matched ignoring case and whitespace runs. A property's quote is read nearest its owner's. An identity value the text refers to but only the episode's context states is grounded there. A number written as digits, or a date, must be what its quote states. What remains are findings, each naming the item it removes. An item with a finding is left out; the rest of the reply stands.

Attributes

CONSEQUENTIAL = frozenset({'endpoint_invalid'}) module-attribute

FINDING_ORDER = ('unknown_type', 'duplicate_ref', 'identity_insufficient', 'undeclared_edge', 'dangling_ref', 'undeclared_property', 'duplicate_property', 'coercion', 'value_not_in_quote', 'quote_not_found', 'endpoint_invalid') module-attribute

FindingKind = Literal['duplicate_ref', 'unknown_type', 'undeclared_property', 'duplicate_property', 'coercion', 'value_not_in_quote', 'quote_not_found', 'identity_insufficient', 'dangling_ref', 'undeclared_edge', 'endpoint_invalid'] module-attribute

Scope = Literal['vertex', 'edge', 'property'] module-attribute

Classes

Finding

Bases: BaseModel

A defect in a reply, and the item it removes.

Source code in lpgcast/validate.py
class Finding(BaseModel):
    """A defect in a reply, and the item it removes."""

    kind: FindingKind
    scope: Scope = Field(
        description="What is left out: a vertex, an edge or one property."
    )
    path: str = Field(
        description="Location in the reply, e.g. ``vertices[1].props[0]``."
    )
    ref: str | None = Field(default=None, description="Vertex ref involved, if any.")
    detail: str | None = Field(
        default=None, description="Sub-kind, e.g. the coercion kind."
    )
    message: str
    fragment: dict[str, Any] = Field(
        description="The offending item as the model wrote it."
    )

Attributes

detail = Field(default=None, description='Sub-kind, e.g. the coercion kind.') class-attribute instance-attribute
fragment = Field(description='The offending item as the model wrote it.') class-attribute instance-attribute
kind instance-attribute
message instance-attribute
path = Field(description='Location in the reply, e.g. ``vertices[1].props[0]``.') class-attribute instance-attribute
ref = Field(default=None, description='Vertex ref involved, if any.') class-attribute instance-attribute
scope = Field(description='What is left out: a vertex, an edge or one property.') class-attribute instance-attribute

Fix

Bases: BaseModel

A deterministic correction applied without asking the model.

Source code in lpgcast/validate.py
class Fix(BaseModel):
    """A deterministic correction applied without asking the model."""

    kind: Literal["coerced", "edge_reversed", "quote_normalized", "grounded_in_context"]
    path: str
    message: str

Attributes

kind instance-attribute
message instance-attribute
path instance-attribute

ValidEdge

Bases: BaseModel

An edge that passed, oriented as declared.

Source code in lpgcast/validate.py
class ValidEdge(BaseModel):
    """An edge that passed, oriented as declared."""

    edge_id: EdgeId
    source_ref: str
    target_ref: str
    props: dict[str, Any]
    span: Span
    prop_spans: dict[str, Span]

Attributes

edge_id instance-attribute
prop_spans instance-attribute
props instance-attribute
source_ref instance-attribute
span instance-attribute
target_ref instance-attribute

ValidUnmapped

Bases: BaseModel

An unmapped entry, located in the text when its quote is found.

Source code in lpgcast/validate.py
class ValidUnmapped(BaseModel):
    """An ``unmapped`` entry, located in the text when its quote is found."""

    quote: str
    note: str
    span: Span | None

Attributes

note instance-attribute
quote instance-attribute
span instance-attribute

ValidVertex

Bases: BaseModel

A vertex that passed, with coerced properties and spans.

Source code in lpgcast/validate.py
class ValidVertex(BaseModel):
    """A vertex that passed, with coerced properties and spans."""

    ref: str
    type: str
    mention: bool
    props: dict[str, Any]
    span: Span
    prop_spans: dict[str, Span]

Attributes

mention instance-attribute
prop_spans instance-attribute
props instance-attribute
ref instance-attribute
span instance-attribute
type instance-attribute

Validation

Bases: BaseModel

The checked reply.

Source code in lpgcast/validate.py
class Validation(BaseModel):
    """The checked reply."""

    vertices: list[ValidVertex] = Field(default_factory=list)
    edges: list[ValidEdge] = Field(default_factory=list)
    unmapped: list[ValidUnmapped] = Field(default_factory=list)
    findings: list[Finding] = Field(default_factory=list)
    fixes: list[Fix] = Field(default_factory=list)

    @property
    def repairable(self) -> list[Finding]:
        """Findings worth showing the model: all but consequences of others."""
        return [f for f in self.findings if f.kind not in CONSEQUENTIAL]

    @property
    def kept(self) -> int:
        """Items kept: vertices, edges and properties. Used to pick the best attempt."""
        return (
            len(self.vertices)
            + len(self.edges)
            + sum(len(v.props) for v in self.vertices)
            + sum(len(e.props) for e in self.edges)
        )

Attributes

edges = Field(default_factory=list) class-attribute instance-attribute
findings = Field(default_factory=list) class-attribute instance-attribute
fixes = Field(default_factory=list) class-attribute instance-attribute
kept property

Items kept: vertices, edges and properties. Used to pick the best attempt.

repairable property

Findings worth showing the model: all but consequences of others.

unmapped = Field(default_factory=list) class-attribute instance-attribute
vertices = Field(default_factory=list) class-attribute instance-attribute

Functions:

identity_fields(vertex)

Properties that take part in any identity of vertex.

Source code in lpgcast/validate.py
def identity_fields(vertex: Vertex) -> frozenset[str]:
    """Properties that take part in any identity of *vertex*."""
    names: set[str] = set()
    if vertex.identity_mode == "natural":
        names.update(vertex.identity)
    elif vertex.identity_mode == "hash":
        names.update(vertex.digest_source_fields)
    for secondary in vertex.secondary_identities:
        names.update(secondary.fields)
    return frozenset(names)

mention_identity(vertex, props)

How a mention can be looked up: ("identity", fields) when the primary identity is complete (or its digest computable), else the first secondary identity whose fields are all present. None when neither.

Source code in lpgcast/validate.py
def mention_identity(
    vertex: Vertex, props: dict[str, Any]
) -> tuple[str, list[str]] | None:
    """How a mention can be looked up: ``("identity", fields)`` when the primary
    identity is complete (or its digest computable), else the first secondary
    identity whose fields are all present. None when neither."""
    mode = vertex.identity_mode
    if (
        mode == "natural"
        and vertex.identity
        and all(f in props for f in vertex.identity)
    ):
        return "identity", list(vertex.identity)
    if mode == "hash" and compute_vertex_identity(props, vertex) is not None:
        return "identity", list(vertex.identity)
    for secondary in vertex.secondary_identities:
        if all(f in props for f in secondary.fields):
            return secondary.name or "secondary", list(secondary.fields)
    return None

validate(wire, slice_, text, context=())

Check wire against slice_, the episode text and its context.

Source code in lpgcast/validate.py
def validate(
    wire: WireExtraction,
    slice_: Schema,
    text: str,
    context: Sequence[ContextItem] = (),
) -> Validation:
    """Check *wire* against *slice_*, the episode *text* and its *context*."""
    checker = _Checker(slice_, text, context)
    refs = checker.vertices(wire)
    checker.edges(wire, refs)
    checker.unmapped(wire)
    order = {kind: i for i, kind in enumerate(FINDING_ORDER)}
    checker.result.findings.sort(key=lambda f: order[f.kind])
    return checker.result