mockforge_intelligence/intelligent_behavior/
behavior.rs1use 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
16pub struct BehaviorModel {
18 config: BehaviorModelConfig,
20
21 rules: BehaviorRules,
23
24 llm_client: Arc<LlmClient>,
26
27 cache: Option<Arc<ResponseCache>>,
29}
30
31impl BehaviorModel {
32 pub fn new(config: BehaviorModelConfig) -> Self {
34 let rules = config.rules.clone();
35 let llm_client = Arc::new(LlmClient::new(config.clone()));
36
37 let cache = Some(Arc::new(ResponseCache::new(300))); Self {
42 config,
43 rules,
44 llm_client,
45 cache,
46 }
47 }
48
49 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 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 self.check_consistency_rules(method, path, context).await?;
77
78 let prompt = self.build_prompt(method, path, request_body.as_ref(), context).await;
80
81 let response = self.generate_with_llm(&prompt).await?;
83
84 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 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 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 match &rule.action {
112 super::rules::RuleAction::RequireAuth { message } => {
113 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 }
129 }
130 }
131 }
132
133 Ok(())
134 }
135
136 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 let context_summary = context.build_context_summary().await;
157 prompt.push('\n');
158 prompt.push_str(&context_summary);
159
160 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 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 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 self.llm_client.generate(&request).await
188 }
189
190 pub fn rules(&self) -> &BehaviorRules {
192 &self.rules
193 }
194
195 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 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}