use std::fmt::Write;
use agentic_tools_core::fmt::TextFormat;
use agentic_tools_core::fmt::TextOptions;
use chrono::DateTime;
use chrono::Utc;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct WebFetchInput {
pub url: String,
#[serde(default)]
pub summarize: bool,
#[serde(default)]
pub max_bytes: Option<usize>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct WebFetchOutput {
pub final_url: String,
pub title: Option<String>,
pub content_type: String,
pub word_count: usize,
pub truncated: bool,
pub retrieved_at: DateTime<Utc>,
pub content: String,
pub summary: Option<String>,
}
impl TextFormat for WebFetchOutput {
fn fmt_text(&self, _opts: &TextOptions) -> String {
let mut out = String::new();
let _ = writeln!(out, "URL: {}", self.final_url);
if let Some(title) = &self.title {
let _ = writeln!(out, "Title: {title}");
}
let _ = write!(
out,
"Retrieved: {} | Words: {}",
self.retrieved_at.format("%Y-%m-%d %H:%M UTC"),
self.word_count
);
if self.truncated {
out.push_str(" | TRUNCATED");
}
out.push('\n');
if let Some(summary) = &self.summary {
out.push_str("\n--- Summary ---\n");
out.push_str(summary);
out.push('\n');
}
out.push_str("\n--- Content ---\n");
out.push_str(&self.content);
out
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct WebSearchInput {
pub query: String,
#[serde(default)]
pub num_results: Option<u32>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct WebSearchOutput {
pub query: String,
pub retrieved_at: DateTime<Utc>,
pub context: Option<String>,
pub results: Vec<WebSearchResultCard>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct WebSearchResultCard {
pub url: String,
pub domain: String,
pub title: Option<String>,
pub published_date: Option<String>,
pub author: Option<String>,
pub score: Option<u32>,
pub snippet: Option<String>,
}
impl TextFormat for WebSearchOutput {
fn fmt_text(&self, _opts: &TextOptions) -> String {
let mut out = String::new();
let _ = writeln!(out, "Query: {}", self.query);
let _ = writeln!(
out,
"Retrieved: {}",
self.retrieved_at.format("%Y-%m-%d %H:%M UTC")
);
if let Some(ctx) = &self.context {
let _ = write!(out, "\n--- Context ---\n{ctx}\n");
}
let _ = write!(out, "\n--- Results ({}) ---\n", self.results.len());
for (i, card) in self.results.iter().enumerate() {
let _ = write!(
out,
"\n{}. {} ({})\n {}\n",
i + 1,
card.title.as_deref().unwrap_or("(untitled)"),
card.domain,
card.url,
);
let mut meta = Vec::new();
if let Some(date) = &card.published_date {
meta.push(format!("Date: {date}"));
}
if let Some(author) = &card.author {
meta.push(format!("Author: {author}"));
}
if !meta.is_empty() {
let _ = writeln!(out, " {}", meta.join(" | "));
}
if let Some(score) = card.score {
let _ = writeln!(out, " Score: {score}/100");
}
if let Some(snippet) = &card.snippet {
let _ = writeln!(out, " {snippet}");
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn author_displayed_without_date() {
let output = WebSearchOutput {
query: "test query".into(),
retrieved_at: Utc::now(),
context: None,
results: vec![WebSearchResultCard {
url: "https://example.com".into(),
domain: "example.com".into(),
title: Some("Test Title".into()),
published_date: None, author: Some("Jane Doe".into()), score: None,
snippet: None,
}],
};
let text = output.fmt_text(&TextOptions::default());
assert!(
text.contains("Author: Jane Doe"),
"Author should be displayed even when date is missing"
);
}
#[test]
fn date_and_author_displayed_together() {
let output = WebSearchOutput {
query: "test query".into(),
retrieved_at: Utc::now(),
context: None,
results: vec![WebSearchResultCard {
url: "https://example.com".into(),
domain: "example.com".into(),
title: Some("Test Title".into()),
published_date: Some("2025-01-15".into()),
author: Some("John Smith".into()),
score: None,
snippet: None,
}],
};
let text = output.fmt_text(&TextOptions::default());
assert!(
text.contains("Date: 2025-01-15 | Author: John Smith"),
"Date and author should be joined with pipe"
);
}
#[test]
fn date_displayed_without_author() {
let output = WebSearchOutput {
query: "test query".into(),
retrieved_at: Utc::now(),
context: None,
results: vec![WebSearchResultCard {
url: "https://example.com".into(),
domain: "example.com".into(),
title: Some("Test Title".into()),
published_date: Some("2025-01-15".into()),
author: None,
score: None,
snippet: None,
}],
};
let text = output.fmt_text(&TextOptions::default());
assert!(
text.contains("Date: 2025-01-15"),
"Date should be displayed when author is missing"
);
assert!(
!text.contains("Author:"),
"Author should not appear when not present"
);
}
}