plan: held-out personas are mapped to the plan's needs by a model, ids only
The one manual step left after `plan` was mapping the attached personas' `need:` to whatever ids the model chose. A second call now does it: the model sees the plan's needs and the persona texts and returns ids only, after the plan is final, so no persona wording can reach a page. An id the plan lacks is ignored and the reader is told. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b1f0210691
commit
209e9d1258
+56
-15
@@ -141,19 +141,12 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
.unwrap_or(4);
|
||||
let intake =
|
||||
std::fs::read_to_string(&intake_path).with_context(|| format!("reading {intake_path}"))?;
|
||||
let personas = match value("--personas") {
|
||||
let mut personas: Option<serde_yaml::Value> = match value("--personas") {
|
||||
Some(path) => {
|
||||
let text = std::fs::read_to_string(&path).with_context(|| format!("reading {path}"))?;
|
||||
let doc: serde_yaml::Value =
|
||||
serde_yaml::from_str(&text).with_context(|| format!("parsing {path}"))?;
|
||||
let list = doc
|
||||
.get("personas")
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("{path} has no `personas:`"))?;
|
||||
Some(serde_yaml::to_string(&serde_yaml::Mapping::from_iter([(
|
||||
serde_yaml::Value::from("personas"),
|
||||
list,
|
||||
)]))?)
|
||||
Some(doc.get("personas").cloned().ok_or_else(|| anyhow!("{path} has no `personas:`"))?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
@@ -172,13 +165,23 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
Ok(plan) => {
|
||||
let mut text = yaml.trim_end().to_string();
|
||||
text.push('\n');
|
||||
if let Some(p) = &personas {
|
||||
text.push_str("\n# Held out from every model that writes words.\n");
|
||||
text.push_str(p);
|
||||
}
|
||||
// The attached personas were written against someone
|
||||
// else's need ids. The person reading the plan maps
|
||||
// them; the mismatches are listed so they know which.
|
||||
// else's need ids. The plan is final by now, so a model
|
||||
// may map them to this plan's ids without its words
|
||||
// reaching any page: it returns ids and nothing else,
|
||||
// and an id not in the plan is ignored. What it leaves
|
||||
// unmapped is listed for the reader.
|
||||
if let Some(list) = personas.as_mut() {
|
||||
let mapped = map_personas(&cmd, &plan, list)?;
|
||||
if mapped > 0 {
|
||||
eprintln!("plan round {round}: {mapped} persona(s) mapped to this plan's needs");
|
||||
}
|
||||
text.push_str("\n# Held out from every model that writes words.\n");
|
||||
text.push_str(&serde_yaml::to_string(&serde_yaml::Mapping::from_iter([(
|
||||
serde_yaml::Value::from("personas"),
|
||||
list.clone(),
|
||||
)]))?);
|
||||
}
|
||||
let whole: Plan =
|
||||
serde_yaml::from_str(&text).context("the plan with its personas attached")?;
|
||||
let strangers: Vec<String> = whole
|
||||
@@ -240,6 +243,44 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Personas whose `need:` is not one of the plan's ids get one mapped
|
||||
/// by a model that sees the plan's needs and the persona texts and
|
||||
/// returns ids only. Returns how many were mapped; an id the plan does
|
||||
/// not have, or a persona the model leaves out, stays as it was.
|
||||
fn map_personas(cmd: &str, plan: &Plan, personas: &mut serde_yaml::Value) -> anyhow::Result<usize> {
|
||||
let ids: Vec<&str> = plan.needs.iter().map(|n| n.id.as_str()).collect();
|
||||
let Some(list) = personas.as_sequence_mut() else { return Ok(0) };
|
||||
let strangers: Vec<(String, String)> = list
|
||||
.iter()
|
||||
.filter(|p| !ids.contains(&p["need"].as_str().unwrap_or_default()))
|
||||
.map(|p| (p["id"].as_str().unwrap_or_default().to_string(), p["text"].as_str().unwrap_or_default().to_string()))
|
||||
.filter(|(id, _)| !id.is_empty())
|
||||
.collect();
|
||||
if strangers.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let needs: Vec<String> = plan.needs.iter().map(|n| format!("- {}: {} - wants {}", n.id, n.who, n.wants)).collect();
|
||||
let people: Vec<String> = strangers.iter().map(|(id, text)| format!("- {id}: {text}")).collect();
|
||||
let prompt = format!(
|
||||
"A site has these needs, one per kind of visitor:\n{}\n\nThese people arrive at it:\n{}\n\nFor each person, which need is theirs? Reply with a JSON object mapping each person's id to one need id from the list, and nothing else. Leave a person out if no need fits.",
|
||||
needs.join("\n"),
|
||||
people.join("\n")
|
||||
);
|
||||
let reply = crate::llm_with(cmd, &prompt, &[("FORMAT", "json"), ("TEMPERATURE", "0.0")])?;
|
||||
let Some(mapping) = first_object(&reply) else { return Ok(0) };
|
||||
let mut mapped = 0;
|
||||
for persona in list.iter_mut() {
|
||||
let id = persona["id"].as_str().unwrap_or_default().to_string();
|
||||
if let Some(need) = mapping.get(&id).and_then(|v| v.as_str()) {
|
||||
if ids.contains(&need) && !ids.contains(&persona["need"].as_str().unwrap_or_default()) {
|
||||
persona["need"] = serde_yaml::Value::from(need);
|
||||
mapped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
/// Parsed and validated exactly as `build` will.
|
||||
fn check(yaml: &str) -> anyhow::Result<Plan> {
|
||||
let plan: Plan =
|
||||
|
||||
Reference in New Issue
Block a user