1use std::collections::HashSet;
5use crate::encoder::{cosine_similarity, SemanticVectorEncoder, VECTOR_DIM};
6
7#[derive(Clone, Debug)]
9pub struct Noul {
10 pub instructions: String,
11 pub threshold: f32,
12 pub temperature: f32,
13 pub uncertainty_low: f32,
14 pub uncertainty_high: f32,
15}
16
17impl Noul {
18 pub fn new(instructions: impl Into<String>) -> Self {
19 Self {
20 instructions: instructions.into(),
21 threshold: 0.85,
22 temperature: 0.25,
23 uncertainty_low: 0.35,
24 uncertainty_high: 0.65,
25 }
26 }
27
28 pub fn with_threshold(mut self, threshold: f32) -> Self {
29 self.threshold = threshold;
30 self
31 }
32
33 pub fn with_temperature(mut self, temperature: f32) -> Self {
34 self.temperature = temperature.max(0.01);
35 self
36 }
37
38 pub fn evaluate(
39 &self,
40 state: &str,
41 state_vec: &[f32; VECTOR_DIM],
42 encoder: &SemanticVectorEncoder,
43 ) -> NoulResult {
44 let query_vec = encoder.encode(&self.instructions);
45 let sim = cosine_similarity(state_vec, &query_vec);
46
47 let state_tokens: HashSet<String> = extract_tokens(state);
48 let query_tokens: HashSet<String> = extract_tokens(&self.instructions);
49
50 let neg_words = ["not", "never", "safe", "normal", "routine", "false", "ignore"];
52 let has_neg = neg_words.iter().any(|w| state_tokens.contains(*w));
53
54 let alarm_tokens = [
56 "scam", "fraud", "wire", "urgent", "phishing", "attack", "critical", "breach",
57 "refund", "stolen", "cancel", "ransomware", "hazard", "threat", "hacked", "emergency",
58 ];
59 let alarm_overlap = alarm_tokens
60 .iter()
61 .filter(|w| state_tokens.contains(**w))
62 .count();
63
64 let query_lower = self.instructions.to_lowercase();
65 let query_is_threat = ["security", "threat", "hazard", "scam", "urgent", "refund"]
66 .iter()
67 .any(|w| query_lower.contains(w));
68
69 let keyword_overlap = query_tokens
70 .iter()
71 .filter(|w| state_tokens.contains(*w))
72 .count();
73
74 let mut effective_sim = sim + (0.30f32).min(keyword_overlap as f32 * 0.08);
75 if query_is_threat && alarm_overlap > 0 {
76 effective_sim = effective_sim.max(0.15 + alarm_overlap as f32 * 0.05);
77 }
78
79 let adjusted_sim = effective_sim - if has_neg { 0.15 } else { 0.0 };
81 let logit = (adjusted_sim - 0.06) / self.temperature;
82 let clamped_logit = logit.clamp(-20.0, 20.0);
83 let prob = 1.0 / (1.0 + (-clamped_logit).exp());
84 let clamped = prob.clamp(0.0, 1.0);
85
86 let is_true = clamped >= self.threshold;
87 let is_false = clamped <= (1.0 - self.threshold);
88 let is_uncertain = clamped >= self.uncertainty_low && clamped <= self.uncertainty_high;
89 let confidence = clamped.max(1.0 - clamped);
90
91 NoulResult {
92 probability: clamped,
93 confidence,
94 is_true,
95 is_false,
96 is_uncertain,
97 }
98 }
99}
100
101#[derive(Clone, Debug, PartialEq)]
103pub struct NoulResult {
104 pub probability: f32,
105 pub confidence: f32,
106 pub is_true: bool,
107 pub is_false: bool,
108 pub is_uncertain: bool,
109}
110
111#[derive(Clone, Debug)]
113pub struct Choice {
114 pub instructions: String,
115 pub options: Vec<String>,
116 pub temperature: f32,
117}
118
119impl Choice {
120 pub fn new(instructions: impl Into<String>, options: Vec<String>) -> Self {
121 Self {
122 instructions: instructions.into(),
123 options,
124 temperature: 0.25,
125 }
126 }
127
128 pub fn with_temperature(mut self, temperature: f32) -> Self {
129 self.temperature = temperature.max(0.01);
130 self
131 }
132
133 pub fn evaluate(
134 &self,
135 _state: &str,
136 state_vec: &[f32; VECTOR_DIM],
137 encoder: &SemanticVectorEncoder,
138 ) -> ChoiceResult {
139 if self.options.is_empty() {
140 return ChoiceResult {
141 selected: String::new(),
142 confidence: 0.0,
143 distribution: Vec::new(),
144 };
145 }
146
147 let mut raw_sims: Vec<f32> = Vec::with_capacity(self.options.len());
148 for opt in &self.options {
149 let opt_text = format!("{} {}", self.instructions, opt);
150 let opt_vec = encoder.encode(&opt_text);
151 let sim = cosine_similarity(state_vec, &opt_vec);
152 raw_sims.push(sim / self.temperature);
153 }
154
155 let max_score = raw_sims.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
157 let exps: Vec<f32> = raw_sims.iter().map(|&s| (s - max_score).exp()).collect();
158 let sum_exp: f32 = exps.iter().sum();
159
160 let mut distribution: Vec<(String, f32)> = Vec::with_capacity(self.options.len());
161 let mut best_idx = 0;
162 let mut best_prob = -1.0f32;
163
164 for (i, opt) in self.options.iter().enumerate() {
165 let p = if sum_exp > 0.0 { exps[i] / sum_exp } else { 0.0 };
166 if p > best_prob {
167 best_prob = p;
168 best_idx = i;
169 }
170 distribution.push((opt.clone(), p));
171 }
172
173 ChoiceResult {
174 selected: self.options[best_idx].clone(),
175 confidence: best_prob,
176 distribution,
177 }
178 }
179}
180
181#[derive(Clone, Debug)]
183pub struct ChoiceResult {
184 pub selected: String,
185 pub confidence: f32,
186 pub distribution: Vec<(String, f32)>,
187}
188
189impl ChoiceResult {
190 pub fn get_prob(&self, option: &str) -> f32 {
191 self.distribution
192 .iter()
193 .find(|(k, _)| k == option)
194 .map(|(_, p)| *p)
195 .unwrap_or(0.0)
196 }
197}
198
199#[derive(Clone, Debug)]
201pub struct Score {
202 pub instructions: String,
203 pub min_val: f32,
204 pub max_val: f32,
205}
206
207impl Score {
208 pub fn new(instructions: impl Into<String>, min_val: f32, max_val: f32) -> Self {
209 Self {
210 instructions: instructions.into(),
211 min_val,
212 max_val,
213 }
214 }
215
216 pub fn evaluate(
217 &self,
218 _state: &str,
219 state_vec: &[f32; VECTOR_DIM],
220 encoder: &SemanticVectorEncoder,
221 ) -> ScoreResult {
222 let instr_vec = encoder.encode(&self.instructions);
223 let sim = cosine_similarity(state_vec, &instr_vec).clamp(0.0, 1.0);
224 let score = self.min_val + sim * (self.max_val - self.min_val);
225 let confidence = (0.50 + (sim - 0.50).abs()).clamp(0.0, 1.0);
226
227 ScoreResult {
228 score,
229 confidence,
230 }
231 }
232}
233
234#[derive(Clone, Debug, PartialEq)]
236pub struct ScoreResult {
237 pub score: f32,
238 pub confidence: f32,
239}
240
241fn extract_tokens(text: &str) -> HashSet<String> {
242 text.to_lowercase()
243 .split(|c: char| !(c.is_alphanumeric() || c == '_'))
244 .filter(|s| !s.is_empty())
245 .map(|s| s.to_string())
246 .collect()
247}