curated_feed.policy =================== .. py:module:: curated_feed.policy .. autoapi-nested-parse:: Assembling the prompt from the policy documents and the day's candidates. No judgement of its own: this module concatenates files and numbers a list. What the model is told lives in two documents, and the split between them is the split between the engine and a runner. The scoring prompt ships with the package. It is not taste but contract: its response section is the schema `models.Response` parses, and its consequence scale is the range `models.Graded` validates, so a prompt and a parser that disagree fail at six in the morning with a malformed day. The two version together or not at all. The editorial policy is named by the runner's configuration and is loaded at runtime, so that a change of taste is a commit against a change in output quality rather than a code change. `docs/scoring-spec.md` is deliberately absent. It names the score floor and the item cap, and a model that knows them pre-filters or pads. The assembled prompt is kept in two parts. The documents are identical on every run and are most of the tokens, so they are the half worth caching; the candidate list is the half that changes. Attributes ---------- .. autoapisummary:: curated_feed.policy.DOCUMENT_SEPARATOR curated_feed.policy.CANDIDATES_HEADING curated_feed.policy.PACKAGE curated_feed.policy.PROMPT_FILENAME curated_feed.policy.NO_SUMMARY curated_feed.policy.UNKNOWN_DATE curated_feed.policy.EXAMPLE_CANDIDATE Classes ------- .. autoapisummary:: curated_feed.policy.Prompt Functions --------- .. autoapisummary:: curated_feed.policy.default_prompt_path curated_feed.policy.read_documents curated_feed.policy.format_candidate curated_feed.policy.render_candidates curated_feed.policy.build_prompt Module Contents --------------- .. py:data:: DOCUMENT_SEPARATOR :value: Multiline-String .. raw:: html
Show Value .. code-block:: python """ --- """ .. raw:: html
.. py:data:: CANDIDATES_HEADING :value: '# Candidates' .. py:data:: PACKAGE :value: 'curated_feed' .. py:data:: PROMPT_FILENAME :value: 'prompt.md' .. py:data:: NO_SUMMARY :value: '(no summary)' .. py:data:: UNKNOWN_DATE :value: 'date unknown' .. py:data:: EXAMPLE_CANDIDATE A neutral candidate, so that the doctests below have something to render. .. py:class:: Prompt(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` An assembled prompt, split at the boundary between stable and variable. .. py:attribute:: documents :type: str .. py:attribute:: candidates :type: str .. py:method:: as_text() -> str The whole prompt, for a client that cannot cache a prefix. .. py:function:: default_prompt_path() -> pathlib.Path The scoring prompt that ships with the package. Resolved through the package rather than the working directory, because the engine is installed rather than checked out wherever it runs: an image, a virtual environment, an editable tree. A runner's configuration cannot name it, which is the point — see this module's docstring. >>> default_prompt_path().name 'prompt.md' >>> default_prompt_path().is_file() True .. py:function:: read_documents(*paths: pathlib.Path) -> str Concatenate the policy documents, in the order given. .. py:function:: format_candidate(candidate: curated_feed.models.Candidate, number: int, *, with_id: bool) -> str One candidate as two lines: the reference line, then the summary. The model refers to articles by number, so the numbering is the contract. Ids are for a human tracing a pick back to the corpus and are left out of the prompt, being long opaque hashes that models mangle. >>> print(format_candidate(EXAMPLE_CANDIDATE, 1, with_id=False)) [1] A title — A source — 2026-08-08T09:14:00Z A summary. .. py:function:: render_candidates(candidates: collections.abc.Sequence[curated_feed.models.Candidate], *, with_ids: bool = False) -> str The day's candidates as a numbered list, one item per two lines. >>> print(render_candidates([EXAMPLE_CANDIDATE])) [1] A title — A source — 2026-08-08T09:14:00Z A summary. .. py:function:: build_prompt(candidates: collections.abc.Sequence[curated_feed.models.Candidate], *, policy_path: pathlib.Path, prompt_path: pathlib.Path | None = None) -> Prompt Assemble the prompt for one day. `prompt_path` overrides the packaged scoring prompt. Nothing in the pipeline passes it: it exists so that a candidate prompt can be tried against a saved day without reinstalling, which is the only way to evaluate a prompt change once the corpus and the engine live in different repositories.