use serde::Deserialize;
use super::scorer::{BinaryQuestion, BinaryVerdict, Judge, Scorecard, score};
use crate::brain::provider::Message;
#[derive(Debug, Clone, Deserialize)]
pub struct DatasetMessage {
pub role: String,
pub text: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FactProbe {
pub dimension: String,
pub question: String,
#[serde(default)]
pub expect_keywords: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CompactionDataset {
pub name: String,
pub conversation: Vec<DatasetMessage>,
pub probes: Vec<FactProbe>,
}
impl CompactionDataset {
pub fn from_json(json: &str) -> serde_json::Result<Self> {
serde_json::from_str(json)
}
pub fn seed() -> Self {
Self::from_json(SEED_DATASET_JSON).expect("seed dataset is valid JSON")
}
pub fn messages(&self) -> Vec<Message> {
self.conversation
.iter()
.map(|m| match m.role.as_str() {
"assistant" => Message::assistant(m.text.clone()),
"system" => Message::system(m.text.clone()),
_ => Message::user(m.text.clone()),
})
.collect()
}
pub fn questions(&self) -> Vec<BinaryQuestion> {
self.probes
.iter()
.map(|p| BinaryQuestion::new(p.dimension.clone(), p.question.clone()))
.collect()
}
pub fn keyword_scorecard(&self, summary: &str) -> Scorecard {
let haystack = summary.to_ascii_lowercase();
let results = self
.probes
.iter()
.map(|p| {
let survived = !p.expect_keywords.is_empty()
&& p.expect_keywords
.iter()
.all(|k| haystack.contains(&k.to_ascii_lowercase()));
let missing: Vec<&str> = p
.expect_keywords
.iter()
.filter(|k| !haystack.contains(&k.to_ascii_lowercase()))
.map(|k| k.as_str())
.collect();
let explanation = if survived {
None
} else if p.expect_keywords.is_empty() {
Some("no keywords to check offline".to_string())
} else {
Some(format!("missing: {}", missing.join(", ")))
};
(
BinaryQuestion::new(p.dimension.clone(), p.question.clone()),
BinaryVerdict {
yes: survived,
explanation,
},
)
})
.collect();
Scorecard::from_verdicts(results)
}
pub async fn judge_scorecard(&self, judge: &dyn Judge, summary: &str) -> Scorecard {
score(judge, self.questions(), summary).await
}
}
const SEED_DATASET_JSON: &str = r#"{
"name": "seed-coding-session",
"conversation": [
{ "role": "user", "text": "Refactor the retry logic in net/client.rs. Always use tabs, never spaces." },
{ "role": "assistant", "text": "Understood. I will edit net/client.rs and keep tab indentation." },
{ "role": "assistant", "text": "Edited net/client.rs: extracted retry_with_backoff. The test suite fails with error E0433: unresolved import crate::net::Backoff." },
{ "role": "user", "text": "Fix the import, then also update the CHANGELOG before you finish." },
{ "role": "assistant", "text": "Import fixed. Pending: update the CHANGELOG entry for the retry refactor." }
],
"probes": [
{ "dimension": "user_prefs", "question": "Does the summary preserve the tabs-not-spaces indentation preference?", "expect_keywords": ["tabs"] },
{ "dimension": "pending_tasks", "question": "Does the summary keep the pending CHANGELOG update task?", "expect_keywords": ["CHANGELOG"] },
{ "dimension": "files_modified", "question": "Does the summary record the edit to net/client.rs?", "expect_keywords": ["net/client.rs"] },
{ "dimension": "errors", "question": "Does the summary retain the E0433 import error encountered?", "expect_keywords": ["E0433"] }
]
}"#;