bindings: colophon's own question, and the reply folded back in

colophon has composed the question about what its pages did not settle
since it was written, and merged a reply back into the record it read.
Both were reachable only from a terminal, so a pipeline wanting the same
fallback would have had to write its own prompt — a second one, drifting
from the first, asking for a shape merge does not accept.

merge fills only the unsettled fields, so a model cannot overwrite what a
book printed about itself. That is the property that makes asking one
safe, and it belongs to colophon rather than to any caller.
This commit is contained in:
funkpower
2026-09-21 10:49:51 -04:00
parent f4497b3f0a
commit 0416359c65
3 changed files with 60 additions and 0 deletions
@@ -16,6 +16,7 @@ from ._catena import (
chunk_markdown,
citation_prompt,
clean_record,
colophon_prompt,
clean_underscores,
compose_subjects,
count_tokens,
@@ -30,6 +31,7 @@ from ._catena import (
looks_like_footnote_citations,
looks_like_person,
merge_citation_reply,
merge_colophon,
needs_subject,
pack_cues,
parse_identifier,
@@ -55,6 +57,7 @@ __all__ = [
"chunk_markdown",
"citation_prompt",
"clean_record",
"colophon_prompt",
"clean_underscores",
"compose_subjects",
"count_tokens",
@@ -69,6 +72,7 @@ __all__ = [
"looks_like_footnote_citations",
"looks_like_person",
"merge_citation_reply",
"merge_colophon",
"needs_subject",
"pack_cues",
"parse_identifier",
@@ -102,3 +102,22 @@ def fetch_identifier(
Blocks on the network. `None` when no register answers about that kind.
"""
...
def colophon_prompt(
record: dict[str, Any], text: str, pages: int = ...
) -> str | None:
"""The question to put to a model about what the pages did not settle.
None when the pages settled everything. Nothing is sent anywhere.
"""
...
def merge_colophon(
record: dict[str, Any], reply: str
) -> tuple[dict[str, Any], int]:
"""A model's reply folded in. Returns (record, fields_settled).
Only fills what the pages left unsettled; a model never overwrites what a
book printed about itself.
"""
...
+37
View File
@@ -434,6 +434,8 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_function(wrap_pyfunction!(read_colophon, module)?)?;
module.add_function(wrap_pyfunction!(shelve, module)?)?;
module.add_function(wrap_pyfunction!(fetch_identifier, module)?)?;
module.add_function(wrap_pyfunction!(colophon_prompt, module)?)?;
module.add_function(wrap_pyfunction!(merge_colophon, module)?)?;
Ok(())
}
@@ -509,3 +511,38 @@ fn fetch_identifier<'py>(
None => Ok(None),
}
}
/// The question to put to a model about the fields a book's pages did not
/// settle, or `None` when they settled everything.
///
/// Nothing is sent anywhere: this composes the text and hands it back, so the
/// caller decides which model, on whose hardware, at what cost. The reply goes
/// to `merge_colophon`.
#[pyfunction]
#[pyo3(signature = (record, text, pages = colophon::PAGES))]
fn colophon_prompt(
record: &Bound<'_, PyAny>,
text: &str,
pages: usize,
) -> PyResult<Option<String>> {
let printed: colophon::Printed = from_py(record)?;
Ok(colophon::prompt::compose(&printed, text, pages))
}
/// A model's reply folded into the record the pages gave.
///
/// Only the fields the pages left unsettled are filled: a model cannot
/// overwrite something a book printed about itself, which is the whole reason
/// the pages are read first. Returns the completed record and how many fields
/// the reply actually settled.
#[pyfunction]
fn merge_colophon<'py>(
py: Python<'py>,
record: &Bound<'py, PyAny>,
reply: &str,
) -> PyResult<(Bound<'py, PyAny>, usize)> {
let mut printed: colophon::Printed = from_py(record)?;
let filled = colophon::prompt::merge(&mut printed, reply)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
Ok((to_py(py, &printed)?, filled))
}