pub mod dataset_summary;
pub mod grounded;
pub use dataset_summary::summarize_dataset;
pub use grounded::{GroundedProposer, ProposeRequest};
use typesayer_types::field::FieldValue;
use crate::{example::Example, format::serialize_value, module::Module};
#[must_use]
pub fn format_example_as_text(example: &Example) -> String {
example
.fields()
.iter()
.map(|(k, v)| format!("{k}: {}", format_field_value(v)))
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn format_examples_batch(examples: &[Example]) -> String {
examples
.iter()
.enumerate()
.map(|(i, ex)| format!("Example {}:\n{}", i + 1, format_example_as_text(ex)))
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn describe_module_structure(module: &dyn Module) -> String {
let predictors = module.named_predictors();
let mut lines = vec![format!("Module with {} predictor(s):", predictors.len())];
for (i, (name, predict)) in predictors.iter().enumerate() {
let sig = predict.signature();
let inputs: Vec<String> = sig
.input_fields()
.map(|f| format!("{} ({})", f.name, f.field_type.type_label()))
.collect();
let outputs: Vec<String> = sig
.output_fields()
.map(|f| format!("{} ({})", f.name, f.field_type.type_label()))
.collect();
lines.push(format!(
"{}. \"{}\" — inputs: {} → outputs: {}",
i + 1,
name,
inputs.join(", "),
outputs.join(", "),
));
lines.push(format!(
" Current instruction: \"{}\"",
sig.instructions()
));
}
lines.join("\n")
}
#[must_use]
pub fn strip_instruction_prefix(instruction: &str) -> String {
let trimmed = instruction.trim();
let lower = trimmed.to_lowercase();
let stripped = if lower.starts_with("new instruction:") {
&trimmed["new instruction:".len()..]
} else if lower.starts_with("instruction:") {
&trimmed["instruction:".len()..]
} else if lower.starts_with("here is the instruction:") {
&trimmed["here is the instruction:".len()..]
} else if lower.starts_with("proposed instruction:") {
&trimmed["proposed instruction:".len()..]
} else {
trimmed
};
let stripped = stripped.trim();
let stripped = stripped.strip_prefix('"').unwrap_or(stripped);
let stripped = stripped.strip_suffix('"').unwrap_or(stripped);
stripped.trim().to_owned()
}
fn format_field_value(value: &FieldValue) -> String {
serialize_value(value)
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashSet};
use async_trait::async_trait;
use typesayer_types::{
error::Result,
field::{FieldDef, FieldType},
signature::Signature,
};
use super::*;
use crate::{context::Context, predict::Predict, prediction::Prediction};
fn qa_signature() -> Signature {
Signature::builder("Answer the question.")
.input(FieldDef::input(
"question",
FieldType::String,
"The question",
))
.output(FieldDef::output("answer", FieldType::String, "The answer"))
.build()
.unwrap()
}
struct TwoStepModule {
classify: Predict,
answer: Predict,
}
#[async_trait]
impl Module for TwoStepModule {
async fn forward(
&self,
_inputs: BTreeMap<String, FieldValue>,
_ctx: &Context,
) -> Result<Prediction> {
unimplemented!("not needed for structure tests")
}
fn named_predictors(&self) -> Vec<(String, &Predict)> {
vec![
("classify".to_owned(), &self.classify),
("answer".to_owned(), &self.answer),
]
}
fn named_predictors_mut(&mut self) -> Vec<(String, &mut Predict)> {
vec![
("classify".to_owned(), &mut self.classify),
("answer".to_owned(), &mut self.answer),
]
}
fn deep_clone(&self) -> Box<dyn Module> {
Box::new(Self {
classify: self.classify.clone(),
answer: self.answer.clone(),
})
}
}
#[test]
fn format_example_basic() {
let ex = Example::new(
BTreeMap::from([
("question".into(), FieldValue::Str("What is 2+2?".into())),
("answer".into(), FieldValue::Str("4".into())),
]),
HashSet::from(["question".into()]),
);
let text = format_example_as_text(&ex);
assert!(text.contains("answer: 4"));
assert!(text.contains("question: What is 2+2?"));
}
#[test]
fn format_example_handles_types() {
let ex = Example::new(
BTreeMap::from([
("count".into(), FieldValue::Int(42)),
("active".into(), FieldValue::Bool(true)),
("name".into(), FieldValue::Str("test".into())),
("empty".into(), FieldValue::Null),
]),
HashSet::from(["name".into()]),
);
let text = format_example_as_text(&ex);
assert!(text.contains("count: 42"));
assert!(text.contains("active: true"));
assert!(text.contains("name: test"));
assert!(text.contains("empty: null"));
}
#[test]
fn describe_module_single_predictor() {
struct SingleModule {
qa: Predict,
}
#[async_trait]
impl Module for SingleModule {
async fn forward(
&self,
inputs: BTreeMap<String, FieldValue>,
_ctx: &Context,
) -> Result<Prediction> {
Ok(Prediction::new(inputs, None))
}
fn named_predictors(&self) -> Vec<(String, &Predict)> {
vec![("qa".to_owned(), &self.qa)]
}
fn named_predictors_mut(&mut self) -> Vec<(String, &mut Predict)> {
vec![("qa".to_owned(), &mut self.qa)]
}
fn deep_clone(&self) -> Box<dyn Module> {
Box::new(Self {
qa: self.qa.clone(),
})
}
}
let module = SingleModule {
qa: Predict::new(qa_signature()),
};
let desc = describe_module_structure(&module);
assert!(desc.contains("1 predictor"));
assert!(desc.contains("\"qa\""));
assert!(desc.contains("question (str)"));
assert!(desc.contains("answer (str)"));
assert!(desc.contains("Answer the question."));
}
#[test]
fn describe_module_two_predictors() {
let classify_sig = Signature::builder("Classify the topic.")
.input(FieldDef::input("question", FieldType::String, "q"))
.output(FieldDef::output("topic", FieldType::String, "t"))
.build()
.unwrap();
let answer_sig = Signature::builder("Answer given topic.")
.input(FieldDef::input("question", FieldType::String, "q"))
.input(FieldDef::input("topic", FieldType::String, "t"))
.output(FieldDef::output("answer", FieldType::String, "a"))
.build()
.unwrap();
let module = TwoStepModule {
classify: Predict::new(classify_sig),
answer: Predict::new(answer_sig),
};
let desc = describe_module_structure(&module);
assert!(desc.contains("2 predictor"));
assert!(desc.contains("\"classify\""));
assert!(desc.contains("\"answer\""));
assert!(desc.contains("topic (str)"));
}
#[test]
fn strip_prefix_instruction() {
assert_eq!(
strip_instruction_prefix("Instruction: Do the thing"),
"Do the thing"
);
}
#[test]
fn strip_prefix_new_instruction() {
assert_eq!(
strip_instruction_prefix("New instruction: Do the thing"),
"Do the thing"
);
}
#[test]
fn strip_prefix_with_quotes() {
assert_eq!(strip_instruction_prefix("\"Do the thing\""), "Do the thing");
}
#[test]
fn strip_prefix_clean_input() {
assert_eq!(strip_instruction_prefix("Do the thing"), "Do the thing");
}
#[test]
fn strip_prefix_case_insensitive() {
assert_eq!(
strip_instruction_prefix("INSTRUCTION: Do the thing"),
"Do the thing"
);
}
#[test]
fn strip_prefix_combined() {
assert_eq!(
strip_instruction_prefix("New Instruction: \"Do the thing\""),
"Do the thing"
);
}
}