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:
msjid
2026-09-18 21:04:09 +02:00
co-authored by Claude Sonnet 5
parent 93e92a1210
commit 771280cde6
56 changed files with 7214 additions and 72 deletions
+57
View File
@@ -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,
}
+49
View File
@@ -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>,
}
+92
View File
@@ -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>,
}
+3
View File
@@ -0,0 +1,3 @@
pub mod agent_io;
pub mod domain;
pub mod eval;