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, PartialEq, Eq)]
pub struct TapeMeta {
pub task_id: Option<String>,
pub final_answer: String,
pub result: String,
}
impl TapeMeta {
#[must_use]
pub fn is_success(&self) -> bool {
normalize(&self.result) == normalize(&self.final_answer)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConvertedTape {
pub run: Run,
pub meta: TapeMeta,
}
pub fn convert_str(raw_json: &str, stem: &str) -> Result<ConvertedTape, IngestError> {
let raw: RawTape = serde_json::from_str(raw_json)
.map_err(|source| IngestError::Parse { path: None, source })?;
let steps: Vec<Step> = raw
.steps
.into_iter()
.enumerate()
.map(|(idx, node)| node.into_step(idx))
.collect();
let task = raw.metadata.task.unwrap_or_default();
let meta = TapeMeta {
task_id: nonempty(task.task_id),
final_answer: task.final_answer,
result: raw.metadata.result.map(stringify).unwrap_or_default(),
};
let outcome = if meta.is_success() {
Outcome::Pass
} else {
Outcome::Fail
};
let run = Run {
schema_version: SchemaVersion::current(),
id: format!("tape_{stem}"),
task: nonempty(task.question),
outcome: Some(outcome),
steps,
edges: None,
};
Ok(ConvertedTape { run, meta })
}
pub fn convert_file(path: impl AsRef<Path>) -> Result<ConvertedTape, 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, stem)
}
#[derive(Deserialize)]
struct RawTape {
#[serde(default)]
metadata: RawTapeMetadata,
#[serde(default)]
steps: Vec<RawNode>,
}
#[derive(Deserialize, Default)]
struct RawTapeMetadata {
#[serde(default, deserialize_with = "object_or_none")]
task: Option<RawTask>,
#[serde(default)]
result: Option<Value>,
}
#[derive(Deserialize, Default)]
struct RawTask {
#[serde(default, rename = "Question")]
question: String,
#[serde(default)]
task_id: String,
#[serde(default, rename = "Final answer")]
final_answer: String,
}
#[derive(Deserialize)]
struct RawNode {
#[serde(default)]
kind: Option<String>,
#[serde(default)]
metadata: Option<RawNodeMetadata>,
#[serde(flatten)]
body: Map<String, Value>,
}
#[derive(Deserialize)]
struct RawNodeMetadata {
#[serde(default)]
agent: Option<String>,
}
impl RawNode {
fn into_step(self, idx: usize) -> Step {
let kind = nonempty(self.kind.unwrap_or_default()).unwrap_or_else(|| "step".to_string());
let agent = self.metadata.and_then(|m| m.agent).and_then(nonempty);
let name = match agent {
Some(agent) => format!("{agent}:{kind}"),
None => kind,
};
let outputs = (!self.body.is_empty()).then_some(Payload::Object(self.body));
Step {
idx,
kind: StepKind::Agent,
name,
inputs: None,
outputs,
attrs: Map::new(),
t_start: None,
t_end: None,
parent_idx: None,
}
}
}
fn normalize(s: &str) -> String {
s.trim().to_lowercase()
}
fn stringify(value: Value) -> String {
match value {
Value::String(s) => s,
other => other.to_string(),
}
}
fn nonempty(s: String) -> Option<String> {
(!s.is_empty()).then_some(s)
}
fn object_or_none<'de, D>(deserializer: D) -> Result<Option<RawTask>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match value {
Value::Object(_) => RawTask::deserialize(value)
.map(Some)
.map_err(serde::de::Error::custom),
_ => Ok(None),
}
}