Add Maritime/Logistics support assistant with safety/eval layer
RAG + multi-agent (retrieval, policy-compliance, drafting) support assistant for a fictional shipping company, built in Rust with Qdrant for vector storage and a hand-rolled Anthropic tool-calling client (no Rust pydantic-ai equivalent exists). A deterministic supervisor orchestrates the agents and runs every response through a rule-based guardrail pipeline with an append-only audit log. Includes a 44-example golden dataset (normal/injection/policy- violation/hallucination-bait/out-of-scope) and a regression harness that scores retrieval, generation, and guardrail correctness separately, plus a write-up of the failure modes and trade-offs. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
//! Structured input/output contracts exchanged between agents.
|
||||
//!
|
||||
//! Every LLM call in this system asks for one of these types back (forced
|
||||
//! via a tool-call with a JSON-schema-derived input, see `llm::client`)
|
||||
//! instead of free text. That is what lets the guardrail and eval layers
|
||||
//! reason about *why* an answer failed, not just that it failed.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RetrievedPassage {
|
||||
pub source: String,
|
||||
pub text: String,
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RetrievalOutput {
|
||||
pub passages: Vec<RetrievedPassage>,
|
||||
/// A booking reference or container number mentioned in the query, if
|
||||
/// any, extracted deterministically so the drafting agent can look it
|
||||
/// up via the `lookup_shipment` tool.
|
||||
pub extracted_reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PolicyVerdict {
|
||||
pub compliant: bool,
|
||||
#[serde(default)]
|
||||
pub violations: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub required_redactions: Vec<String>,
|
||||
pub reasoning: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DraftResponse {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub citations: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub used_shipment_reference: Option<String>,
|
||||
#[serde(default)]
|
||||
pub requires_escalation: bool,
|
||||
#[serde(default)]
|
||||
pub escalation_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SupervisorResult {
|
||||
pub final_answer: String,
|
||||
pub citations: Vec<String>,
|
||||
pub guardrail_action: String,
|
||||
pub escalated: bool,
|
||||
pub audit_id: String,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Domain models for the fictional shipping company, Meridian Sea Logistics.
|
||||
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShipmentStatus {
|
||||
Booked,
|
||||
InTransit,
|
||||
CustomsHold,
|
||||
Delayed,
|
||||
Delivered,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Shipment {
|
||||
pub booking_reference: String,
|
||||
pub container_number: String,
|
||||
pub customer_name: String,
|
||||
pub origin_port: String,
|
||||
pub destination_port: String,
|
||||
pub vessel_name: String,
|
||||
pub status: ShipmentStatus,
|
||||
pub eta: NaiveDate,
|
||||
pub last_update: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EscalationPriority {
|
||||
Low,
|
||||
Normal,
|
||||
High,
|
||||
Urgent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EscalationTicket {
|
||||
pub ticket_id: String,
|
||||
pub reason: String,
|
||||
pub customer_message: String,
|
||||
pub priority: EscalationPriority,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub related_reference: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Models for the golden dataset and the regression harness that scores it.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExampleCategory {
|
||||
Normal,
|
||||
PromptInjection,
|
||||
PolicyViolation,
|
||||
HallucinationBait,
|
||||
OutOfScope,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GoldenExample {
|
||||
pub id: String,
|
||||
pub category: ExampleCategory,
|
||||
pub query: String,
|
||||
#[serde(default)]
|
||||
pub expected_sources: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub expected_reference: Option<String>,
|
||||
#[serde(default)]
|
||||
pub answer_must_contain: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub answer_must_not_contain: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub should_escalate: bool,
|
||||
/// True if the guardrail layer is expected to block/refuse this request.
|
||||
#[serde(default)]
|
||||
pub should_block: bool,
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RetrievalScore {
|
||||
pub passed: bool,
|
||||
pub expected_sources: Vec<String>,
|
||||
pub retrieved_sources: Vec<String>,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct GenerationScore {
|
||||
pub passed: bool,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct GuardrailScore {
|
||||
pub passed: bool,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EvalResult {
|
||||
pub example: GoldenExample,
|
||||
pub retrieval: RetrievalScore,
|
||||
pub generation: GenerationScore,
|
||||
pub guardrail: GuardrailScore,
|
||||
pub final_answer: String,
|
||||
pub latency_ms: f64,
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub fn passed(&self) -> bool {
|
||||
self.retrieval.passed && self.generation.passed && self.guardrail.passed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CategoryBreakdown {
|
||||
pub category: ExampleCategory,
|
||||
pub total: usize,
|
||||
pub retrieval_pass_rate: f64,
|
||||
pub generation_pass_rate: f64,
|
||||
pub guardrail_pass_rate: f64,
|
||||
pub overall_pass_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EvalReport {
|
||||
pub total_examples: usize,
|
||||
pub overall_pass_rate: f64,
|
||||
pub retrieval_pass_rate: f64,
|
||||
pub generation_pass_rate: f64,
|
||||
pub guardrail_pass_rate: f64,
|
||||
pub by_category: Vec<CategoryBreakdown>,
|
||||
pub failures: Vec<EvalResult>,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod agent_io;
|
||||
pub mod domain;
|
||||
pub mod eval;
|
||||
Reference in New Issue
Block a user