Skip to main content

sokuto_server/guardrails/
mod.rs

1//! # 前処理ガードレール統括パイプラインモジュール
2//!
3//! リクエスト受信直後から推論エンジン投入までの間に、以下の一連のパイプラインを適用する。
4//!
5//! - 物理的 OOM 防壁 (Limits / Fast-Fail)
6//! - 特殊トークン偽装・プロンプトインジェクション防壁 (Sanitizer)
7//! - コンテキスト縮約・ノイズ除去 (Compactor / Anti-Context Rot)
8//! - 相対日時絶対正規化 (Temporal Grounding)
9//! - 算術・数え上げ事前集計 (Arithmetic Annotation)
10
11pub mod arithmetic;
12pub mod attention;
13pub mod bimodal;
14pub mod compactor;
15pub mod fast_fail;
16pub mod limits;
17pub mod multitask;
18pub mod nli;
19pub mod proxy;
20pub mod sanitization;
21pub mod streaming;
22pub mod temporal;
23pub mod window;
24
25use std::sync::Arc;
26
27use chrono::{DateTime, Utc};
28use indexmap::IndexMap;
29use serde_json::Value;
30use sokuto_core::schema::{Answer, Criteria, Question, QuestionType, SystemOneRequest};
31
32use crate::error::ServerError;
33use crate::guardrails::arithmetic::{ArithmeticConfig, compute_arithmetic_summary};
34pub use crate::guardrails::attention::{
35    ATTN_MASK_ALLOW, ATTN_MASK_BLOCK, AttentionIsolationConfig, SegmentType,
36    build_flat_attention_mask, build_segment_isolated_attention_mask,
37};
38use crate::guardrails::bimodal::{BimodalConfig, BimodalDetector, verify_and_adjust_bimodal};
39use crate::guardrails::compactor::{CompactorConfig, compact_text};
40pub use crate::guardrails::fast_fail::{
41    FastFailConfig, FastFailScanner, GuardrailAction, GuardrailViolation,
42};
43use crate::guardrails::limits::{LimitsConfig, validate_limits};
44pub use crate::guardrails::multitask::{
45    MARKER_INJECTION, MARKER_PII, MARKER_TOXICITY, MultiTaskScanResult, MultiTaskScannerConfig,
46    build_multitask_prompt, compute_free_energy, evaluate_multitask_logits, sigmoid,
47};
48pub use crate::guardrails::nli::{
49    HallucinationVerificationResult, HallucinationVerifierConfig, NormalizedEntity,
50    check_entity_mismatch, compute_synonym_coverage, extract_normalized_entities, verify_claim_nli,
51};
52pub use crate::guardrails::proxy::{GuardrailEngine, inline_guardrail_middleware};
53use crate::guardrails::sanitization::{
54    SanitizerConfig, normalize_noul_instruction, sanitize_score_criteria_text, sanitize_text,
55};
56pub use crate::guardrails::streaming::{
57    SENTENCE_DELIMITERS, SentenceChunkBuffer, SpeculativeStreamInterceptor,
58    StreamingVerifierConfig, extract_content_from_sse_line, intercept_sse_stream,
59};
60use crate::guardrails::temporal::{
61    TemporalConfig, append_reference_time_metadata, normalize_temporal,
62};
63pub use crate::guardrails::window::{
64    WindowConfig, WindowExtractionResult, estimate_tokens, extract_head_tail_window,
65};
66
67/// ガードレール全体の統合設定構造体。
68#[derive(Debug, Clone, PartialEq)]
69pub struct GuardrailConfig {
70    /// ガードレール全体の有効化フラグ。
71    pub enabled: bool,
72    /// 物理リソース制限設定。
73    pub limits: LimitsConfig,
74    /// 決定論的 Fast-Fail 禁止語設定。
75    pub fast_fail: FastFailConfig,
76    /// 境界縮約窓設定。
77    pub window: WindowConfig,
78    /// 構造的分離アテンション設定。
79    pub attention: AttentionIsolationConfig,
80    /// マルチタスク動的マーカー並列スキャン設定。
81    pub multitask: MultiTaskScannerConfig,
82    /// 出力ハルシネーション検証設定。
83    pub hallucination: HallucinationVerifierConfig,
84    /// 投機的ストリーミング検証設定。
85    pub streaming: StreamingVerifierConfig,
86    /// サニタイズ設定。
87    pub sanitizer: SanitizerConfig,
88    /// コンテキスト縮約設定。
89    pub compactor: CompactorConfig,
90    /// 相対日時正規化設定。
91    pub temporal: TemporalConfig,
92    /// 算術事前集計設定。
93    pub arithmetic: ArithmeticConfig,
94    /// 二峰性 (バイモーダル) 分布検出設定。
95    pub bimodal: BimodalConfig,
96}
97
98impl Default for GuardrailConfig {
99    fn default() -> Self {
100        Self {
101            enabled: true,
102            limits: LimitsConfig::default(),
103            fast_fail: FastFailConfig::default(),
104            window: WindowConfig::default(),
105            attention: AttentionIsolationConfig::default(),
106            multitask: MultiTaskScannerConfig::default(),
107            hallucination: HallucinationVerifierConfig::default(),
108            streaming: StreamingVerifierConfig::default(),
109            sanitizer: SanitizerConfig::default(),
110            compactor: CompactorConfig::default(),
111            temporal: TemporalConfig::default(),
112            arithmetic: ArithmeticConfig::default(),
113            bimodal: BimodalConfig::default(),
114        }
115    }
116}
117
118/// 前処理ガードレール実行結果レポート。
119#[derive(Debug, Clone, Default)]
120pub struct PreprocessReport {
121    /// 事前見積もりされたトークン数。
122    pub estimated_tokens: usize,
123    /// 算術集計ブロックが付加されたか。
124    pub added_arithmetic_summary: bool,
125    /// 基準時刻メタデータが付加されたか。
126    pub added_reference_time: bool,
127    /// 境界縮約窓が適用されたか。
128    pub window_truncated: bool,
129}
130
131/// 前処理および後処理ガードレールパイプライン。
132#[derive(Clone)]
133pub struct GuardrailPipeline {
134    config: Arc<GuardrailConfig>,
135    bimodal_detector: BimodalDetector,
136    fast_fail_scanner: Arc<FastFailScanner>,
137}
138
139impl GuardrailPipeline {
140    /// 新規 `GuardrailPipeline` を生成する。
141    pub fn new(config: GuardrailConfig) -> Self {
142        let bimodal_detector = BimodalDetector::new(config.bimodal.clone());
143        let fast_fail_scanner = Arc::new(FastFailScanner::new(config.fast_fail.clone()));
144        Self {
145            config: Arc::new(config),
146            bimodal_detector,
147            fast_fail_scanner,
148        }
149    }
150
151    /// 設定への参照を取得する。
152    pub fn config(&self) -> &GuardrailConfig {
153        &self.config
154    }
155
156    /// リクエストを検査および正規化する。
157    ///
158    /// # 引数
159    /// - `req`: 可変推論リクエスト参照。
160    /// - `ref_time`: 基準日時 (省略時は現在日時 `Utc::now()`)。
161    ///
162    /// # 戻り値
163    /// 成功時は前処理レポートを返却し、物理上限超過時は `ServerError::PayloadTooLarge` を返却する。
164    pub fn process(
165        &self,
166        req: &mut SystemOneRequest,
167        ref_time: Option<DateTime<Utc>>,
168    ) -> Result<PreprocessReport, ServerError> {
169        if !self.config.enabled {
170            return Ok(PreprocessReport::default());
171        }
172
173        let now = ref_time.unwrap_or_else(Utc::now);
174        let mut report = PreprocessReport::default();
175
176        // 0. 決定論的 Fast-Fail 走査 (Aho-Corasick, <0.1ms)
177        if let Value::String(ref s) = req.state
178            && let Err(violation) = self.fast_fail_scanner.scan(s)
179        {
180            return Err(ServerError::GuardrailViolation(violation.rule));
181        }
182        for question in req.questions.values() {
183            if let Err(violation) = self.fast_fail_scanner.scan(&question.instructions) {
184                return Err(ServerError::GuardrailViolation(violation.rule));
185            }
186        }
187
188        // 1. 物理 OOM 防壁 (L1/L2 Fast-Fail)
189        let (_, estimated_tokens) = validate_limits(req, &self.config.limits)?;
190        report.estimated_tokens = estimated_tokens;
191
192        // 2. State の正規化 (文字列への展開と前処理)
193        let mut raw_state = match &req.state {
194            Value::String(s) => s.clone(),
195            other => other.to_string(),
196        };
197
198        // 境界縮約窓の適用 (長文に対する Early-Exit 窓走査)
199        let window_res = extract_head_tail_window(&raw_state, &self.config.window);
200        if window_res.was_truncated {
201            raw_state = window_res.window_text.into_owned();
202            report.window_truncated = true;
203        }
204
205        // 算術集計サマリーの事前計算 (元の Value から集計)
206        let arithmetic_summary = compute_arithmetic_summary(&req.state, &self.config.arithmetic);
207
208        // State 文字列のサニタイズ、縮約、日時正規化
209        let state_sanitized = sanitize_text(&raw_state, &self.config.sanitizer);
210        let state_compacted = compact_text(&state_sanitized, &self.config.compactor);
211        let state_temporal = normalize_temporal(&state_compacted, now, &self.config.temporal);
212
213        raw_state = state_temporal.into_owned();
214
215        // 算術集計サマリーの付加
216        if let Some(summary) = arithmetic_summary {
217            raw_state.push_str(&summary);
218            report.added_arithmetic_summary = true;
219        }
220
221        // 基準時刻メタデータの付加
222        if self.config.temporal.attach_reference_time {
223            append_reference_time_metadata(
224                &mut raw_state,
225                now,
226                self.config.temporal.timezone_offset_hours,
227            );
228            report.added_reference_time = true;
229        }
230
231        // 正規化後の State を req に再格納
232        req.state = Value::String(raw_state);
233
234        // 3. 各 Question (Instructions, Criteria) の正規化
235        let mut normalized_questions = IndexMap::with_capacity(req.questions.len());
236
237        for (q_key, question) in &req.questions {
238            // Instructions の正規化
239            let inst_sanitized = sanitize_text(&question.instructions, &self.config.sanitizer);
240            let inst_compacted = compact_text(&inst_sanitized, &self.config.compactor);
241            let inst_temporal = normalize_temporal(&inst_compacted, now, &self.config.temporal);
242            let mut normalized_inst = inst_temporal.into_owned();
243
244            // Noul 型言明の正準プロンプトテンプレート自動ラッピング
245            if question.question_type == QuestionType::Noul
246                && self.config.sanitizer.normalize_noul_instructions
247            {
248                normalized_inst = normalize_noul_instruction(&normalized_inst).into_owned();
249            }
250
251            // Criteria の正規化
252            let normalized_criteria = match &question.criteria {
253                Some(Criteria::Map(map)) => {
254                    let mut new_map = IndexMap::with_capacity(map.len());
255                    for (k, v) in map {
256                        let k_sanitized = sanitize_text(k, &self.config.sanitizer);
257                        let v_sanitized = sanitize_text(v, &self.config.sanitizer);
258                        let v_compacted = compact_text(&v_sanitized, &self.config.compactor);
259                        new_map.insert(k_sanitized.into_owned(), v_compacted.into_owned());
260                    }
261                    Some(Criteria::Map(new_map))
262                }
263                Some(Criteria::List(list)) => {
264                    let mut new_list = Vec::with_capacity(list.len());
265                    for item in list {
266                        let item_sanitized = sanitize_text(item, &self.config.sanitizer);
267                        let item_compacted = compact_text(&item_sanitized, &self.config.compactor);
268                        let mut item_final = item_compacted.into_owned();
269
270                        // Score 型 Criteria の数字・記号プレフィックス自動サニタイズ (プロンプト用)
271                        if question.question_type == QuestionType::Score
272                            && self.config.sanitizer.sanitize_score_prefixes
273                        {
274                            item_final = sanitize_score_criteria_text(&item_final).into_owned();
275                        }
276
277                        new_list.push(item_final);
278                    }
279                    Some(Criteria::List(new_list))
280                }
281                Some(Criteria::None) => Some(Criteria::None),
282                None => None,
283            };
284
285            let new_question = Question {
286                question_type: question.question_type,
287                instructions: normalized_inst,
288                criteria: normalized_criteria,
289            };
290
291            normalized_questions.insert(q_key.clone(), new_question);
292        }
293
294        req.questions = normalized_questions;
295
296        Ok(report)
297    }
298
299    /// 推論完了後のレスポンス (Answer 群) に対し、後処理ガードレールを適用する。
300    ///
301    /// # 処理内容
302    /// 1. **二峰性 (バイモーダル) 分布検出**: Score 型の回答において意見二極化を検知した場合、Gating を `ConfirmOrEscalate` へ強制降格する。
303    /// 2. **Score 型レスポンスキーの非破壊復元**: 前処理サニタイズで剥離されたプレフィックスを持つ元のラベル名に `probabilities` のマップキーを復元し、クライアント API 破壊を防止する。
304    pub fn post_process(
305        &self,
306        answers: &mut IndexMap<String, Answer>,
307        original_questions: &IndexMap<String, Question>,
308    ) {
309        if !self.config.enabled {
310            return;
311        }
312
313        // 1. 二峰性 (バイモーダル) 分布検出と Gating 降格
314        verify_and_adjust_bimodal(answers, original_questions, &self.bimodal_detector);
315
316        // 2. Score 型レスポンスキーの復元
317        if self.config.sanitizer.sanitize_score_prefixes {
318            for (qid, answer) in answers.iter_mut() {
319                let Some(orig_q) = original_questions.get(qid) else {
320                    continue;
321                };
322
323                if orig_q.question_type != QuestionType::Score {
324                    continue;
325                }
326
327                let Some(Criteria::List(orig_list)) = &orig_q.criteria else {
328                    continue;
329                };
330
331                let Some(ref mut probs) = answer.probabilities else {
332                    continue;
333                };
334
335                // 段階数が一致している場合、元のラベルキーでマップを再構築する
336                if probs.len() == orig_list.len() {
337                    let mut restored_probs = IndexMap::with_capacity(orig_list.len());
338                    for (k, orig_label) in orig_list.iter().enumerate() {
339                        if let Some((_, &p)) = probs.get_index(k) {
340                            restored_probs.insert(orig_label.clone(), p);
341                        }
342                    }
343                    *probs = restored_probs;
344                }
345            }
346        }
347    }
348}
349
350impl Default for GuardrailPipeline {
351    fn default() -> Self {
352        Self::new(GuardrailConfig::default())
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use chrono::TimeZone;
360    use serde_json::json;
361
362    #[test]
363    fn test_pipeline_end_to_end() {
364        let pipeline = GuardrailPipeline::default();
365        let ref_time = Utc.with_ymd_and_hms(2026, 9, 22, 10, 0, 0).unwrap();
366
367        let mut questions = IndexMap::new();
368        questions.insert(
369            "q1".to_string(),
370            Question::new_choice(
371                "3日前の注文 [OP] を確認せよ。".to_string(),
372                IndexMap::from([("yes".to_string(), "はい [OP]".to_string())]),
373            ),
374        );
375
376        let mut req = SystemOneRequest::new(
377            json!({
378                "items": [
379                    {"name": "apple", "amount": 100},
380                    {"name": "banana", "amount": 200}
381                ]
382            }),
383            questions,
384        );
385
386        let res = pipeline.process(&mut req, Some(ref_time));
387        assert!(res.is_ok());
388
389        let state_str = req.state.as_str().unwrap();
390        // 特殊トークンは含まれていないが、算術集計と基準時刻が付与されていること
391        assert!(state_str.contains("items_count=2"));
392        assert!(state_str.contains("total_amount=300"));
393        assert!(state_str.contains("[Reference Time: 2026-09-22T10:00:00Z]"));
394
395        // Question 側の [OP] がサニタイズされ、相対日時が絶対化されていること
396        let q1 = req.questions.get("q1").unwrap();
397        assert!(q1.instructions.contains("3日前 (2026-09-19)"));
398        assert!(q1.instructions.contains("[ OP ]"));
399
400        let criteria_map = q1.criteria.as_ref().unwrap().as_map().unwrap();
401        assert_eq!(criteria_map.get("yes").unwrap(), "はい [ OP ]");
402    }
403
404    #[test]
405    fn test_pipeline_score_and_noul_guardrails() {
406        let pipeline = GuardrailPipeline::default();
407
408        let mut questions = IndexMap::new();
409        // 1. Score 型質問 (数字記号プレフィックス付き)
410        questions.insert(
411            "q_score".to_string(),
412            Question::new_score(
413                "インシデントの緊急度を判定せよ。".to_string(),
414                vec![
415                    "1: 軽微".to_string(),
416                    "2. 中度".to_string(),
417                    "(3) 重大".to_string(),
418                    "第4段階: 致命的".to_string(),
419                ],
420            ),
421        );
422        // 2. Noul 型質問 (簡潔な疑問文)
423        questions.insert(
424            "q_noul".to_string(),
425            Question::new_noul("初期不良か?".to_string()),
426        );
427
428        let orig_questions = questions.clone();
429        let mut req = SystemOneRequest::new("システムログ本文".to_string(), questions);
430
431        // 前処理の実行
432        let res = pipeline.process(&mut req, None);
433        assert!(res.is_ok());
434
435        // Score 型の Criteria がサニタイズされていること (プロンプト用)
436        let q_score = req.questions.get("q_score").unwrap();
437        match &q_score.criteria {
438            Some(Criteria::List(list)) => {
439                assert_eq!(list, &["軽微", "中度", "重大", "致命的"]);
440            }
441            _ => panic!("Criteria::List が必要です。"),
442        }
443
444        // Noul 型の Instructions が正準テンプレートに正規化されていること
445        let q_noul = req.questions.get("q_noul").unwrap();
446        assert_eq!(
447            q_noul.instructions,
448            "前提テキストの情報のみに基づいて、言明「初期不良」が真実であるか評価せよ。"
449        );
450
451        // 推論結果のシミュレーション (サニタイズされたラベルで Answer が生成されたと仮定)
452        let mut answers = IndexMap::new();
453        let mut score_probs = IndexMap::new();
454        score_probs.insert("軽微".to_string(), 0.1);
455        score_probs.insert("中度".to_string(), 0.7);
456        score_probs.insert("重大".to_string(), 0.15);
457        score_probs.insert("致命的".to_string(), 0.05);
458
459        answers.insert(
460            "q_score".to_string(),
461            Answer::score(1.15, score_probs, 0.85),
462        );
463
464        // 後処理の適用
465        pipeline.post_process(&mut answers, &orig_questions);
466
467        // Score 型の probabilities キーがクライアント送信時の元ラベルに復元されていること
468        let ans_score = answers.get("q_score").unwrap();
469        let probs = ans_score.probabilities.as_ref().unwrap();
470        assert_eq!(probs.get("1: 軽微"), Some(&0.1));
471        assert_eq!(probs.get("2. 中度"), Some(&0.7));
472        assert_eq!(probs.get("(3) 重大"), Some(&0.15));
473        assert_eq!(probs.get("第4段階: 致命的"), Some(&0.05));
474    }
475}