use anyhow::{anyhow, Result};
use fastembed::{EmbeddingModel, InitOptions, TextEmbedding};
use parking_lot::Mutex;
use tokenmiser_providers::ChatRequest;
use tracing::info;
use crate::Difficulty;
struct Exemplar {
embedding: Vec<f32>,
difficulty: Difficulty,
}
pub struct Tier1Classifier {
embedder: Mutex<TextEmbedding>,
exemplars: Vec<Exemplar>,
}
impl Tier1Classifier {
pub fn new() -> Result<Self> {
let opts = InitOptions::new(EmbeddingModel::BGESmallENV15);
let mut embedder = TextEmbedding::try_new(opts)
.map_err(|e| anyhow!("bge-small init for Tier1 failed: {e}"))?;
let raw = default_exemplars();
let texts: Vec<&str> = raw.iter().map(|(t, _)| *t).collect();
let embeddings = embedder
.embed(texts, None)
.map_err(|e| anyhow!("Tier1 exemplar embed failed: {e}"))?;
let exemplars = embeddings
.into_iter()
.zip(raw.iter())
.map(|(emb, (_, d))| Exemplar {
embedding: emb,
difficulty: *d,
})
.collect::<Vec<_>>();
info!(
exemplars = exemplars.len(),
"Tier1 semantic classifier ready"
);
Ok(Self {
embedder: Mutex::new(embedder),
exemplars,
})
}
pub fn classify(&self, req: &ChatRequest) -> Difficulty {
let text = req
.messages
.iter()
.filter(|m| m.role == "user")
.filter_map(|m| match &m.content {
serde_json::Value::String(s) => Some(s.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
if text.trim().is_empty() {
return Difficulty::Medium;
}
let mut e = self.embedder.lock();
let emb = match e.embed(vec![text.as_str()], None) {
Ok(mut v) => match v.pop() {
Some(x) => x,
None => return Difficulty::Medium,
},
Err(_) => return Difficulty::Medium,
};
drop(e);
let mut best: Option<(f32, Difficulty)> = None;
for ex in &self.exemplars {
let sim = cosine(&emb, &ex.embedding);
if best.map(|(b, _)| sim > b).unwrap_or(true) {
best = Some((sim, ex.difficulty));
}
}
best.map(|(_, d)| d).unwrap_or(Difficulty::Medium)
}
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let mut dot = 0.0;
let mut na = 0.0;
let mut nb = 0.0;
for i in 0..a.len().min(b.len()) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
if na == 0.0 || nb == 0.0 {
return 0.0;
}
dot / (na.sqrt() * nb.sqrt())
}
fn default_exemplars() -> Vec<(&'static str, Difficulty)> {
vec![
("what is the capital of france", Difficulty::Easy),
("translate 'hello' to spanish", Difficulty::Easy),
("define photosynthesis in one sentence", Difficulty::Easy),
(
"summarize: the cat sat on the mat. the mat was red.",
Difficulty::Easy,
),
("is 17 a prime number?", Difficulty::Easy),
(
"classify this sentence as positive or negative: the food was great",
Difficulty::Easy,
),
("convert 32 fahrenheit to celsius", Difficulty::Easy),
("write a haiku about programming", Difficulty::Medium),
(
"compare REST and GraphQL in three bullet points",
Difficulty::Medium,
),
(
"explain how DNS resolution works to a junior dev",
Difficulty::Medium,
),
(
"draft an email apologizing for a missed deadline",
Difficulty::Medium,
),
(
"what are the trade-offs between SQL and NoSQL databases",
Difficulty::Medium,
),
(
"refactor this 400-line authentication middleware to use JWT tokens",
Difficulty::Hard,
),
(
"design a distributed rate limiter that handles 1M requests per second",
Difficulty::Hard,
),
(
"prove that this sorting algorithm terminates in O(n log n) time",
Difficulty::Hard,
),
(
"debug this race condition in my concurrent queue implementation",
Difficulty::Hard,
),
(
"implement a B-tree with concurrent insertions",
Difficulty::Hard,
),
(
"optimize this SQL query that joins seven tables for sub-100ms latency",
Difficulty::Hard,
),
]
}