1use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
16#[repr(u8)]
17pub enum ContextType {
18 General = 0x0,
19 Medical = 0x1,
20 Legal = 0x2,
21 Chemical = 0x3,
22 Biological = 0x4,
23 Technical = 0x5,
24}
25
26impl ContextType {
27 pub fn from_metadata_bits(bits: u8) -> Self {
29 match bits & 0xF {
30 0x1 => ContextType::Medical,
31 0x2 => ContextType::Legal,
32 0x3 => ContextType::Chemical,
33 0x4 => ContextType::Biological,
34 0x5 => ContextType::Technical,
35 _ => ContextType::General,
36 }
37 }
38
39 pub fn to_metadata_bits(self) -> u8 {
40 self as u8
41 }
42
43 pub fn adapter_filename(self) -> &'static str {
44 match self {
45 ContextType::General => "general_v1.lora",
46 ContextType::Medical => "medical_v1.lora",
47 ContextType::Legal => "legal_v1.lora",
48 ContextType::Chemical => "chemical_v1.lora",
49 ContextType::Biological => "biological_v1.lora",
50 ContextType::Technical => "technical_v1.lora",
51 }
52 }
53
54 pub fn all() -> &'static [ContextType] {
55 &[
56 ContextType::General,
57 ContextType::Medical,
58 ContextType::Legal,
59 ContextType::Chemical,
60 ContextType::Biological,
61 ContextType::Technical,
62 ]
63 }
64}
65
66impl std::fmt::Display for ContextType {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 ContextType::General => write!(f, "general"),
70 ContextType::Medical => write!(f, "medical"),
71 ContextType::Legal => write!(f, "legal"),
72 ContextType::Chemical => write!(f, "chemical"),
73 ContextType::Biological => write!(f, "biological"),
74 ContextType::Technical => write!(f, "technical"),
75 }
76 }
77}
78
79struct NGramAnalyzer {
82 bigram_weights: HashMap<(&'static str, &'static str), (ContextType, f32)>,
83}
84
85impl NGramAnalyzer {
86 fn new() -> Self {
87 let mut bigram_weights = HashMap::new();
88 for pair in [
90 ("patient", "diagnosis"),
91 ("medical", "record"),
92 ("clinical", "trial"),
93 ("emergency", "room"),
94 ("blood", "pressure"),
95 ("heart", "rate"),
96 ("drug", "dosage"),
97 ("surgical", "procedure"),
98 ] {
99 bigram_weights.insert(pair, (ContextType::Medical, 0.85));
100 }
101 for pair in [
103 ("legal", "contract"),
104 ("court", "order"),
105 ("intellectual", "property"),
106 ("due", "diligence"),
107 ("breach", "contract"),
108 ("statute", "limitations"),
109 ("case", "law"),
110 ("legal", "counsel"),
111 ] {
112 bigram_weights.insert(pair, (ContextType::Legal, 0.85));
113 }
114 for pair in [
116 ("chemical", "reaction"),
117 ("organic", "compound"),
118 ("synthesis", "reaction"),
119 ("chemical", "bond"),
120 ("molecular", "weight"),
121 ("oxidation", "reduction"),
122 ("acid", "base"),
123 ("reaction", "mechanism"),
124 ] {
125 bigram_weights.insert(pair, (ContextType::Chemical, 0.85));
126 }
127 for pair in [
129 ("gene", "expression"),
130 ("cell", "division"),
131 ("protein", "synthesis"),
132 ("natural", "selection"),
133 ("dna", "replication"),
134 ("immune", "system"),
135 ("metabolic", "pathway"),
136 ("stem", "cell"),
137 ] {
138 bigram_weights.insert(pair, (ContextType::Biological, 0.85));
139 }
140 for pair in [
142 ("machine", "learning"),
143 ("neural", "network"),
144 ("api", "endpoint"),
145 ("data", "structure"),
146 ("time", "complexity"),
147 ("memory", "management"),
148 ("software", "architecture"),
149 ("distributed", "system"),
150 ] {
151 bigram_weights.insert(pair, (ContextType::Technical, 0.85));
152 }
153 Self { bigram_weights }
154 }
155
156 fn score(&self, tokens: &[&str]) -> HashMap<ContextType, f32> {
157 let mut scores: HashMap<ContextType, f32> = HashMap::new();
158 for window in tokens.windows(2) {
159 if let [a, b] = window {
160 if let Some(&(ctx, w)) = self.bigram_weights.get(&(*a, *b)) {
161 *scores.entry(ctx).or_insert(0.0) += w;
162 }
163 }
164 }
165 scores
166 }
167}
168
169pub struct ContextDetector {
180 keyword_weights: HashMap<&'static str, Vec<(ContextType, f32)>>,
182 ngrams: NGramAnalyzer,
183 pub confidence_threshold: f32,
185}
186
187impl ContextDetector {
188 pub fn new() -> Self {
189 let mut kw: HashMap<&'static str, Vec<(ContextType, f32)>> = HashMap::new();
190
191 macro_rules! add {
192 ($word:expr, $( ($ctx:expr, $w:expr) ),+) => {
193 kw.entry($word).or_default().extend([$( ($ctx, $w) ),+]);
194 };
195 }
196
197 for w in [
199 "diagnosis",
200 "symptom",
201 "treatment",
202 "medication",
203 "patient",
204 "clinical",
205 "prescription",
206 "therapy",
207 "disease",
208 "anatomy",
209 "physiology",
210 "pharmacology",
211 "surgery",
212 "emergency",
213 "vaccine",
214 "oncology",
215 "radiology",
216 "pathology",
217 "neurology",
218 "cardiology",
219 "prognosis",
220 "aetiology",
221 "comorbidity",
222 "triage",
223 "dosage",
224 ] {
225 add!(
226 w,
227 (ContextType::Medical, 0.9),
228 (ContextType::Biological, 0.4)
229 );
230 }
231
232 for w in [
234 "contract",
235 "agreement",
236 "liability",
237 "legal",
238 "court",
239 "law",
240 "jurisdiction",
241 "statute",
242 "regulation",
243 "compliance",
244 "litigation",
245 "plaintiff",
246 "defendant",
247 "attorney",
248 "verdict",
249 "evidence",
250 "indemnity",
251 "arbitration",
252 "injunction",
253 "tort",
254 "precedent",
255 "affidavit",
256 "deposition",
257 "fiduciary",
258 "subpoena",
259 ] {
260 add!(w, (ContextType::Legal, 0.9));
261 }
262
263 for w in [
265 "molecule",
266 "compound",
267 "reaction",
268 "chemical",
269 "synthesis",
270 "bond",
271 "catalyst",
272 "reagent",
273 "solvent",
274 "stoichiometry",
275 "organic",
276 "inorganic",
277 "polymer",
278 "spectroscopy",
279 "titration",
280 "oxidation",
281 "reduction",
282 "isomer",
283 "alkyl",
284 "hydroxyl",
285 "carbonyl",
286 "ester",
287 "molar",
288 "entropy",
289 "enthalpy",
290 ] {
291 add!(
292 w,
293 (ContextType::Chemical, 0.9),
294 (ContextType::Technical, 0.2)
295 );
296 }
297
298 for w in [
300 "cell",
301 "gene",
302 "protein",
303 "dna",
304 "rna",
305 "organism",
306 "species",
307 "evolution",
308 "ecosystem",
309 "metabolism",
310 "genetics",
311 "biology",
312 "immunology",
313 "mitosis",
314 "meiosis",
315 "chromosome",
316 "allele",
317 "phenotype",
318 "genotype",
319 "ribosome",
320 "enzyme",
321 "chlorophyll",
322 "photosynthesis",
323 "fermentation",
324 "microbiome",
325 ] {
326 add!(
327 w,
328 (ContextType::Biological, 0.9),
329 (ContextType::Chemical, 0.3)
330 );
331 }
332
333 for w in [
335 "algorithm",
336 "software",
337 "hardware",
338 "programming",
339 "code",
340 "system",
341 "database",
342 "network",
343 "protocol",
344 "interface",
345 "api",
346 "encryption",
347 "authentication",
348 "optimization",
349 "latency",
350 "throughput",
351 "compiler",
352 "runtime",
353 "kernel",
354 "concurrency",
355 "async",
356 "cache",
357 "shader",
358 "tensor",
359 "gradient",
360 ] {
361 add!(w, (ContextType::Technical, 0.9));
362 }
363
364 Self {
365 keyword_weights: kw,
366 ngrams: NGramAnalyzer::new(),
367 confidence_threshold: 0.55,
368 }
369 }
370
371 pub fn analyze_text(&self, text: &str) -> (ContextType, f32) {
375 let tokens: Vec<&str> = text
376 .split(|c: char| !c.is_alphanumeric())
377 .filter(|s| !s.is_empty())
378 .map(|s| {
379 s
383 })
384 .collect();
385
386 let lowered: Vec<String> = tokens.iter().map(|t| t.to_lowercase()).collect();
388 let lower_refs: Vec<&str> = lowered.iter().map(|s| s.as_str()).collect();
389
390 let mut scores: HashMap<ContextType, f32> = HashMap::new();
391
392 for tok in &lower_refs {
394 if let Some(weights) = self.keyword_weights.get(tok) {
395 for &(ctx, w) in weights {
396 *scores.entry(ctx).or_insert(0.0) += w;
397 }
398 }
399 }
400
401 for (ctx, w) in self.ngrams.score(&lower_refs) {
403 *scores.entry(ctx).or_insert(0.0) += w * 0.5;
404 }
405
406 if scores.is_empty() {
407 return (ContextType::General, 0.0);
408 }
409
410 let best = scores
411 .iter()
412 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
413 .map(|(&ctx, &s)| (ctx, s))
414 .unwrap();
415
416 let total: f32 = scores.values().sum();
417 let confidence = if total > 0.0 { best.1 / total } else { 0.0 };
418
419 if confidence < self.confidence_threshold {
420 (ContextType::General, confidence)
421 } else {
422 (best.0, confidence)
423 }
424 }
425
426 pub fn combine(
430 &self,
431 text_result: (ContextType, f32),
432 meta_result: (ContextType, f32),
433 ) -> (ContextType, f32) {
434 if text_result.1 > meta_result.1 {
435 text_result
436 } else {
437 meta_result
438 }
439 }
440}
441
442impl Default for ContextDetector {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn test_medical_detection() {
456 let det = ContextDetector::new();
457 let (ctx, conf) = det.analyze_text("The patient requires medication for the diagnosis");
458 assert_eq!(
459 ctx,
460 ContextType::Medical,
461 "expected Medical, got {:?} (conf={conf:.2})",
462 ctx
463 );
464 assert!(conf > 0.5);
465 }
466
467 #[test]
468 fn test_legal_detection() {
469 let det = ContextDetector::new();
470 let (ctx, conf) = det.analyze_text("The plaintiff filed a contract litigation case");
471 assert_eq!(
472 ctx,
473 ContextType::Legal,
474 "expected Legal, got {:?} (conf={conf:.2})",
475 ctx
476 );
477 assert!(conf > 0.5);
478 }
479
480 #[test]
481 fn test_chemical_detection() {
482 let det = ContextDetector::new();
483 let (ctx, conf) =
484 det.analyze_text("Organic synthesis of the catalyst compound via reaction");
485 assert_eq!(
486 ctx,
487 ContextType::Chemical,
488 "expected Chemical, got {:?} (conf={conf:.2})",
489 ctx
490 );
491 assert!(conf > 0.4);
492 }
493
494 #[test]
495 fn test_biological_detection() {
496 let det = ContextDetector::new();
497 let (ctx, conf) =
498 det.analyze_text("Gene expression in the cell affects protein synthesis via RNA");
499 assert_eq!(
500 ctx,
501 ContextType::Biological,
502 "expected Biological, got {:?} (conf={conf:.2})",
503 ctx
504 );
505 assert!(conf > 0.4);
506 }
507
508 #[test]
509 fn test_technical_detection() {
510 let det = ContextDetector::new();
511 let (ctx, conf) =
512 det.analyze_text("Optimizing the algorithm latency in the distributed system");
513 assert_eq!(
514 ctx,
515 ContextType::Technical,
516 "expected Technical, got {:?} (conf={conf:.2})",
517 ctx
518 );
519 assert!(conf > 0.5);
520 }
521
522 #[test]
523 fn test_general_fallback() {
524 let det = ContextDetector::new();
525 let (ctx, _conf) = det.analyze_text("Hello world");
526 assert_eq!(ctx, ContextType::General);
527 }
528
529 #[test]
530 fn test_roundtrip_metadata_bits() {
531 for &ct in ContextType::all() {
532 let bits = ct.to_metadata_bits();
533 let decoded = ContextType::from_metadata_bits(bits);
534 assert_eq!(decoded, ct, "roundtrip failed for {:?}", ct);
535 }
536 }
537}