Skip to main content

mockforge_intelligence/intelligent_behavior/
behavior.rs

1//! Behavior model for LLM-powered decision making
2//!
3//! This module provides the BehaviorModel which uses LLMs to make intelligent
4//! decisions about how the mock API should respond based on context.
5
6use std::sync::Arc;
7
8use super::cache::{generate_cache_key, ResponseCache};
9use super::config::BehaviorModelConfig;
10use super::context::StatefulAiContext;
11use super::llm_client::LlmClient;
12use super::rules::EvaluationContext;
13use super::types::{BehaviorRules, LlmGenerationRequest};
14use mockforge_foundation::Result;
15
16/// Behavior model that uses LLMs to generate intelligent responses
17pub struct BehaviorModel {
18    /// Configuration
19    config: BehaviorModelConfig,
20
21    /// Behavior rules
22    rules: BehaviorRules,
23
24    /// LLM client for generation
25    llm_client: Arc<LlmClient>,
26
27    /// Response cache
28    cache: Option<Arc<ResponseCache>>,
29}
30
31impl BehaviorModel {
32    /// Create a new behavior model
33    pub fn new(config: BehaviorModelConfig) -> Self {
34        let rules = config.rules.clone();
35        let llm_client = Arc::new(LlmClient::new(config.clone()));
36
37        // Create cache if enabled in config
38        // Note: Cache config should be in PerformanceConfig
39        let cache = Some(Arc::new(ResponseCache::new(300))); // 5 minutes default
40
41        Self {
42            config,
43            rules,
44            llm_client,
45            cache,
46        }
47    }
48
49    /// Generate a response based on request context and session state
50    ///
51    /// # Arguments
52    /// * `method` - HTTP method
53    /// * `path` - Request path
54    /// * `request_body` - Optional request body
55    /// * `context` - Stateful AI context for this session
56    ///
57    /// # Returns
58    /// Generated response as JSON value
59    pub async fn generate_response(
60        &self,
61        method: &str,
62        path: &str,
63        request_body: Option<serde_json::Value>,
64        context: &StatefulAiContext,
65    ) -> Result<serde_json::Value> {
66        // 1. Check cache if enabled
67        if let Some(ref cache) = self.cache {
68            let cache_key = generate_cache_key(method, path, request_body.as_ref());
69            if let Some(cached_response) = cache.get(&cache_key).await {
70                tracing::debug!("Cache hit for {} {}", method, path);
71                return Ok(cached_response);
72            }
73        }
74
75        // 2. Check consistency rules
76        self.check_consistency_rules(method, path, context).await?;
77
78        // 3. Build LLM prompt with context
79        let prompt = self.build_prompt(method, path, request_body.as_ref(), context).await;
80
81        // 4. Generate response using LLM
82        let response = self.generate_with_llm(&prompt).await?;
83
84        // 5. Store in cache if enabled
85        if let Some(ref cache) = self.cache {
86            let cache_key = generate_cache_key(method, path, request_body.as_ref());
87            cache.put(cache_key, response.clone()).await;
88        }
89
90        Ok(response)
91    }
92
93    /// Check consistency rules
94    async fn check_consistency_rules(
95        &self,
96        method: &str,
97        path: &str,
98        context: &StatefulAiContext,
99    ) -> Result<()> {
100        let state = context.get_state().await;
101        let _eval_context =
102            EvaluationContext::new(method, path).with_session_state(state.state.clone());
103
104        // Sort rules by priority (highest first)
105        let mut rules = self.rules.consistency_rules.clone();
106        rules.sort_by(|a, b| b.priority.cmp(&a.priority));
107
108        for rule in &rules {
109            if rule.matches(method, path) {
110                // Apply rule action
111                match &rule.action {
112                    super::rules::RuleAction::RequireAuth { message } => {
113                        // Check if user is authenticated
114                        if !state.state.contains_key("auth_token")
115                            && !state.state.contains_key("user_id")
116                        {
117                            return Err(mockforge_foundation::Error::internal(message.clone()));
118                        }
119                    }
120                    super::rules::RuleAction::Error { status, message } => {
121                        return Err(mockforge_foundation::Error::internal(format!(
122                            "Rule '{}' failed: {} (status {})",
123                            rule.name, message, status
124                        )));
125                    }
126                    _ => {
127                        // Other actions handled elsewhere
128                    }
129                }
130            }
131        }
132
133        Ok(())
134    }
135
136    /// Build LLM prompt from context
137    async fn build_prompt(
138        &self,
139        method: &str,
140        path: &str,
141        request_body: Option<&serde_json::Value>,
142        context: &StatefulAiContext,
143    ) -> String {
144        let mut prompt = format!(
145            "Generate a realistic response for this API request:\n\n\
146             Method: {}\n\
147             Path: {}\n",
148            method, path
149        );
150
151        if let Some(body) = request_body {
152            prompt.push_str(&format!("Request Body: {}\n", body));
153        }
154
155        // Add context summary
156        let context_summary = context.build_context_summary().await;
157        prompt.push('\n');
158        prompt.push_str(&context_summary);
159
160        // Add schemas
161        if !self.rules.schemas.is_empty() {
162            prompt.push_str("\n# Available Schemas\n");
163            for (name, schema) in &self.rules.schemas {
164                prompt.push_str(&format!("- {}: {}\n", name, schema));
165            }
166        }
167
168        prompt.push_str("\nGenerate a realistic JSON response that:\n");
169        prompt.push_str("1. Matches the request method and path\n");
170        prompt.push_str("2. Is consistent with the session context\n");
171        prompt.push_str("3. Conforms to the relevant schema if applicable\n");
172        prompt.push_str("4. Maintains logical consistency\n");
173
174        prompt
175    }
176
177    /// Generate response using LLM
178    async fn generate_with_llm(&self, prompt: &str) -> Result<serde_json::Value> {
179        tracing::debug!("Generating LLM response with prompt ({} chars)", prompt.len());
180
181        // Create LLM generation request
182        let request = LlmGenerationRequest::new(self.rules.system_prompt.clone(), prompt)
183            .with_temperature(self.config.temperature)
184            .with_max_tokens(self.config.max_tokens);
185
186        // Generate response using LLM client
187        self.llm_client.generate(&request).await
188    }
189
190    /// Get behavior rules
191    pub fn rules(&self) -> &BehaviorRules {
192        &self.rules
193    }
194
195    /// Get configuration
196    pub fn config(&self) -> &BehaviorModelConfig {
197        &self.config
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::super::config::IntelligentBehaviorConfig;
204    use super::*;
205
206    #[tokio::test]
207    async fn test_behavior_model_creation() {
208        let config = BehaviorModelConfig::default();
209        let model = BehaviorModel::new(config);
210
211        assert!(!model.rules().schemas.is_empty() || model.rules().schemas.is_empty());
212    }
213
214    #[tokio::test]
215    async fn test_generate_response() {
216        // Skip test if no OpenAI API key is available
217        if std::env::var("OPENAI_API_KEY").is_err() {
218            eprintln!("Skipping test_generate_response: OPENAI_API_KEY not set");
219            return;
220        }
221
222        let config = BehaviorModelConfig::default();
223        let model = BehaviorModel::new(config);
224
225        let ai_config = IntelligentBehaviorConfig::default();
226        let context = StatefulAiContext::new("test_session", ai_config);
227
228        let response = model.generate_response("GET", "/api/users", None, &context).await.unwrap();
229
230        assert!(response.is_object());
231    }
232}