feat(fase2): first-class Pydantic provenance stamp
This commit is contained in:
parent
ebe12a8675
commit
a0f263bf41
2 changed files with 104 additions and 0 deletions
57
src/portfolio_optimiser/provenance.py
Normal file
57
src/portfolio_optimiser/provenance.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""First-class Pydantic provenance stamp.
|
||||
|
||||
Provenance is authoritative framework data — **independent** of MAF's ``Annotation`` type,
|
||||
which silently drops on the Python streaming path (#4316, research 02 Dim 2). A
|
||||
``ProvenanceStamp`` is a Pydantic model that must carry at least one ``Citation`` (a
|
||||
``min_length=1`` constraint), the model + role that produced the proposal, the validator's
|
||||
decision, and the token usage. ``to_annotations()`` maps to MAF ``Annotation`` dicts for
|
||||
DISPLAY only — never the source of truth.
|
||||
|
||||
The locator type (``TextSpan``) is owned by ``retrieval.py`` (Step 5) and imported here, so
|
||||
a citation's span is the same exact object the retriever produced.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from agent_framework import Annotation, TextSpanRegion
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from portfolio_optimiser.retrieval import TextSpan
|
||||
|
||||
|
||||
class Citation(BaseModel):
|
||||
"""One exact-span citation into a source document (locator owned by retrieval.py)."""
|
||||
|
||||
file: str
|
||||
locator: TextSpan
|
||||
snippet: str
|
||||
|
||||
|
||||
class ProvenanceStamp(BaseModel):
|
||||
"""Authoritative provenance for one proposal — at least one citation is mandatory."""
|
||||
|
||||
citations: list[Citation] = Field(min_length=1)
|
||||
model: str
|
||||
role: str
|
||||
validator_decision: Literal["validated", "rejected"]
|
||||
token_usage: int
|
||||
|
||||
def to_annotations(self) -> list[Annotation]:
|
||||
"""Map to MAF ``Annotation`` dicts for display only (NOT the source of truth)."""
|
||||
return [
|
||||
Annotation(
|
||||
type="citation",
|
||||
snippet=c.snippet,
|
||||
annotated_regions=[
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=c.locator.start_index,
|
||||
end_index=c.locator.end_index,
|
||||
)
|
||||
],
|
||||
additional_properties={"file": c.file},
|
||||
)
|
||||
for c in self.citations
|
||||
]
|
||||
47
tests/test_provenance.py
Normal file
47
tests/test_provenance.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Step 6 tests — first-class Pydantic provenance stamp.
|
||||
|
||||
At least one citation is mandatory (the load-bearing NFR); a valid stamp exposes the
|
||||
proposal's provenance fields; the annotation adapter yields MAF citation dicts for display.
|
||||
Pattern: tests/spikes/test_c_validator.py (pydantic raises + field assertions).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from portfolio_optimiser.provenance import Citation, ProvenanceStamp
|
||||
from portfolio_optimiser.retrieval import TextSpan
|
||||
|
||||
|
||||
def _stamp() -> ProvenanceStamp:
|
||||
return ProvenanceStamp(
|
||||
citations=[Citation(file="asphalt.txt", locator=TextSpan(0, 10), snippet="0123456789")],
|
||||
model="qwen3:4b",
|
||||
role="proposer",
|
||||
validator_decision="validated",
|
||||
token_usage=42,
|
||||
)
|
||||
|
||||
|
||||
def test_zero_citations_is_rejected() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
ProvenanceStamp(
|
||||
citations=[],
|
||||
model="m",
|
||||
role="proposer",
|
||||
validator_decision="validated",
|
||||
token_usage=1,
|
||||
)
|
||||
|
||||
|
||||
def test_valid_stamp_exposes_provenance_fields() -> None:
|
||||
stamp = _stamp()
|
||||
assert len(stamp.citations) >= 1
|
||||
assert stamp.model and stamp.role
|
||||
assert stamp.validator_decision in {"validated", "rejected"}
|
||||
assert isinstance(stamp.token_usage, int) and stamp.token_usage > 0
|
||||
|
||||
|
||||
def test_to_annotations_yields_citation_dicts() -> None:
|
||||
anns = _stamp().to_annotations()
|
||||
assert anns and all(a["type"] == "citation" for a in anns)
|
||||
assert anns[0]["annotated_regions"][0]["type"] == "text_span"
|
||||
Loading…
Add table
Add a link
Reference in a new issue