Skip to main content

mockforge_http/management/
rule_explanations.rs

1// Uses deprecated `mockforge_core::intelligent_behavior::RuleGenerator`
2// pending the eventual intelligent_behavior migration.
3#![allow(deprecated)]
4
5//! Rule-explanation storage and learn-from-examples endpoints
6//! (`GET /__mockforge/mockai/rules/explanations`,
7//! `GET /__mockforge/mockai/rules/{id}/explanation`,
8//! `POST /__mockforge/mockai/learn`).
9//!
10//! Split out of the original `management/ai_gen.rs` under #656. These
11//! three handlers share `ManagementState.rule_explanations` — the
12//! learn endpoint writes, the list/get endpoints read.
13//!
14//! Stays in `mockforge-http` rather than moving to `mockforge-intelligence`:
15//! the handlers are tightly coupled to `ManagementState`, and moving
16//! them would require either relocating `ManagementState` (large blast
17//! radius) or factoring out a slimmer state type just for these three.
18//! Neither has been judged worth the churn yet.
19
20use axum::{
21    extract::{Path, Query, State},
22    http::StatusCode,
23    response::{IntoResponse, Json},
24};
25use serde::Deserialize;
26
27use super::ManagementState;
28
29/// List all rule explanations
30pub(crate) async fn list_rule_explanations(
31    State(state): State<ManagementState>,
32    Query(params): Query<std::collections::HashMap<String, String>>,
33) -> impl IntoResponse {
34    use mockforge_foundation::intelligent_behavior::rule_types::RuleType;
35
36    let explanations = state.rule_explanations.read().await;
37    let mut explanations_vec: Vec<_> = explanations.values().cloned().collect();
38
39    // Filter by rule type if provided
40    if let Some(rule_type_str) = params.get("rule_type") {
41        if let Ok(rule_type) = serde_json::from_str::<RuleType>(&format!("\"{}\"", rule_type_str)) {
42            explanations_vec.retain(|e| e.rule_type == rule_type);
43        }
44    }
45
46    // Filter by minimum confidence if provided
47    if let Some(min_confidence_str) = params.get("min_confidence") {
48        if let Ok(min_confidence) = min_confidence_str.parse::<f64>() {
49            explanations_vec.retain(|e| e.confidence >= min_confidence);
50        }
51    }
52
53    // Sort by confidence (descending) and then by generated_at (descending)
54    explanations_vec.sort_by(|a, b| {
55        b.confidence
56            .partial_cmp(&a.confidence)
57            .unwrap_or(std::cmp::Ordering::Equal)
58            .then_with(|| b.generated_at.cmp(&a.generated_at))
59    });
60
61    Json(serde_json::json!({
62        "explanations": explanations_vec,
63        "total": explanations_vec.len(),
64    }))
65    .into_response()
66}
67
68/// Get a specific rule explanation by ID
69pub(crate) async fn get_rule_explanation(
70    State(state): State<ManagementState>,
71    Path(rule_id): Path<String>,
72) -> impl IntoResponse {
73    let explanations = state.rule_explanations.read().await;
74
75    match explanations.get(&rule_id) {
76        Some(explanation) => Json(serde_json::json!({
77            "explanation": explanation,
78        }))
79        .into_response(),
80        None => (
81            StatusCode::NOT_FOUND,
82            Json(serde_json::json!({
83                "error": "Rule explanation not found",
84                "message": format!("No explanation found for rule ID: {}", rule_id)
85            })),
86        )
87            .into_response(),
88    }
89}
90
91/// Request for learning from examples
92#[derive(Debug, Deserialize)]
93pub struct LearnFromExamplesRequest {
94    /// Example request/response pairs to learn from
95    pub examples: Vec<ExamplePairRequest>,
96    /// Optional configuration override
97    #[serde(default)]
98    pub config: Option<serde_json::Value>,
99}
100
101/// Example pair request format
102#[derive(Debug, Deserialize)]
103pub struct ExamplePairRequest {
104    /// Request data (method, path, body, etc.)
105    pub request: serde_json::Value,
106    /// Response data (status_code, body, etc.)
107    pub response: serde_json::Value,
108}
109
110/// Learn behavioral rules from example pairs
111///
112/// This endpoint accepts example request/response pairs, generates behavioral rules
113/// with explanations, and stores the explanations for later retrieval.
114pub(crate) async fn learn_from_examples(
115    State(state): State<ManagementState>,
116    Json(request): Json<LearnFromExamplesRequest>,
117) -> impl IntoResponse {
118    use mockforge_core::intelligent_behavior::{
119        config::{BehaviorModelConfig, IntelligentBehaviorConfig},
120        rule_generator::{ExamplePair, RuleGenerator},
121    };
122
123    if request.examples.is_empty() {
124        return (
125            StatusCode::BAD_REQUEST,
126            Json(serde_json::json!({
127                "error": "No examples provided",
128                "message": "At least one example pair is required"
129            })),
130        )
131            .into_response();
132    }
133
134    // Convert request examples to ExamplePair format
135    let example_pairs: Result<Vec<ExamplePair>, String> = request
136        .examples
137        .into_iter()
138        .enumerate()
139        .map(|(idx, ex)| {
140            // Parse request JSON to extract method, path, body, etc.
141            let method = ex
142                .request
143                .get("method")
144                .and_then(|v| v.as_str())
145                .map(|s| s.to_string())
146                .unwrap_or_else(|| "GET".to_string());
147            let path = ex
148                .request
149                .get("path")
150                .and_then(|v| v.as_str())
151                .map(|s| s.to_string())
152                .unwrap_or_else(|| "/".to_string());
153            let request_body = ex.request.get("body").cloned();
154            let query_params = ex
155                .request
156                .get("query_params")
157                .and_then(|v| v.as_object())
158                .map(|obj| {
159                    obj.iter()
160                        .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
161                        .collect()
162                })
163                .unwrap_or_default();
164            let headers = ex
165                .request
166                .get("headers")
167                .and_then(|v| v.as_object())
168                .map(|obj| {
169                    obj.iter()
170                        .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
171                        .collect()
172                })
173                .unwrap_or_default();
174
175            // Parse response JSON to extract status, body, etc.
176            let status = ex
177                .response
178                .get("status_code")
179                .or_else(|| ex.response.get("status"))
180                .and_then(|v| v.as_u64())
181                .map(|n| n as u16)
182                .unwrap_or(200);
183            let response_body = ex.response.get("body").cloned();
184
185            Ok(ExamplePair {
186                method,
187                path,
188                request: request_body,
189                status,
190                response: response_body,
191                query_params,
192                headers,
193                metadata: {
194                    let mut meta = std::collections::HashMap::new();
195                    meta.insert("source".to_string(), "api".to_string());
196                    meta.insert("example_index".to_string(), idx.to_string());
197                    meta
198                },
199            })
200        })
201        .collect();
202
203    let example_pairs = match example_pairs {
204        Ok(pairs) => pairs,
205        Err(e) => {
206            return (
207                StatusCode::BAD_REQUEST,
208                Json(serde_json::json!({
209                    "error": "Invalid examples",
210                    "message": e
211                })),
212            )
213                .into_response();
214        }
215    };
216
217    // Create behavior config (use provided config or default)
218    let behavior_config = if let Some(config_json) = request.config {
219        // Try to deserialize custom config, fall back to default
220        serde_json::from_value(config_json)
221            .unwrap_or_else(|_| IntelligentBehaviorConfig::default())
222            .behavior_model
223    } else {
224        BehaviorModelConfig::default()
225    };
226
227    // Create rule generator
228    let generator = RuleGenerator::new(behavior_config);
229
230    // Generate rules with explanations
231    let (rules, explanations) =
232        match generator.generate_rules_with_explanations(example_pairs).await {
233            Ok(result) => result,
234            Err(e) => {
235                return (
236                    StatusCode::INTERNAL_SERVER_ERROR,
237                    Json(serde_json::json!({
238                        "error": "Rule generation failed",
239                        "message": format!("Failed to generate rules: {}", e)
240                    })),
241                )
242                    .into_response();
243            }
244        };
245
246    // Store explanations in ManagementState
247    {
248        let mut stored_explanations = state.rule_explanations.write().await;
249        for explanation in &explanations {
250            stored_explanations.insert(explanation.rule_id.clone(), explanation.clone());
251        }
252    }
253
254    // Prepare response
255    let response = serde_json::json!({
256        "success": true,
257        "rules_generated": {
258            "consistency_rules": rules.consistency_rules.len(),
259            "schemas": rules.schemas.len(),
260            "state_machines": rules.state_transitions.len(),
261            "system_prompt": !rules.system_prompt.is_empty(),
262        },
263        "explanations": explanations.iter().map(|e| serde_json::json!({
264            "rule_id": e.rule_id,
265            "rule_type": e.rule_type,
266            "confidence": e.confidence,
267            "reasoning": e.reasoning,
268        })).collect::<Vec<_>>(),
269        "total_explanations": explanations.len(),
270    });
271
272    Json(response).into_response()
273}