use std::collections::BTreeSet;
use serde_json::{json, Map, Value};
use super::export::{
export_metadata, normalized_message, record_id, record_string, ConvertedExport, ExportTarget,
};
pub(super) fn is_projected_example(record: &Map<String, Value>) -> bool {
record.get("schema_version").and_then(Value::as_str)
== Some(harn_vm::orchestration::TRAINING_EXAMPLE_SCHEMA_VERSION)
}
pub(super) fn convert_projected_example(
record: &Map<String, Value>,
target: &ExportTarget,
dataset_format: &str,
behavior_classes: &BTreeSet<String>,
) -> Result<ConvertedExport, String> {
let example: harn_vm::orchestration::AgentTrainingExample =
serde_json::from_value(Value::Object(record.clone()))
.map_err(|error| format!("row is not a valid projected training example: {error}"))?;
harn_vm::orchestration::validate_training_example_pairing(&example.messages)
.map_err(|error| error.to_string())?;
if example.tools.is_empty() {
return Err("projected example carries an empty tool catalog".to_string());
}
let tools = example
.tools
.iter()
.map(|row| {
harn_vm::orchestration::function_schema_from_catalog_row(row).ok_or_else(|| {
format!("projected example carries a tool catalog row Harn cannot render: {row}")
})
})
.collect::<Result<Vec<Value>, String>>()?;
let declared_format = example.provenance.effective_tool_format.as_str();
let system_text = example
.messages
.iter()
.filter(|message| message.role == "system")
.map(|message| message.content.as_str())
.collect::<Vec<_>>()
.join("\n");
let metadata = projected_metadata(
record,
target,
dataset_format,
&example,
&tools,
&system_text,
);
let tool_calls = example
.messages
.iter()
.map(|message| message.tool_calls.len() as u64)
.sum();
let tool_results = example
.messages
.iter()
.filter(|message| message.role == "tool")
.count() as u64;
if dataset_format == "messages_with_tool_calls" {
let row = json!({
"id": record_string(record, "id"),
"eval_name": record_string(record, "eval_name"),
"language": record_string(record, "language"),
"task_type": record_string(record, "task_type"),
"messages": example.messages,
"tools": tools,
"metadata": metadata,
});
return Ok(ConvertedExport {
rows: vec![row],
tool_calls,
tool_results,
behavior_classes: behavior_classes.clone(),
});
}
if declared_format != "text" && declared_format != "json" {
return Err(format!(
"cannot export a projected example served with tool_format={declared_format} as \
`{dataset_format}`; that dataset trains on text-channel assistant turns"
));
}
let mut rows = Vec::new();
let mut context: Vec<Value> = Vec::new();
for (index, message) in example.messages.iter().enumerate() {
if message.role == "assistant" && !message.tool_calls.is_empty() {
rows.push(json!({
"id": format!("{}#turn-{index}", record_id(record, index + 1)),
"source_id": record_string(record, "id"),
"eval_name": record_string(record, "eval_name"),
"language": record_string(record, "language"),
"task_type": record_string(record, "task_type"),
"messages": context.clone(),
"tools": tools.clone(),
"assistant_tool_text": message.content,
"metadata": metadata.clone(),
}));
}
let served_role = if message.role == "tool" {
"user"
} else {
message.role.as_str()
};
context.push(normalized_message(served_role, &message.content));
}
if rows.is_empty() {
return Err("projected example has no assistant tool-call turn to train on".to_string());
}
Ok(ConvertedExport {
rows,
tool_calls,
tool_results,
behavior_classes: behavior_classes.clone(),
})
}
fn projected_metadata(
record: &Map<String, Value>,
target: &ExportTarget,
dataset_format: &str,
example: &harn_vm::orchestration::AgentTrainingExample,
tools: &[Value],
system_text: &str,
) -> Value {
let mut metadata = export_metadata(
record,
target,
"harn_projected_run_v1",
dataset_format,
tools,
system_text,
&BTreeSet::new(),
);
if let Some(object) = metadata.as_object_mut() {
object.insert(
"source_projection".to_string(),
serde_json::to_value(&example.provenance).unwrap_or(Value::Null),
);
object.insert(
"tool_catalog_source".to_string(),
Value::String("projected".to_string()),
);
}
metadata
}