use chrono::Utc;
use serde::Serialize;
use serde_json::{json, Map, Value};
use crate::schema::PdfSource;
use crate::SERVER_VERSION;
pub const PRODUCT: &str = "anymd";
pub const ENVELOPE_VERSION: &str = "1";
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfToolEvidence {
pub subject: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_hash: Option<String>,
pub freshness: Freshness,
pub locator: Locator,
pub route: Route,
pub confidence: &'static str,
pub warnings: Vec<String>,
pub next_actions: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Freshness {
pub indexed_at: String,
pub stale: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Locator {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
pub tool: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Route {
pub extraction: String,
pub tool: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
}
pub fn primary_source_label(sources: &[PdfSource]) -> String {
sources
.first()
.and_then(|source| source.path.clone().or_else(|| source.url.clone()))
.unwrap_or_else(|| "unknown".into())
}
pub fn attach_evidence(
tool: &str,
operation: Option<&str>,
sources: &[PdfSource],
route: &str,
source_hash: Option<String>,
warnings: Vec<String>,
payload: Value,
) -> Value {
let source = primary_source_label(sources);
let first = sources.first();
let evidence = PdfToolEvidence {
subject: source.clone(),
source: source.clone(),
source_hash: source_hash.clone(),
freshness: Freshness {
indexed_at: Utc::now().to_rfc3339(),
stale: false,
},
locator: Locator {
path: first.and_then(|s| s.path.clone()),
url: first.and_then(|s| s.url.clone()),
tool: tool.into(),
operation: operation.map(str::to_string),
},
route: Route {
extraction: route.into(),
tool: tool.into(),
operation: operation.map(str::to_string),
},
confidence: "deterministic",
warnings: warnings.clone(),
next_actions: vec![
"Use search_pdf for literal retrieval with page and bbox locators".into(),
"Use pdf_evidence for inspect, render, crop, OCR, or visual-region follow-up".into(),
],
};
let mut object = match payload {
Value::Object(map) => map,
other => {
let mut map = Map::new();
map.insert("payload".into(), other);
map
}
};
object.insert(
"evidence".into(),
serde_json::to_value(evidence).unwrap_or(json!({})),
);
object.insert("envelope_version".into(), json!(ENVELOPE_VERSION));
object.insert("status".into(), json!("ok"));
object.insert("tool".into(), json!(tool));
object.insert("product".into(), json!(PRODUCT));
object.insert("product_version".into(), json!(SERVER_VERSION));
object.insert(
"route".into(),
json!({
"engine": "rust-core",
"path": route,
}),
);
object.insert("warnings".into(), json!(warnings));
if !object.contains_key("gaps") {
object.insert("gaps".into(), json!([]));
}
object.insert(
"confidence".into(),
json!({ "kind": "deterministic", "notes": [] }),
);
let mut source_obj = Map::new();
if let Some(path) = first.and_then(|s| s.path.clone()) {
source_obj.insert("path".into(), json!(path));
}
if let Some(url) = first.and_then(|s| s.url.clone()) {
source_obj.insert("url".into(), json!(url));
}
if let Some(hash) = source_hash {
source_obj.insert("hash".into(), json!(hash));
}
if !source_obj.is_empty() {
object.insert("source".into(), Value::Object(source_obj));
}
if !object.contains_key("payload") {
if object.contains_key("results")
|| object.contains_key("documents")
|| object.contains_key("markdown")
|| object.contains_key("pages")
{
}
}
if let Some(op) = operation {
object.insert("operation".into(), json!(op));
}
Value::Object(object)
}
pub fn attach_error_envelope(tool: &str, code: &str, message: &str, warnings: Vec<String>) -> Value {
json!({
"envelope_version": ENVELOPE_VERSION,
"status": "error",
"tool": tool,
"product": PRODUCT,
"product_version": SERVER_VERSION,
"route": { "engine": "rust-core" },
"payload": null,
"warnings": warnings,
"gaps": [],
"error": { "code": code, "message": message },
"confidence": { "kind": "deterministic", "notes": [] },
})
}