diff --git a/src/plan.rs b/src/plan.rs index fe88920..683114c 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -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 = 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 = 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 { + 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 = plan.needs.iter().map(|n| format!("- {}: {} - wants {}", n.id, n.who, n.wants)).collect(); + let people: Vec = 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 { let plan: Plan =