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
+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))
}