sokuto_server/guardrails/
multitask.rs1use serde::{Deserialize, Serialize};
9
10use crate::guardrails::fast_fail::{GuardrailAction, GuardrailViolation};
11
12pub const MARKER_INJECTION: &str = "[INJ]";
14pub const MARKER_TOXICITY: &str = "[TOX]";
15pub const MARKER_PII: &str = "[PII]";
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct MultiTaskScannerConfig {
20 pub enabled: bool,
22 pub threshold_injection: f64,
24 pub threshold_toxicity: f64,
26 pub threshold_pii: f64,
28 pub threshold_energy: f64,
30 pub temperature: f64,
32 pub action: GuardrailAction,
34}
35
36impl Default for MultiTaskScannerConfig {
37 fn default() -> Self {
38 Self {
39 enabled: true,
40 threshold_injection: 0.5,
41 threshold_toxicity: 0.5,
42 threshold_pii: 0.5,
43 threshold_energy: -2.5,
44 temperature: 1.0,
45 action: GuardrailAction::Block,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct MultiTaskScanResult {
53 pub injection_prob: f64,
55 pub toxicity_prob: f64,
57 pub pii_prob: f64,
59 pub free_energy: f64,
61 pub is_threat_detected: bool,
63 pub is_ood: bool,
65 pub violations: Vec<GuardrailViolation>,
67}
68
69pub fn build_multitask_prompt(text: &str) -> String {
73 format!(
74 "{} {} {} {}",
75 MARKER_INJECTION, MARKER_TOXICITY, MARKER_PII, text
76 )
77}
78
79#[inline]
81pub fn sigmoid(logit: f64) -> f64 {
82 1.0 / (1.0 + (-logit).exp())
83}
84
85pub fn compute_free_energy(logits: &[f64], temperature: f64) -> f64 {
94 if logits.is_empty() {
95 return 0.0;
96 }
97 let t = if temperature <= 0.0 { 1.0 } else { temperature };
98
99 let mut total_energy = 0.0;
100 for &z in logits {
101 let abs_z = z.abs();
102 let a = abs_z / (2.0 * t);
105 let log_sum_exp = a + (-2.0 * a).exp().ln_1p();
106 total_energy += -t * log_sum_exp;
107 }
108
109 total_energy
110}
111
112pub fn evaluate_multitask_logits(
120 inj_logit: f64,
121 tox_logit: f64,
122 pii_logit: f64,
123 config: &MultiTaskScannerConfig,
124) -> MultiTaskScanResult {
125 let inj_prob = sigmoid(inj_logit);
126 let tox_prob = sigmoid(tox_logit);
127 let pii_prob = sigmoid(pii_logit);
128
129 let logits = [inj_logit, tox_logit, pii_logit];
130 let free_energy = compute_free_energy(&logits, config.temperature);
131
132 let mut violations = Vec::new();
133 let mut threat_detected = false;
134
135 if inj_prob >= config.threshold_injection {
136 threat_detected = true;
137 violations.push(GuardrailViolation {
138 rule: "multitask_prompt_injection".to_string(),
139 confidence: inj_prob,
140 snippet: None,
141 action_taken: config.action,
142 });
143 }
144
145 if tox_prob >= config.threshold_toxicity {
146 threat_detected = true;
147 violations.push(GuardrailViolation {
148 rule: "multitask_toxicity".to_string(),
149 confidence: tox_prob,
150 snippet: None,
151 action_taken: config.action,
152 });
153 }
154
155 if pii_prob >= config.threshold_pii {
156 threat_detected = true;
157 violations.push(GuardrailViolation {
158 rule: "multitask_pii_leakage".to_string(),
159 confidence: pii_prob,
160 snippet: None,
161 action_taken: config.action,
162 });
163 }
164
165 let is_ood = free_energy > config.threshold_energy;
166 if is_ood {
167 violations.push(GuardrailViolation {
168 rule: "multitask_free_energy_ood".to_string(),
169 confidence: sigmoid(free_energy),
170 snippet: Some(format!("free_energy={:.3}", free_energy)),
171 action_taken: config.action,
172 });
173 }
174
175 MultiTaskScanResult {
176 injection_prob: inj_prob,
177 toxicity_prob: tox_prob,
178 pii_prob,
179 free_energy,
180 is_threat_detected: threat_detected,
181 is_ood,
182 violations,
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_build_multitask_prompt() {
192 let prompt = build_multitask_prompt("ユーザー入力テスト");
193 assert_eq!(prompt, "[INJ] [TOX] [PII] ユーザー入力テスト");
194 }
195
196 #[test]
197 fn test_evaluate_multitask_safe() {
198 let config = MultiTaskScannerConfig::default();
199 let res = evaluate_multitask_logits(-5.0, -4.0, -6.0, &config);
201 assert!(!res.is_threat_detected);
202 assert!(res.injection_prob < 0.1);
203 assert!(res.toxicity_prob < 0.1);
204 assert!(res.pii_prob < 0.1);
205 }
206
207 #[test]
208 fn test_evaluate_multitask_injection_violation() {
209 let config = MultiTaskScannerConfig::default();
210 let res = evaluate_multitask_logits(3.0, -4.0, -5.0, &config);
212 assert!(res.is_threat_detected);
213 assert!(res.injection_prob > 0.9);
214 assert!(
215 res.violations
216 .iter()
217 .any(|v| v.rule == "multitask_prompt_injection")
218 );
219 }
220
221 #[test]
222 fn test_free_energy_ood() {
223 let config = MultiTaskScannerConfig {
224 threshold_energy: -3.0,
225 ..Default::default()
226 };
227 let res_confident = evaluate_multitask_logits(-5.0, -4.0, -5.0, &config);
229 assert!(!res_confident.is_ood);
230 assert!(res_confident.free_energy < -3.0);
231
232 let res_ambiguous = evaluate_multitask_logits(0.0, 0.0, 0.0, &config);
234 assert!(res_ambiguous.is_ood);
235 assert!(
236 res_ambiguous
237 .violations
238 .iter()
239 .any(|v| v.rule == "multitask_free_energy_ood")
240 );
241 }
242}