use std::collections::HashMap;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DomainType {
General,
Medical,
Legal,
Technical,
Financial,
Scientific,
Custom,
}
impl DomainType {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::General => "General",
Self::Medical => "Medical",
Self::Legal => "Legal",
Self::Technical => "Technical",
Self::Financial => "Financial",
Self::Scientific => "Scientific",
Self::Custom => "Custom",
}
}
#[must_use]
pub fn has_predefined_terms(&self) -> bool {
!matches!(self, Self::General | Self::Custom)
}
}
#[derive(Debug, Clone)]
pub struct DomainConfig {
pub base_boost: f32,
pub priority_multiplier: f32,
pub max_boost: f32,
pub suppress_out_of_domain: bool,
pub suppression_factor: f32,
}
impl DomainConfig {
#[must_use]
pub fn new() -> Self {
Self {
base_boost: 1.0,
priority_multiplier: 1.5,
max_boost: 5.0,
suppress_out_of_domain: false,
suppression_factor: 0.5,
}
}
#[must_use]
pub fn with_base_boost(mut self, boost: f32) -> Self {
self.base_boost = boost;
self
}
#[must_use]
pub fn with_priority_multiplier(mut self, multiplier: f32) -> Self {
self.priority_multiplier = multiplier;
self
}
#[must_use]
pub fn with_max_boost(mut self, max: f32) -> Self {
self.max_boost = max;
self
}
#[must_use]
pub fn with_suppression(mut self, factor: f32) -> Self {
self.suppress_out_of_domain = true;
self.suppression_factor = factor;
self
}
}
impl Default for DomainConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DomainTerm {
pub text: String,
pub tokens: Vec<u32>,
pub boost: f32,
pub is_priority: bool,
pub category: Option<String>,
}
impl DomainTerm {
#[must_use]
pub fn new(text: String, tokens: Vec<u32>, boost: f32) -> Self {
Self {
text,
tokens,
boost,
is_priority: false,
category: None,
}
}
#[must_use]
pub fn with_priority(mut self) -> Self {
self.is_priority = true;
self
}
#[must_use]
pub fn with_category(mut self, category: &str) -> Self {
self.category = Some(category.to_string());
self
}
#[must_use]
pub fn first_token(&self) -> Option<u32> {
self.tokens.first().copied()
}
}
#[derive(Debug, Clone)]
pub struct DomainAdapter {
domain_type: DomainType,
config: DomainConfig,
pub(crate) terms: Vec<DomainTerm>,
first_token_map: HashMap<u32, Vec<usize>>,
domain_tokens: HashMap<u32, f32>,
}
impl DomainAdapter {
#[must_use]
pub fn new(domain_type: DomainType) -> Self {
let mut adapter = Self {
domain_type,
config: DomainConfig::default(),
terms: Vec::new(),
first_token_map: HashMap::new(),
domain_tokens: HashMap::new(),
};
if domain_type.has_predefined_terms() {
adapter.load_predefined_terms();
}
adapter
}
#[must_use]
pub fn with_config(domain_type: DomainType, config: DomainConfig) -> Self {
let mut adapter = Self {
domain_type,
config,
terms: Vec::new(),
first_token_map: HashMap::new(),
domain_tokens: HashMap::new(),
};
if domain_type.has_predefined_terms() {
adapter.load_predefined_terms();
}
adapter
}
#[must_use]
pub fn domain_type(&self) -> DomainType {
self.domain_type
}
#[must_use]
pub fn config(&self) -> &DomainConfig {
&self.config
}
#[must_use]
pub fn len(&self) -> usize {
self.terms.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.terms.is_empty()
}
#[must_use]
pub fn terms(&self) -> &[DomainTerm] {
&self.terms
}
pub fn add_term_with_tokens(&mut self, text: &str, tokens: Vec<u32>, boost: f32) {
if tokens.is_empty() {
return;
}
let clamped_boost = boost.clamp(-self.config.max_boost, self.config.max_boost);
let first_token = tokens[0];
let term_idx = self.terms.len();
for &token in &tokens {
self.domain_tokens
.entry(token)
.and_modify(|b| *b = b.max(clamped_boost))
.or_insert(clamped_boost);
}
self.terms
.push(DomainTerm::new(text.to_string(), tokens, clamped_boost));
self.first_token_map
.entry(first_token)
.or_default()
.push(term_idx);
}
pub fn add_term_with_tokens_default(&mut self, text: &str, tokens: Vec<u32>) {
self.add_term_with_tokens(text, tokens, self.config.base_boost);
}
pub fn add_priority_term(&mut self, text: &str, tokens: Vec<u32>) {
let boost = self.config.base_boost * self.config.priority_multiplier;
if !tokens.is_empty() {
let term_idx = self.terms.len();
let first_token = tokens[0];
for &token in &tokens {
self.domain_tokens
.entry(token)
.and_modify(|b| *b = b.max(boost))
.or_insert(boost);
}
let mut term = DomainTerm::new(text.to_string(), tokens, boost);
term.is_priority = true;
self.terms.push(term);
self.first_token_map
.entry(first_token)
.or_default()
.push(term_idx);
}
}
pub fn apply_bias(&self, logits: &mut [f32]) {
if self.is_empty() {
return;
}
for (&token, &boost) in &self.domain_tokens {
if (token as usize) < logits.len() {
logits[token as usize] += boost;
}
}
}
#[must_use]
pub fn is_domain_token(&self, token: u32) -> bool {
self.domain_tokens.contains_key(&token)
}
#[must_use]
pub fn get_token_boost(&self, token: u32) -> Option<f32> {
self.domain_tokens.get(&token).copied()
}
pub fn clear(&mut self) {
self.terms.clear();
self.first_token_map.clear();
self.domain_tokens.clear();
}
fn load_predefined_terms(&mut self) {
match self.domain_type {
DomainType::Medical => self.load_medical_terms(),
DomainType::Legal => self.load_legal_terms(),
DomainType::Technical => self.load_technical_terms(),
DomainType::Financial => self.load_financial_terms(),
DomainType::Scientific => self.load_scientific_terms(),
DomainType::General | DomainType::Custom => {}
}
}
#[allow(clippy::needless_pass_by_ref_mut)]
fn load_medical_terms(&mut self) {
let _ = self; }
#[allow(clippy::needless_pass_by_ref_mut)]
fn load_legal_terms(&mut self) {
let _ = self; }
#[allow(clippy::needless_pass_by_ref_mut)]
fn load_technical_terms(&mut self) {
let _ = self; }
#[allow(clippy::needless_pass_by_ref_mut)]
fn load_financial_terms(&mut self) {
let _ = self; }
#[allow(clippy::needless_pass_by_ref_mut)]
fn load_scientific_terms(&mut self) {
let _ = self; }
#[must_use]
pub fn terms_by_category(&self, category: &str) -> Vec<&DomainTerm> {
self.terms
.iter()
.filter(|t| t.category.as_deref() == Some(category))
.collect()
}
#[must_use]
pub fn categories(&self) -> Vec<String> {
let mut categories: Vec<_> = self
.terms
.iter()
.filter_map(|t| t.category.clone())
.collect();
categories.sort();
categories.dedup();
categories
}
#[must_use]
pub fn priority_terms(&self) -> Vec<&DomainTerm> {
self.terms.iter().filter(|t| t.is_priority).collect()
}
}
impl DomainAdapter {
#[must_use]
pub fn medical() -> Self {
Self::new(DomainType::Medical)
}
#[must_use]
pub fn legal() -> Self {
Self::new(DomainType::Legal)
}
#[must_use]
pub fn technical() -> Self {
Self::new(DomainType::Technical)
}
#[must_use]
pub fn financial() -> Self {
Self::new(DomainType::Financial)
}
#[must_use]
pub fn scientific() -> Self {
Self::new(DomainType::Scientific)
}
#[must_use]
pub fn custom() -> Self {
Self::new(DomainType::Custom)
}
}