//! Fixed Pandoc HTML-to-Markdown conversion for invocation-local artifacts.
use std::time::Duration;
use serde_json::Value;
use tempfile::tempdir;
use tokio::{io::AsyncWriteExt, process::Command, time::timeout};
use crate::config::WebConfig;
use super::{ToolResult, ToolScope};
#[derive(Clone)]
pub(super) struct HtmlToMarkdownTool
{
config: WebConfig,
}
impl HtmlToMarkdownTool
{
pub(super) fn new(config: WebConfig) -> Self
{
Self { config }
}
pub(super) async fn execute(&self, arguments: Value, scope: &mut ToolScope) -> ToolResult
{
let Some(object) = arguments.as_object() else
{
return ToolResult::failure("invalid_arguments", "Tool arguments must be an object.");
};
if object.len() != 1 || !object.contains_key("artifact_id")
{
return ToolResult::failure("invalid_arguments", "Only an artifact ID argument is accepted.");
}
let Some(identifier) = object.get("artifact_id").and_then(Value::as_str) else
{
return ToolResult::failure("invalid_arguments", "An artifact ID string is required.");
};
let Some(artifact) = scope.artifacts.get(identifier) else
{
return ToolResult::failure("artifact_not_found", "The artifact does not exist in this invocation.");
};
if artifact.content_type != "text/html"
{
return ToolResult::failure("invalid_arguments", "Only HTML artifacts can be converted to Markdown.");
}
let html = artifact.bytes.clone();
let final_url = artifact.final_url.clone();
let Ok(directory) = tempdir() else
{
return ToolResult::failure("conversion_failed", "The converter could not create its temporary directory.");
};
let mut command = Command::new("pandoc");
command.args(["--from=html", "--to=gfm", "--wrap=none", "--sandbox"])
.current_dir(directory.path()).env_clear()
.stdin(std::process::Stdio::piped()).stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null()).kill_on_drop(true);
let Ok(mut child) = command.spawn() else
{
return ToolResult::failure("conversion_failed", "Pandoc is unavailable.");
};
let Some(mut stdin) = child.stdin.take() else
{
return ToolResult::failure("conversion_failed", "Pandoc standard input was unavailable.");
};
if stdin.write_all(&html).await.is_err() || stdin.shutdown().await.is_err()
{
return ToolResult::failure("conversion_failed", "Pandoc could not read the HTML artifact.");
}
let output = match timeout(Duration::from_secs(self.config.pandoc_timeout_seconds), child.wait_with_output()).await
{
Ok(Ok(output)) if output.status.success() => output.stdout,
Ok(Ok(_)) => return ToolResult::failure("conversion_failed", "Pandoc could not convert the page."),
Ok(Err(_)) => return ToolResult::failure("conversion_failed", "Pandoc failed while converting the page."),
Err(_) => return ToolResult::failure("timeout", "Pandoc exceeded the configured time limit."),
};
if output.len() > self.config.max_tool_output_characters as usize * 4
{
return ToolResult::failure("output_too_large", "Pandoc produced too much output.");
}
let markdown: String = String::from_utf8_lossy(&output).replace("\r\n", "\n")
.chars().filter(|character| !character.is_control() || *character == '\n' || *character == '\t')
.take(self.config.max_tool_output_characters as usize).collect();
let mut metadata = serde_json::Map::new();
metadata.insert("final_url".to_owned(), Value::String(final_url.to_string()));
let markdown_artifact_id = match scope.artifacts.insert(
"text/markdown".to_owned(), markdown.as_bytes().to_vec(), final_url,
)
{
Ok(identifier) => identifier,
Err(error) => return error,
};
metadata.insert("artifact_id".to_owned(), Value::String(markdown_artifact_id));
ToolResult::success("conversion_ok", markdown, metadata)
}
}