const MAX_AGENT_NAME_LENGTH: usize = 20;
#[derive(Debug, Clone)]
pub struct ParsingTraceStep {
pub step_number: usize,
pub description: String,
pub input: Option<String>,
pub result: Option<String>,
pub success: bool,
pub details: String,
}
impl ParsingTraceStep {
#[must_use]
pub fn new(step_number: usize, description: &str) -> Self {
Self {
step_number,
description: description.to_string(),
input: None,
result: None,
success: false,
details: String::new(),
}
}
#[must_use]
pub fn with_input(mut self, input: &str) -> Self {
const MAX_INPUT_SIZE: usize = 10_000;
self.input = if input.len() > MAX_INPUT_SIZE {
Some(format!(
"{}\n\n[... input truncated {} bytes ...]",
&input[..MAX_INPUT_SIZE / 2],
input.len() - MAX_INPUT_SIZE
))
} else {
Some(input.to_string())
};
self
}
#[must_use]
pub fn with_result(mut self, result: &str) -> Self {
const MAX_RESULT_SIZE: usize = 10_000;
self.result = if result.len() > MAX_RESULT_SIZE {
Some(format!(
"{}\n\n[... result truncated {} bytes ...]",
&result[..MAX_RESULT_SIZE / 2],
result.len() - MAX_RESULT_SIZE
))
} else {
Some(result.to_string())
};
self
}
#[must_use]
pub const fn with_success(mut self, success: bool) -> Self {
self.success = success;
self
}
#[must_use]
pub fn with_details(mut self, details: &str) -> Self {
self.details = details.to_string();
self
}
}
#[derive(Debug, Clone)]
pub struct ParsingTraceLog {
pub attempt_number: usize,
pub agent: String,
pub strategy: String,
pub raw_output: Option<String>,
pub steps: Vec<ParsingTraceStep>,
pub final_message: Option<String>,
pub timestamp: DateTime<Local>,
}
impl ParsingTraceLog {
#[must_use]
pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
Self {
attempt_number,
agent: agent.to_string(),
strategy: strategy.to_string(),
raw_output: None,
steps: Vec::new(),
final_message: None,
timestamp: Local::now(),
}
}
#[must_use]
pub fn with_raw_output(mut self, output: &str) -> Self {
const MAX_OUTPUT_SIZE: usize = 50_000;
self.raw_output = if output.len() > MAX_OUTPUT_SIZE {
Some(format!(
"{}\n\n[... raw output truncated {} bytes ...]\n\n{}",
&output[..MAX_OUTPUT_SIZE / 2],
output.len() - MAX_OUTPUT_SIZE,
&output[output.len() - MAX_OUTPUT_SIZE / 2..]
))
} else {
Some(output.to_string())
};
self
}
#[must_use]
pub fn add_step(mut self, step: ParsingTraceStep) -> Self {
self.steps = self.steps.into_iter().chain([step]).collect();
self
}
#[must_use]
pub fn with_final_message(mut self, message: &str) -> Self {
self.final_message = Some(message.to_string());
self
}
pub fn write_to_workspace(
&self,
log_dir: &Path,
workspace: &dyn Workspace,
) -> std::io::Result<PathBuf> {
let trace_path = log_dir.join(format!(
"attempt_{:03}_parsing_trace.log",
self.attempt_number
));
let content = Self::build_trace_content(self);
workspace.create_dir_all(log_dir)?;
workspace.write(&trace_path, &content)?;
Ok(trace_path)
}
#[must_use]
fn build_trace_content(trace: &Self) -> String {
[
Self::header_to_string(trace).as_str(),
&Self::raw_output_to_string(trace),
&Self::parsing_steps_to_string(trace),
&Self::final_message_to_string(trace),
Self::footer_to_string().as_str(),
]
.concat()
}
#[must_use]
fn header_to_string(trace: &Self) -> String {
format!(
"\
================================================================================
PARSING TRACE LOG - Attempt #{:03}
================================================================================
Agent: {}
Strategy: {}
Timestamp: {}
",
trace.attempt_number,
trace.agent,
trace.strategy,
trace.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
)
}
#[must_use]
fn raw_output_to_string(trace: &Self) -> String {
format!(
"\
--------------------------------------------------------------------------------
RAW AGENT OUTPUT
--------------------------------------------------------------------------------
{}
",
trace
.raw_output
.as_deref()
.unwrap_or("[No raw output captured]")
)
}
#[must_use]
fn parsing_steps_to_string(trace: &Self) -> String {
if trace.steps.is_empty() {
return "\
--------------------------------------------------------------------------------
PARSING STEPS
--------------------------------------------------------------------------------
[No parsing steps recorded]
"
.to_string();
}
let steps_content: String = trace
.steps
.iter()
.map(|step| {
let status = if step.success {
"✓ SUCCESS"
} else {
"✗ FAILED"
};
let step_str = format!("{}. {} [{}]\n", step.step_number, step.description, status);
let input_str = step
.input
.as_ref()
.map(|input| {
input
.lines()
.map(|line| format!(" INPUT:\n {}\n", line))
.collect::<String>()
})
.unwrap_or_default();
let result_str = step
.result
.as_ref()
.map(|result| {
result
.lines()
.map(|line| format!(" RESULT:\n {}\n", line))
.collect::<String>()
})
.unwrap_or_default();
let details_str = if !step.details.is_empty() {
format!(" DETAILS: {}\n", step.details)
} else {
String::new()
};
format!("{}{}{}{}", step_str, input_str, result_str, details_str)
})
.collect();
format!(
"\
--------------------------------------------------------------------------------
PARSING STEPS
--------------------------------------------------------------------------------
{}
",
steps_content
)
}
#[must_use]
fn final_message_to_string(trace: &Self) -> String {
format!(
"\
--------------------------------------------------------------------------------
FINAL EXTRACTED MESSAGE
--------------------------------------------------------------------------------
{}
",
trace
.final_message
.as_deref()
.unwrap_or("[No message extracted]")
)
}
#[must_use]
fn footer_to_string() -> String {
"\
================================================================================
END OF TRACE LOG
================================================================================
"
.to_string()
}
}
#[derive(Debug, Clone)]
pub struct ExtractionAttempt {
pub method: &'static str,
pub success: bool,
pub detail: String,
}
impl ExtractionAttempt {
#[must_use]
pub const fn success(method: &'static str, detail: String) -> Self {
Self {
method,
success: true,
detail,
}
}
#[must_use]
pub const fn failure(method: &'static str, detail: String) -> Self {
Self {
method,
success: false,
detail,
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationCheck {
pub name: &'static str,
pub passed: bool,
pub error: Option<String>,
}
impl ValidationCheck {
#[cfg(test)]
#[must_use]
pub const fn pass(name: &'static str) -> Self {
Self {
name,
passed: true,
error: None,
}
}
#[cfg(test)]
#[must_use]
pub const fn fail(name: &'static str, error: String) -> Self {
Self {
name,
passed: false,
error: Some(error),
}
}
}
#[derive(Debug, Clone)]
pub enum AttemptOutcome {
Success(String),
XsdValidationFailed(String),
ExtractionFailed(String),
}
impl std::fmt::Display for AttemptOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Success(msg) => write!(f, "SUCCESS: {}", preview_message(msg)),
Self::XsdValidationFailed(err) => write!(f, "XSD_VALIDATION_FAILED: {err}"),
Self::ExtractionFailed(err) => write!(f, "EXTRACTION_FAILED: {err}"),
}
}
}
fn preview_message(msg: &str) -> String {
let first_line = msg.lines().next().unwrap_or(msg);
truncate_text(first_line, 63)
}