use crate::IngestError;
use amberfork_model::{Outcome, Payload, Run, SchemaVersion, Step, StepKind};
use serde::Deserialize;
use serde_json::{Map, Value};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Split {
HandCrafted,
AlgorithmGenerated,
}
impl Split {
const fn slug(self) -> &'static str {
match self {
Self::HandCrafted => "hand",
Self::AlgorithmGenerated => "algo",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GoldStep {
Valid(usize),
Absent,
Unusable(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WhoWhenGold {
pub split: Split,
pub question_id: String,
pub gold_step: GoldStep,
pub mistake_agent: Option<String>,
pub ground_truth: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Converted {
pub run: Run,
pub gold: WhoWhenGold,
}
pub fn convert_str(raw_json: &str, split: Split, stem: &str) -> Result<Converted, IngestError> {
let raw: RawLog = serde_json::from_str(raw_json)
.map_err(|source| IngestError::Parse { path: None, source })?;
let steps: Vec<Step> = raw
.history
.iter()
.enumerate()
.map(|(idx, entry)| entry.to_step(idx))
.collect();
let gold = WhoWhenGold {
split,
question_id: raw.question_id,
gold_step: resolve_gold_step(raw.mistake_step.as_ref(), steps.len()),
mistake_agent: nonempty(raw.mistake_agent.as_deref()).map(normalize_agent_name),
ground_truth: raw.ground_truth.or(raw.groundtruth).unwrap_or_default(),
};
let run = Run {
schema_version: SchemaVersion::current(),
id: format!("whowhen_{}_{stem}", split.slug()),
task: (!raw.question.is_empty()).then_some(raw.question),
outcome: Some(Outcome::Fail),
steps,
edges: None,
};
Ok(Converted { run, gold })
}
pub fn convert_file(path: impl AsRef<Path>, split: Split) -> Result<Converted, IngestError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|source| IngestError::Io {
path: path.to_path_buf(),
source,
})?;
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
convert_str(&text, split, stem)
}
#[derive(Deserialize)]
struct RawLog {
#[serde(default)]
question: String,
#[serde(default, rename = "question_ID")]
question_id: String,
#[serde(default)]
history: Vec<RawEntry>,
#[serde(default)]
mistake_agent: Option<String>,
#[serde(default)]
mistake_step: Option<Value>,
#[serde(default)]
ground_truth: Option<String>,
#[serde(default)]
groundtruth: Option<String>,
}
#[derive(Deserialize)]
struct RawEntry {
#[serde(default)]
content: Option<String>,
#[serde(default)]
role: Option<String>,
#[serde(default)]
name: Option<String>,
}
impl RawEntry {
fn to_step(&self, idx: usize) -> Step {
let speaker = nonempty(self.name.as_deref())
.or_else(|| nonempty(self.role.as_deref()))
.unwrap_or("unknown");
Step {
idx,
kind: StepKind::Agent,
name: normalize_agent_name(speaker),
inputs: None,
outputs: nonempty(self.content.as_deref()).map(|c| Payload::Text(c.to_string())),
attrs: Map::new(),
t_start: None,
t_end: None,
parent_idx: None,
}
}
}
fn resolve_gold_step(raw: Option<&Value>, n_steps: usize) -> GoldStep {
let value = match raw {
None | Some(Value::Null) => return GoldStep::Absent,
Some(value) => value,
};
let parsed = match value {
Value::String(s) => s.trim().parse::<usize>().ok(),
Value::Number(n) => n.as_u64().and_then(|u| usize::try_from(u).ok()),
_ => None,
};
match parsed {
Some(idx) if idx < n_steps => GoldStep::Valid(idx),
_ => GoldStep::Unusable(display_raw(value)),
}
}
fn display_raw(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn normalize_agent_name(name: &str) -> String {
name.replace("Websurfer", "WebSurfer")
}
fn nonempty(s: Option<&str>) -> Option<&str> {
s.filter(|t| !t.is_empty())
}