pub const DECOMPOSE_TASK_SYSTEM: &str = "\ You are a task-decomposition assistant. Given a task description, produce \ between 3 and 7 concrete, physical micro-steps. Each step must be a short \ imperative sentence, actionable today, with no commentary. Output ONLY a \ JSON array of strings."; // Phase 9 content-tag extraction. The model emits a {topic, intent} // JSON pair under a strict GBNF (see grammars::CONTENT_TAGS_GRAMMAR). // CONTENT_TAGS_SYSTEM is the system message; the user message wraps // the transcript text. pub const CONTENT_TAGS_SYSTEM: &str = "\ You tag a transcript with ONE topic and ONE intent. \ TOPIC is a 1 to 3 token lowercase hyphen-joined noun phrase naming the \ dominant subject. Examples: interview-prep, grant-application, \ daily-standup. \ INTENT is exactly one of: planning, reflection, venting, capture, \ decision, question. \ Return JSON only, with this exact shape: \ {\"topic\":\"...\",\"intent\":\"...\"}"; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ContentTags { pub topic: String, pub intent: String, } pub const INTENT_CLOSED_SET: &[&str] = &[ "planning", "reflection", "venting", "capture", "decision", "question", ]; pub fn is_valid_intent(s: &str) -> bool { INTENT_CLOSED_SET.contains(&s) } pub const EXTRACT_TASKS_SYSTEM: &str = "\ You are a task-extraction assistant. Given a transcript of spoken notes, \ output a JSON array of action items the speaker committed to. Each item must \ be a short imperative sentence. Omit observations, wishes, and background \ context that are not explicit commitments. Output an empty array if there are \ no action items."; /// Compact representation of a human-in-the-loop feedback example used /// for few-shot prompt conditioning. Built by kon-storage and fed to the /// prompt builder below; we keep this struct local to the LLM crate so /// kon-llm does not depend on kon-storage. #[derive(Debug, Clone)] pub struct FeedbackExample { /// What the AI was given as input (e.g. the parent task text, or /// the transcript chunk). Kept verbatim. pub input: String, /// What the AI produced originally. `None` if the user only /// gave a thumbs-up without a prior edit (positive signal /// without a paired correction). pub original_output: Option, /// What the user changed it to. `None` for thumbs-only rows. /// This is the highest-value signal — when present, inject it /// as the "good" output in the few-shot example. pub corrected_output: Option, } /// Render a feedback example into the exemplar block used in prompt /// conditioning. Returns `None` for rows that carry no usable pairing /// (e.g. a thumbs-up with no input context). fn render_feedback_exemplar(ex: &FeedbackExample) -> Option { if ex.input.trim().is_empty() { return None; } let good = ex .corrected_output .as_deref() .or(ex.original_output.as_deref())?; let good = good.trim(); if good.is_empty() { return None; } Some(format!("Input: {}\nGood output: {}", ex.input.trim(), good)) } /// Build a system prompt that combines the base task system prompt /// with a few-shot block assembled from recent HITL examples. If no /// usable examples are available, returns the base prompt unchanged /// so early users see the generic behaviour and the LLM is not /// confused by an empty exemplar section. /// /// The exemplars are ordered most-recent-first (caller's order is /// preserved) so the LLM weights the user's current style over /// earlier noise, mirroring what a human reviewer would do. pub fn build_conditioned_system_prompt(base: &str, examples: &[FeedbackExample]) -> String { let rendered: Vec = examples .iter() .filter_map(render_feedback_exemplar) .collect(); if rendered.is_empty() { return base.to_string(); } let block = rendered .iter() .map(|s| format!("- {s}")) .collect::>() .join("\n"); format!( "{base}\n\nHere are examples of the style this user prefers, in the \ user's own words. Match this style closely when producing your output:\n{block}" ) } #[cfg(test)] mod tests { use super::*; #[test] fn builds_plain_prompt_when_no_examples() { let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &[]); assert_eq!(out, DECOMPOSE_TASK_SYSTEM); } #[test] fn skips_empty_input_examples() { let examples = vec![FeedbackExample { input: String::new(), original_output: None, corrected_output: Some("ignored".into()), }]; let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &examples); assert_eq!(out, DECOMPOSE_TASK_SYSTEM); } #[test] fn prefers_corrected_over_original() { let examples = vec![FeedbackExample { input: "Clean room".into(), original_output: Some("Organise your bedroom".into()), corrected_output: Some("Pick up one shirt from the floor".into()), }]; let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &examples); assert!(out.contains("Pick up one shirt from the floor")); assert!(!out.contains("Organise your bedroom")); } #[test] fn falls_back_to_original_when_no_correction() { let examples = vec![FeedbackExample { input: "Write report".into(), original_output: Some("Open a blank document".into()), corrected_output: None, }]; let out = build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, &examples); assert!(out.contains("Open a blank document")); } }