wauldo 0.12.0

Official Rust SDK for Wauldo — Verified AI answers from your documents
Documentation
//! HTTP API request/response types (OpenAI-compatible)

use serde::{Deserialize, Serialize};

// ── Chat Completions ────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
pub struct ChatRequest {
    pub model: String,
    pub messages: Vec<ChatMessage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_tokens: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
    pub role: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatResponse {
    pub id: String,
    pub object: String,
    pub created: i64,
    pub model: String,
    pub choices: Vec<ChatChoice>,
    pub usage: Usage,
}

impl ChatResponse {
    /// Get the text content of the first choice (None if no content)
    pub fn text(&self) -> Option<&str> {
        self.choices
            .first()
            .and_then(|c| c.message.content.as_deref())
    }

    /// Get the text content or an empty string — convenience for display
    pub fn content(&self) -> String {
        self.text().unwrap_or("").to_string()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatChoice {
    pub index: u32,
    pub message: ChatMessage,
    pub finish_reason: Option<String>,
}

// ── Streaming ───────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatChunk {
    pub id: String,
    pub choices: Vec<ChatChunkChoice>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatChunkChoice {
    pub delta: ChatDelta,
    pub finish_reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatDelta {
    pub content: Option<String>,
}

// ── Usage ───────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
    pub prompt_tokens: u32,
    pub completion_tokens: u32,
    pub total_tokens: u32,
}

// ── Models ──────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelList {
    pub object: String,
    pub data: Vec<Model>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Model {
    pub id: String,
    pub object: String,
    pub created: i64,
    pub owned_by: String,
}

// ── Embeddings ──────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
pub struct EmbeddingRequest {
    pub input: EmbeddingInput,
    pub model: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum EmbeddingInput {
    Single(String),
    Multiple(Vec<String>),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingResponse {
    pub data: Vec<EmbeddingData>,
    pub model: String,
    pub usage: EmbeddingUsage,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingData {
    pub embedding: Vec<f32>,
    pub index: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingUsage {
    pub prompt_tokens: u32,
    pub total_tokens: u32,
}

// ── RAG ─────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
pub struct RagUploadRequest {
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filename: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagUploadResponse {
    pub document_id: String,
    pub chunks_count: usize,
}

#[derive(Debug, Clone, Serialize)]
pub struct RagQueryRequest {
    pub query: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_k: Option<usize>,
    /// Enable debug mode — returns retrieval funnel details
    #[serde(skip_serializing_if = "Option::is_none")]
    pub debug: Option<bool>,
    /// Enable SSE streaming (sources → token* → audit → \[DONE\])
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,
    /// Quality mode: "fast", "balanced", "premium"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quality_mode: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagQueryResponse {
    pub answer: String,
    pub sources: Vec<RagSource>,
    /// Full audit trail — always present
    #[serde(default)]
    pub audit: Option<RagAuditInfo>,
    // Legacy flat fields (servers < v1.6.5 may return these at root level)
    #[serde(default)]
    pub confidence: Option<f32>,
    #[serde(default)]
    pub grounded: Option<bool>,
}

impl RagQueryResponse {
    /// Get confidence from audit (preferred) or legacy flat field
    pub fn confidence(&self) -> Option<f32> {
        self.audit
            .as_ref()
            .map(|a| a.confidence)
            .or(self.confidence)
    }

    /// Get grounded from audit (preferred) or legacy flat field
    pub fn grounded(&self) -> Option<bool> {
        self.audit.as_ref().map(|a| a.grounded).or(self.grounded)
    }
}

/// Audit trail for RAG responses — verification and accountability
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagAuditInfo {
    pub confidence: f32,
    pub retrieval_path: String,
    pub sources_evaluated: usize,
    pub sources_used: usize,
    pub best_score: f32,
    pub grounded: bool,
    pub confidence_label: String,
    pub model: String,
    pub latency_ms: u64,
    /// Retrieval funnel diagnostics (v1.6.5+)
    #[serde(default)]
    pub candidates_found: Option<usize>,
    #[serde(default)]
    pub candidates_after_tenant: Option<usize>,
    #[serde(default)]
    pub candidates_after_score: Option<usize>,
    #[serde(default)]
    pub query_type: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagSource {
    pub document_id: String,
    pub content: String,
    pub score: f32,
    #[serde(default)]
    pub chunk_id: Option<String>,
    #[serde(default)]
    pub metadata: Option<serde_json::Value>,
}

// ── Guard (Fact-Check) ──────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
pub struct GuardRequest {
    pub text: String,
    pub source_context: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mode: Option<String>,
}

/// A single verified claim from the Guard response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardClaim {
    pub text: String,
    #[serde(default)]
    pub claim_type: Option<String>,
    pub supported: bool,
    pub confidence: f32,
    #[serde(default)]
    pub confidence_label: Option<String>,
    pub verdict: String,
    pub action: String,
    #[serde(default)]
    pub reason: Option<String>,
    #[serde(default)]
    pub evidence: Option<String>,
}

/// Response from POST /v1/fact-check — the Guard verification API
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardResponse {
    pub verdict: String,
    pub action: String,
    pub hallucination_rate: f32,
    pub mode: String,
    pub total_claims: usize,
    pub supported_claims: usize,
    pub confidence: f32,
    pub claims: Vec<GuardClaim>,
    #[serde(default)]
    pub mode_warning: Option<String>,
    #[serde(default)]
    pub processing_time_ms: Option<u64>,
}

impl GuardResponse {
    /// True if the verdict allows the content through
    pub fn is_safe(&self) -> bool {
        self.verdict == "verified"
    }

    /// True if the content should be blocked
    pub fn is_blocked(&self) -> bool {
        self.action == "block"
    }
}

// ── Orchestrator ────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
pub struct OrchestratorRequest {
    pub prompt: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestratorResponse {
    pub final_output: String,
}

// ── Builders ────────────────────────────────────────────────────────────

impl ChatRequest {
    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
        Self {
            model: model.into(),
            messages,
            temperature: None,
            max_tokens: None,
            stream: None,
            top_p: None,
            stop: None,
        }
    }

    /// Create a quick single-message chat request
    pub fn quick(model: impl Into<String>, message: impl Into<String>) -> Self {
        Self::new(model, vec![ChatMessage::user(message)])
    }
}

impl ChatMessage {
    pub fn user(content: impl Into<String>) -> Self {
        Self {
            role: "user".into(),
            content: Some(content.into()),
            name: None,
        }
    }

    pub fn system(content: impl Into<String>) -> Self {
        Self {
            role: "system".into(),
            content: Some(content.into()),
            name: None,
        }
    }

    pub fn assistant(content: impl Into<String>) -> Self {
        Self {
            role: "assistant".into(),
            content: Some(content.into()),
            name: None,
        }
    }
}