use std::sync::Arc;
use std::collections::HashMap;
use ort::session::Session;
use tokenizers::Tokenizer;
use ndarray::Array1;
use anyhow::Result;
use super::error::ClassifierError;
use super::embedding::TextEmbedding;
use crate::ModelCharacteristics;
#[derive(Debug)]
pub struct Classifier {
pub model_path: String,
pub tokenizer_path: String,
pub tokenizer: Arc<Tokenizer>,
pub session: Arc<Session>,
pub embedded_prototypes: Arc<HashMap<String, Array1<f32>>>,
pub class_descriptions: Arc<HashMap<String, String>>,
pub model_characteristics: ModelCharacteristics,
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
fn verify_thread_safety() {
assert_send_sync::<Classifier>();
}
};
impl TextEmbedding for Classifier {
fn tokenizer(&self) -> Option<&Tokenizer> {
Some(&self.tokenizer)
}
fn session(&self) -> Option<&Session> {
Some(&self.session)
}
}
impl Classifier {
pub fn builder() -> super::builder::ClassifierBuilder {
super::builder::ClassifierBuilder::new()
}
pub fn info(&self) -> super::ClassifierInfo {
super::ClassifierInfo {
model_path: self.model_path.clone(),
tokenizer_path: self.tokenizer_path.clone(),
num_classes: self.embedded_prototypes.len(),
class_labels: self.embedded_prototypes.keys().cloned().collect(),
class_descriptions: Arc::clone(&self.class_descriptions),
embedding_size: self.model_characteristics.embedding_size,
}
}
pub fn predict(&self, text: &str) -> Result<(String, HashMap<String, f32>), ClassifierError> {
if text.is_empty() {
return Err(ClassifierError::ValidationError("Input text cannot be empty".into()));
}
let input_vector = self.embed_text(text)?;
let mut scores = HashMap::new();
for (label, prototype) in self.embedded_prototypes.as_ref() {
let similarity = Self::cosine_similarity(&input_vector, prototype);
scores.insert(label.clone(), similarity);
}
let best_class = scores.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(class, _)| class.clone())
.unwrap_or_else(|| "unknown".to_string());
Ok((best_class, scores))
}
fn cosine_similarity(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
a.dot(b)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use crate::{ModelManager, BuiltinModel, ClassDefinition};
async fn setup_test_classifier() -> Result<Classifier, Box<dyn std::error::Error>> {
let manager = ModelManager::new_default()?;
let model = BuiltinModel::MiniLM;
if !manager.is_model_downloaded(model) {
manager.download_model(model).await?;
}
assert!(manager.is_model_downloaded(model));
let classifier = Classifier::builder()
.with_model(model)?
.add_class(
ClassDefinition::new("test", "Test class")
.with_examples(vec!["example text"])
)?
.build()?;
Ok(classifier)
}
#[tokio::test]
async fn test_class_info() -> Result<(), Box<dyn std::error::Error>> {
let classifier = setup_test_classifier().await?;
let info = classifier.info();
assert_eq!(info.num_classes, 1);
assert!(info.class_descriptions.contains_key("test"));
Ok(())
}
#[tokio::test]
async fn test_model_loading() -> Result<(), Box<dyn std::error::Error>> {
let manager = ModelManager::new_default()?;
let model = BuiltinModel::MiniLM;
if !manager.is_model_downloaded(model) {
manager.download_model(model).await?;
}
assert!(manager.is_model_downloaded(model));
let classifier = Classifier::builder()
.with_model(model)?
.add_class(
ClassDefinition::new("test", "Test class")
.with_examples(vec!["test example"])
)?
.build()?;
assert!(classifier.predict("test input").is_ok());
Ok(())
}
#[tokio::test]
async fn test_model_verification() -> Result<(), Box<dyn std::error::Error>> {
let manager = ModelManager::new("/tmp/test-prefrontal/models").unwrap();
let model = BuiltinModel::MiniLM;
let model_path = manager.get_model_path(model);
let tokenizer_path = manager.get_tokenizer_path(model);
if model_path.exists() {
fs::remove_file(&model_path)?;
}
if tokenizer_path.exists() {
fs::remove_file(&tokenizer_path)?;
}
assert!(!manager.verify_model(model)?);
manager.download_model(model).await?;
assert!(manager.verify_model(model)?);
fs::write(&model_path, "corrupted data")?;
assert!(!manager.verify_model(model)?);
Ok(())
}
#[tokio::test]
async fn test_model_download() -> Result<(), Box<dyn std::error::Error>> {
let manager = ModelManager::new_default()?;
let model = BuiltinModel::MiniLM;
if !manager.is_model_downloaded(model) {
manager.download_model(model).await?;
}
let model_path = manager.get_model_path(model);
let tokenizer_path = manager.get_tokenizer_path(model);
assert!(model_path.exists());
assert!(tokenizer_path.exists());
Ok(())
}
#[tokio::test]
async fn test_model_setup() -> Result<(), Box<dyn std::error::Error>> {
let manager = ModelManager::new_default()?;
let model = BuiltinModel::MiniLM;
if !manager.is_model_downloaded(model) {
manager.download_model(model).await?;
}
assert!(manager.is_model_downloaded(model));
let classifier = Classifier::builder()
.with_model(model)?
.add_class(
ClassDefinition::new("test", "Test class")
.with_examples(vec!["test example"])
)?
.build()?;
assert!(classifier.predict("test input").is_ok());
Ok(())
}
#[tokio::test]
async fn test_model_characteristics() -> Result<(), Box<dyn std::error::Error>> {
let manager = ModelManager::new_default()?;
let model = BuiltinModel::MiniLM;
if !manager.is_model_downloaded(model) {
manager.download_model(model).await?;
}
let characteristics = model.characteristics();
assert_eq!(characteristics.embedding_size, 384);
assert_eq!(characteristics.max_sequence_length, 256);
assert_eq!(characteristics.model_size_mb, 85);
Ok(())
}
}