use std::error::Error;
use std::fs::File;
use std::io::{BufWriter, Write as _};
use std::path::Path;
use serde::Serialize;
use crate::dataset::Qa;
use crate::eval::{EvalCfg, RetrievedFact};
use crate::ollama_gen::TokenUsage;
#[derive(Serialize)]
pub struct FactRecord {
pub rank: usize,
pub id: u64,
pub ts: i64,
pub vector_score: f64,
pub graph_weight: f64,
pub evidence_hit: bool,
pub dia_ids: Vec<String>,
pub text: String,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize)]
pub struct QuestionRecord {
pub conversation_id: String,
pub question_idx: usize,
pub graph_on: bool,
pub category: String,
pub question: String,
pub gold: Option<String>,
pub predicted: String,
pub correct: bool,
pub f1: Option<f64>,
pub evidence_hit: bool,
pub date_on: bool,
pub scaffold_on: bool,
pub prompt_kind: String,
pub is_temporal_trigger: bool,
pub latest_ts: i64,
pub prompt_tokens: Option<u64>,
pub completion_tokens: Option<u64>,
pub context_tokens: usize,
pub n_distinct_dates: usize,
pub date_span_days: i64,
pub raw_facts: Vec<FactRecord>,
pub reranked_facts: Vec<FactRecord>,
}
pub struct DumpSink {
writer: BufWriter<File>,
}
impl DumpSink {
pub fn create(path: &Path) -> Result<Self, Box<dyn Error>> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(Self {
writer: BufWriter::new(File::create(path)?),
})
}
fn write(&mut self, record: &QuestionRecord) -> Result<(), Box<dyn Error>> {
serde_json::to_writer(&mut self.writer, record)?;
self.writer.write_all(b"\n")?;
self.writer.flush()?;
Ok(())
}
}
pub struct QuestionTrace<'a> {
pub conversation_id: &'a str,
pub question_idx: usize,
pub sink: &'a mut DumpSink,
}
#[allow(clippy::struct_excessive_bools)]
pub struct RecordInputs<'a> {
pub qa: &'a Qa,
pub cfg: EvalCfg,
pub graph_on: bool,
pub raw: &'a [RetrievedFact],
pub reranked: &'a [RetrievedFact],
pub candidate: &'a str,
pub usage: Option<TokenUsage>,
pub correct: bool,
pub f1: Option<f64>,
pub evidence_hit: bool,
pub date_on: bool,
pub scaffold_on: bool,
pub is_temporal_trigger: bool,
pub latest_ts: i64,
}
#[allow(clippy::needless_pass_by_value)]
pub fn write_record(
trace: QuestionTrace<'_>,
inputs: &RecordInputs<'_>,
) -> Result<(), Box<dyn Error>> {
let raw_facts = to_fact_records(inputs.raw, &inputs.qa.evidence);
let reranked_facts = to_fact_records(inputs.reranked, &inputs.qa.evidence);
let (n_distinct_dates, date_span_days) = date_span(&reranked_facts);
let context_tokens = reranked_facts
.iter()
.map(|f| estimate_tokens(&f.text))
.sum();
let prompt_kind = if inputs.scaffold_on {
"scaffold"
} else if inputs.cfg.cot {
"cot"
} else {
"terse"
};
let record = QuestionRecord {
conversation_id: trace.conversation_id.to_string(),
question_idx: trace.question_idx,
graph_on: inputs.graph_on,
category: inputs.qa.category.label().to_string(),
question: inputs.qa.question.clone(),
gold: inputs.qa.answer.clone(),
predicted: inputs.candidate.to_string(),
correct: inputs.correct,
f1: inputs.f1,
evidence_hit: inputs.evidence_hit,
date_on: inputs.date_on,
scaffold_on: inputs.scaffold_on,
prompt_kind: prompt_kind.to_string(),
is_temporal_trigger: inputs.is_temporal_trigger,
latest_ts: inputs.latest_ts,
prompt_tokens: inputs.usage.map(|u| u.prompt),
completion_tokens: inputs.usage.map(|u| u.completion),
context_tokens,
n_distinct_dates,
date_span_days,
raw_facts,
reranked_facts,
};
trace.sink.write(&record)
}
fn to_fact_records(facts: &[RetrievedFact], evidence: &[String]) -> Vec<FactRecord> {
facts
.iter()
.enumerate()
.map(|(rank, f)| FactRecord {
rank,
id: f.id,
ts: f.ts,
vector_score: f.score,
graph_weight: f.graph_weight,
evidence_hit: f.dia_ids.iter().any(|id| evidence.contains(id)),
dia_ids: f.dia_ids.clone(),
text: f.text.clone(),
})
.collect()
}
fn estimate_tokens(text: &str) -> usize {
text.split_whitespace().count()
}
fn date_span(facts: &[FactRecord]) -> (usize, i64) {
let mut days: Vec<i64> = facts
.iter()
.map(|f| f.ts)
.filter(|&ts| ts > 0)
.filter_map(ymd_to_ordinal)
.collect();
days.sort_unstable();
days.dedup();
let span = match (days.first(), days.last()) {
(Some(&lo), Some(&hi)) => hi - lo,
_ => 0,
};
(days.len(), span)
}
fn ymd_to_ordinal(ts: i64) -> Option<i64> {
let (year, month, day) = crate::judge::decompose_ymd(ts)?;
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let month_shifted = (month + 9) % 12;
let day_of_year = (153 * month_shifted + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + day_of_year;
Some(era * 146_097 + doe - 719_468)
}
#[cfg(test)]
#[path = "dump/dump_tests.rs"]
mod tests;