use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::agent::AgentHistoryList;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserStateView {
pub url: String,
pub title: String,
pub content_sample: String,
pub screenshot: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionView {
pub action_type: String,
pub parameters: HashMap<String, String>,
pub result: Option<String>,
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepView {
pub step_number: usize,
pub browser_state: BrowserStateView,
pub actions: Vec<ActionView>,
pub reasoning: String,
pub is_complete: bool,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryView {
pub task: String,
pub steps: Vec<StepView>,
}
impl HistoryView {
pub fn from_history(history: &AgentHistoryList) -> Self {
let mut steps = Vec::new();
let mut task = String::new();
for (idx, step) in history.steps.iter().enumerate() {
if idx == 0 {
task = step.output.current_state.summary.clone();
}
let screenshot =
extract_screenshot_from_state(&step.output.current_state.important_contents);
let browser_state = BrowserStateView {
url: extract_url_from_state(&step.output.current_state.important_contents),
title: extract_title_from_state(&step.output.current_state.important_contents),
content_sample: extract_content_sample(
&step.output.current_state.important_contents,
),
screenshot,
};
let actions = step
.output
.action
.iter()
.map(|a| ActionView {
action_type: a.action.clone(),
parameters: a.parameters.clone(),
result: None,
success: false,
error: None,
})
.collect();
let step_view = StepView {
step_number: idx,
browser_state,
actions,
reasoning: step.output.current_state.thought.clone(),
is_complete: step
.output
.action
.iter()
.any(|a| a.action.eq_ignore_ascii_case("done")),
timestamp: step.timestamp.to_rfc3339(),
};
steps.push(step_view);
}
Self { task, steps }
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
fn extract_url_from_state(state: &str) -> String {
for line in state.lines() {
if line.starts_with("URL:") {
return line.trim_start_matches("URL:").trim().to_string();
}
}
"about:blank".to_string()
}
fn extract_title_from_state(state: &str) -> String {
for line in state.lines() {
if line.starts_with("Title:") {
return line.trim_start_matches("Title:").trim().to_string();
}
}
"Untitled".to_string()
}
fn extract_content_sample(state: &str) -> String {
for line in state.lines() {
if line.starts_with("Content Sample:") {
return line
.trim_start_matches("Content Sample:")
.trim()
.to_string();
}
}
let max_length = 500;
if state.len() > max_length {
format!("{}...", &state[0..max_length])
} else {
state.to_string()
}
}
fn extract_screenshot_from_state(state: &str) -> Option<String> {
let mut found = false;
let mut b64 = String::new();
for line in state.lines() {
if found {
b64.push_str(line.trim());
break;
}
if line
.trim()
.starts_with("[IMAGE: Base64-encoded screenshot]")
{
found = true;
}
}
if !b64.is_empty() { Some(b64) } else { None }
}