use std::path::PathBuf;
use std::sync::Arc;
use nedb_cast_slm::Cast as CastModel;
#[derive(Clone)]
pub struct Caster {
inner: Arc<CastModel>,
source: String,
}
impl Caster {
pub fn load(data_dir: &std::path::Path) -> Result<Self, String> {
for cand in Self::candidates(data_dir) {
if !cand.exists() {
continue;
}
return match CastModel::load(&cand) {
Ok(m) => {
if !m.checksum_verified {
eprintln!(
" cast: WARNING {} has no checksum; bytes unverified",
cand.display()
);
}
Ok(Caster {
inner: Arc::new(m),
source: cand.display().to_string(),
})
}
Err(e) => Err(format!("failed to load {}: {}", cand.display(), e)),
};
}
Err(format!(
"no model.cast found. Looked in:\n {}\n\
Download it from \
https://github.com/aiassistsecure/nedb-cast-slm/releases \
or set NEDBD_CAST_MODEL to its path.",
Self::candidates(data_dir)
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n ")
))
}
fn candidates(data_dir: &std::path::Path) -> Vec<PathBuf> {
let mut v = Vec::new();
if let Ok(p) = std::env::var("NEDBD_CAST_MODEL") {
v.push(PathBuf::from(p));
}
v.push(data_dir.join("model.cast"));
if let Ok(h) = std::env::var("CAST_HOME") {
v.push(PathBuf::from(&h).join("model.cast"));
v.push(PathBuf::from(&h).join("v10.30.90").join("model.cast"));
}
if let Ok(home) = std::env::var("HOME") {
let base = PathBuf::from(home).join(".cache").join("nedb-cast-slm");
v.push(base.join("v10.30.90").join("model.cast"));
v.push(base.join("model.cast"));
}
v
}
pub fn source(&self) -> &str {
&self.source
}
pub fn n_params(&self) -> usize {
self.inner.n_params()
}
pub fn vocab_size(&self) -> usize {
self.inner.vocab_size()
}
pub fn cast(&self, prompt: &str) -> String {
self.inner.cast(prompt)
}
pub fn cast_checked(&self, prompt: &str, collections: &[String]) -> CastResult {
let nql = self.cast(prompt);
let coll = extract_collection(&nql);
let known = match &coll {
Some(c) => collections.iter().any(|k| k == c),
None => false,
};
let drift = detect_drift(prompt, &nql);
CastResult { nql, collection: coll, collection_known: known, drift }
}
}
#[derive(Debug, Clone)]
pub struct CastResult {
pub nql: String,
pub collection: Option<String>,
pub collection_known: bool,
pub drift: Option<String>,
}
fn detect_drift(prompt: &str, nql: &str) -> Option<String> {
let p = prompt.to_lowercase();
for lit in quoted_literals(nql) {
let l = lit.to_lowercase();
if p.contains(&l) || l.split_whitespace().all(|w| p.contains(w)) {
continue;
}
return Some(format!(
"generated the literal {lit:?}, which does not appear in the prompt — \
likely outside the model's vocabulary and substituted. Verify before \
trusting these results."
));
}
None
}
fn quoted_literals(nql: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut inside = false;
for ch in nql.chars() {
if ch == '"' {
if inside {
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
cur.clear();
inside = false;
} else {
inside = true;
cur.clear();
}
} else if inside {
cur.push(ch);
}
}
out
}
fn extract_collection(nql: &str) -> Option<String> {
let mut it = nql.split_whitespace();
while let Some(tok) = it.next() {
if tok.eq_ignore_ascii_case("FROM") {
return it.next().map(|s| s.trim_matches('"').to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_collection_after_from() {
assert_eq!(
extract_collection("FROM orders WHERE total > 99"),
Some("orders".to_string())
);
assert_eq!(
extract_collection("from stylists limit 5"),
Some("stylists".to_string())
);
assert_eq!(extract_collection("WHERE total > 99"), None);
assert_eq!(extract_collection(""), None);
}
#[test]
fn strips_quotes_from_collection() {
assert_eq!(
extract_collection("FROM \"my coll\" LIMIT 1"),
Some("my".to_string()),
"quoted multiword collections are not a thing in NQL; \
first token is the right answer"
);
}
#[test]
fn drift_fires_on_substituted_search_term() {
let d = detect_drift("memories about pricing", r#"FROM memories SEARCH "handoff""#);
assert!(d.is_some(), "must flag a literal absent from the prompt");
assert!(d.unwrap().contains("handoff"), "must name the offending literal");
for p in ["memories about deadlines", "memories about kubernetes"] {
assert!(
detect_drift(p, r#"FROM memories SEARCH "handoff""#).is_some(),
"should flag: {p}"
);
}
}
#[test]
fn drift_silent_when_the_term_was_copied() {
for (p, nql) in [
("memories about the release flow", r#"FROM memories SEARCH "release flow""#),
("memories about guardrail", r#"FROM memories SEARCH "guardrail""#),
("memories about handoff", r#"FROM memories SEARCH "handoff""#),
] {
assert!(detect_drift(p, nql).is_none(), "false positive on: {p}");
}
}
#[test]
fn drift_ignores_inferred_enum_values() {
for (p, nql) in [
("orders that were refunded", r#"FROM orders WHERE status = "refunded""#),
("customers in orlando", r#"FROM customers WHERE city = "orlando""#),
("runs still running", r#"FROM runs WHERE status = "running""#),
("runs by vex", r#"FROM runs WHERE owner = "vex""#),
] {
assert!(detect_drift(p, nql).is_none(), "false positive on: {p}");
}
}
#[test]
fn drift_tolerates_requoted_multiword_phrases() {
assert!(detect_drift(
"customers in winter park",
r#"FROM customers WHERE city = "winter park""#
).is_none());
assert!(detect_drift(
"show me the park in winter",
r#"FROM customers WHERE city = "winter park""#
).is_none(), "all words present — not a substitution");
}
#[test]
fn drift_is_none_when_no_literals() {
assert!(detect_drift("show me all orders", "FROM orders").is_none());
assert!(detect_drift("orders with total over 100",
"FROM orders WHERE total > 100.0").is_none());
}
#[test]
fn quoted_literals_handles_degenerate_output() {
assert_eq!(quoted_literals(r#"FROM a WHERE b = "x" AND c = "y""#),
vec!["x".to_string(), "y".to_string()]);
assert_eq!(quoted_literals("FROM FROM FROM 6"), Vec::<String>::new());
assert_eq!(quoted_literals(r#"unterminated "quote"#), Vec::<String>::new(),
"an unclosed quote yields nothing, and must not hang");
assert_eq!(quoted_literals(r#"empty "" literal"#), Vec::<String>::new());
assert_eq!(quoted_literals(""), Vec::<String>::new());
}
}