Skip to main content

mockforge_intelligence/handlers/
ai_studio.rs

1//! AI Studio HTTP handlers
2//!
3//! This module provides HTTP endpoints for AI Studio features:
4//! - API Architecture Critique
5//! - System Generation
6//! - Behavioral Simulation
7
8// AI Studio engines (ApiCritiqueEngine, SystemGenerator, BehavioralSimulator,
9// ArtifactFreezer) hold LlmClient + do I/O, so they stay in core. Data types
10// were extracted to foundation::ai_studio_types (A20). Allow here for the
11// remaining engine imports.
12#![allow(deprecated)]
13
14use crate::ai_studio::{
15    api_critique::ApiCritiqueEngine, artifact_freezer::ArtifactFreezer,
16    behavioral_simulator::BehavioralSimulator, config::DeterministicModeConfig,
17    system_generator::SystemGenerator,
18};
19use crate::intelligent_behavior::IntelligentBehaviorConfig;
20use axum::{
21    extract::{Path, State},
22    http::StatusCode,
23    response::Json,
24    routing::post,
25    Router,
26};
27use mockforge_foundation::ai_studio_types::{
28    CreateAgentRequest, CritiqueRequest, FreezeMetadata, FreezeRequest, SimulateBehaviorRequest,
29    SystemGenerationRequest,
30};
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::sync::Arc;
34use tokio::sync::{Mutex, RwLock};
35use tracing::{error, info, warn};
36
37/// State for AI Studio handlers
38#[derive(Clone)]
39pub struct AiStudioState {
40    /// API critique engine
41    pub critique_engine: Arc<ApiCritiqueEngine>,
42    /// System generator engine
43    pub system_generator: Arc<SystemGenerator>,
44    /// Behavioral simulator engine (wrapped in Mutex for mutability)
45    pub behavioral_simulator: Arc<Mutex<BehavioralSimulator>>,
46    /// Artifact freezer for storing critiques and systems
47    pub artifact_freezer: Arc<ArtifactFreezer>,
48    /// AI configuration (for metadata)
49    pub config: IntelligentBehaviorConfig,
50    /// Deterministic mode configuration (optional, from workspace)
51    pub deterministic_config: Option<DeterministicModeConfig>,
52    /// Workspace ID (optional, can be set per request)
53    pub workspace_id: Option<String>,
54    /// In-memory storage for generated systems (system_id -> GeneratedSystem)
55    pub system_storage:
56        Arc<RwLock<HashMap<String, mockforge_foundation::ai_studio_types::GeneratedSystem>>>,
57}
58
59impl AiStudioState {
60    /// Create new AI Studio state
61    pub fn new(config: IntelligentBehaviorConfig) -> Self {
62        let critique_engine = Arc::new(ApiCritiqueEngine::new(config.clone()));
63        let system_generator = Arc::new(SystemGenerator::new(config.clone()));
64        let behavioral_simulator = Arc::new(Mutex::new(BehavioralSimulator::new(config.clone())));
65        let artifact_freezer = Arc::new(ArtifactFreezer::new());
66        Self {
67            critique_engine,
68            system_generator,
69            behavioral_simulator,
70            artifact_freezer,
71            config,
72            deterministic_config: None,
73            workspace_id: None,
74            system_storage: Arc::new(RwLock::new(HashMap::new())),
75        }
76    }
77
78    /// Create with custom workspace ID and deterministic config
79    pub fn with_workspace(
80        mut self,
81        workspace_id: String,
82        deterministic_config: Option<DeterministicModeConfig>,
83    ) -> Self {
84        self.workspace_id = Some(workspace_id);
85        self.deterministic_config = deterministic_config;
86        self
87    }
88}
89
90/// Request body for API critique endpoint
91#[derive(Debug, Deserialize, Serialize)]
92pub struct ApiCritiqueRequest {
93    /// API schema (OpenAPI JSON, GraphQL SDL, or Protobuf)
94    pub schema: serde_json::Value,
95
96    /// Schema type: "openapi", "graphql", or "protobuf"
97    pub schema_type: String,
98
99    /// Optional focus areas for analysis
100    /// Valid values: "anti-patterns", "redundancy", "naming", "tone", "restructuring"
101    #[serde(default)]
102    pub focus_areas: Vec<String>,
103
104    /// Optional workspace ID
105    pub workspace_id: Option<String>,
106}
107
108/// Response for API critique endpoint
109#[derive(Debug, Serialize)]
110pub struct ApiCritiqueResponse {
111    /// The critique result
112    pub critique: crate::ai_studio::api_critique::ApiCritique,
113
114    /// Artifact ID if stored
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub artifact_id: Option<String>,
117
118    /// Whether the critique was frozen as an artifact
119    pub frozen: bool,
120}
121
122/// Analyze an API schema and generate critique
123///
124/// POST /api/v1/ai-studio/api-critique
125pub async fn api_critique_handler(
126    State(state): State<AiStudioState>,
127    Json(request): Json<ApiCritiqueRequest>,
128) -> Result<Json<ApiCritiqueResponse>, StatusCode> {
129    info!("API critique request received for schema type: {}", request.schema_type);
130
131    // Build critique request
132    let critique_request = CritiqueRequest {
133        schema: request.schema,
134        schema_type: request.schema_type,
135        focus_areas: request.focus_areas,
136        workspace_id: request.workspace_id.or_else(|| state.workspace_id.clone()),
137    };
138
139    // Generate critique
140    let critique = match state.critique_engine.analyze(&critique_request).await {
141        Ok(c) => c,
142        Err(e) => {
143            error!("Failed to generate API critique: {}", e);
144            return Err(StatusCode::INTERNAL_SERVER_ERROR);
145        }
146    };
147
148    // Store as artifact
149    let critique_json = match serde_json::to_value(&critique) {
150        Ok(v) => v,
151        Err(e) => {
152            error!("Failed to serialize critique: {}", e);
153            return Err(StatusCode::INTERNAL_SERVER_ERROR);
154        }
155    };
156
157    // Create metadata for artifact freezing
158    let freeze_request = FreezeRequest {
159        artifact_type: "api_critique".to_string(),
160        content: critique_json,
161        format: "json".to_string(),
162        path: None,
163        metadata: Some(FreezeMetadata {
164            llm_provider: Some(state.config.behavior_model.llm_provider.clone()),
165            llm_model: Some(state.config.behavior_model.model.clone()),
166            llm_version: None,
167            prompt_hash: None,
168            output_hash: None,
169            original_prompt: None,
170        }),
171    };
172
173    let frozen_artifact = match state.artifact_freezer.freeze(&freeze_request).await {
174        Ok(a) => a,
175        Err(e) => {
176            error!("Failed to freeze critique artifact: {}", e);
177            return Err(StatusCode::INTERNAL_SERVER_ERROR);
178        }
179    };
180
181    info!(
182        "API critique completed. Artifact path: {}, Tokens used: {:?}, Cost: ${:.4}",
183        frozen_artifact.path,
184        critique.tokens_used,
185        critique.cost_usd.unwrap_or(0.0)
186    );
187
188    Ok(Json(ApiCritiqueResponse {
189        critique,
190        artifact_id: Some(frozen_artifact.path),
191        frozen: true,
192    }))
193}
194
195/// Request body for system generation endpoint
196#[derive(Debug, Deserialize)]
197pub struct SystemGenerationHttpRequest {
198    /// Natural language description
199    pub description: String,
200
201    /// Output formats to generate
202    #[serde(default)]
203    pub output_formats: Vec<String>,
204
205    /// Optional workspace ID
206    pub workspace_id: Option<String>,
207
208    /// Optional system ID (for versioning)
209    pub system_id: Option<String>,
210}
211
212/// Response for system generation endpoint
213#[derive(Debug, Serialize)]
214pub struct SystemGenerationResponse {
215    /// Generated system
216    pub system: mockforge_foundation::ai_studio_types::GeneratedSystem,
217}
218
219/// Request body for apply system design endpoint
220#[derive(Debug, Deserialize)]
221pub struct ApplySystemRequest {
222    /// Optional artifact IDs to apply (if None, applies all)
223    #[serde(default)]
224    pub artifact_ids: Option<Vec<String>>,
225}
226
227/// Response for apply system design endpoint
228#[derive(Debug, Serialize)]
229pub struct ApplySystemResponse {
230    /// Applied system result
231    pub applied: crate::ai_studio::system_generator::AppliedSystem,
232}
233
234/// Request body for freeze artifacts endpoint
235#[derive(Debug, Deserialize)]
236pub struct FreezeArtifactsRequest {
237    /// Artifact IDs to freeze
238    pub artifact_ids: Vec<String>,
239}
240
241/// Response for freeze artifacts endpoint
242#[derive(Debug, Serialize)]
243pub struct FreezeArtifactsResponse {
244    /// Frozen artifact paths
245    pub frozen_paths: Vec<String>,
246}
247
248/// Generate a complete system from natural language description
249///
250/// POST /api/v1/ai-studio/generate-system
251pub async fn generate_system_handler(
252    State(state): State<AiStudioState>,
253    Json(request): Json<SystemGenerationHttpRequest>,
254) -> Result<Json<SystemGenerationResponse>, StatusCode> {
255    info!("System generation request received");
256
257    let generation_request = SystemGenerationRequest {
258        description: request.description,
259        output_formats: request.output_formats,
260        workspace_id: request.workspace_id.or_else(|| state.workspace_id.clone()),
261        system_id: request.system_id,
262    };
263
264    let system = match state
265        .system_generator
266        .generate(&generation_request, state.deterministic_config.as_ref())
267        .await
268    {
269        Ok(s) => s,
270        Err(e) => {
271            error!("Failed to generate system: {}", e);
272            return Err(StatusCode::INTERNAL_SERVER_ERROR);
273        }
274    };
275
276    // Store the generated system in storage
277    {
278        let mut storage = state.system_storage.write().await;
279        storage.insert(system.system_id.clone(), system.clone());
280    }
281
282    info!(
283        "System generation completed. System ID: {}, Version: {}, Status: {}, Tokens: {:?}, Cost: ${:.4}",
284        system.system_id,
285        system.version,
286        system.status,
287        system.tokens_used,
288        system.cost_usd.unwrap_or(0.0)
289    );
290
291    Ok(Json(SystemGenerationResponse { system }))
292}
293
294/// Apply system design (freeze artifacts if deterministic mode requires it)
295///
296/// POST /api/v1/ai-studio/system/{system_id}/apply
297pub async fn apply_system_handler(
298    State(state): State<AiStudioState>,
299    Path(system_id): Path<String>,
300    Json(request): Json<ApplySystemRequest>,
301) -> Result<Json<ApplySystemResponse>, StatusCode> {
302    info!("Apply system request received for system: {}", system_id);
303
304    // Load the system from storage
305    let system = {
306        let storage = state.system_storage.read().await;
307        storage.get(&system_id).cloned()
308    };
309
310    let system = match system {
311        Some(s) => s,
312        None => {
313            warn!("System not found: {}", system_id);
314            return Err(StatusCode::NOT_FOUND);
315        }
316    };
317
318    // Apply the system design
319    let applied = match state
320        .system_generator
321        .apply_system_design(
322            &system,
323            state.deterministic_config.as_ref(),
324            request.artifact_ids.clone(),
325        )
326        .await
327    {
328        Ok(a) => a,
329        Err(e) => {
330            error!("Failed to apply system design: {}", e);
331            return Err(StatusCode::INTERNAL_SERVER_ERROR);
332        }
333    };
334
335    // Update the system status in storage if it was frozen
336    if applied.frozen {
337        let mut storage = state.system_storage.write().await;
338        if let Some(stored_system) = storage.get_mut(&system_id) {
339            stored_system.status = "frozen".to_string();
340        }
341    }
342
343    info!(
344        "System design applied. System ID: {}, Applied artifacts: {}, Frozen: {}",
345        applied.system_id,
346        applied.applied_artifacts.len(),
347        applied.frozen
348    );
349
350    Ok(Json(ApplySystemResponse { applied }))
351}
352
353/// Freeze specific artifacts manually
354///
355/// POST /api/v1/ai-studio/system/{system_id}/freeze
356pub async fn freeze_artifacts_handler(
357    State(state): State<AiStudioState>,
358    Path(system_id): Path<String>,
359    Json(request): Json<FreezeArtifactsRequest>,
360) -> Result<Json<FreezeArtifactsResponse>, StatusCode> {
361    info!(
362        "Freeze artifacts request received for system: {}, artifacts: {:?}",
363        system_id, request.artifact_ids
364    );
365
366    // Load the system from storage
367    let system = {
368        let storage = state.system_storage.read().await;
369        storage.get(&system_id).cloned()
370    };
371
372    let system = match system {
373        Some(s) => s,
374        None => {
375            warn!("System not found: {}", system_id);
376            return Err(StatusCode::NOT_FOUND);
377        }
378    };
379
380    // Freeze the specified artifacts
381    let frozen_paths = match state
382        .system_generator
383        .freeze_artifacts(&system, request.artifact_ids.clone())
384        .await
385    {
386        Ok(paths) => paths,
387        Err(e) => {
388            error!("Failed to freeze artifacts: {}", e);
389            return Err(StatusCode::INTERNAL_SERVER_ERROR);
390        }
391    };
392
393    // Update the system status in storage if all artifacts are frozen
394    if !frozen_paths.is_empty() {
395        let mut storage = state.system_storage.write().await;
396        if let Some(stored_system) = storage.get_mut(&system_id) {
397            // Check if all artifacts are now frozen
398            let all_frozen = stored_system
399                .artifacts
400                .values()
401                .all(|artifact| request.artifact_ids.contains(&artifact.artifact_id));
402            if all_frozen {
403                stored_system.status = "frozen".to_string();
404            }
405        }
406    }
407
408    info!(
409        "Artifacts frozen. System ID: {}, Frozen paths: {}",
410        system_id,
411        frozen_paths.len()
412    );
413
414    Ok(Json(FreezeArtifactsResponse { frozen_paths }))
415}
416
417/// Request body for create agent endpoint
418#[derive(Debug, Deserialize)]
419pub struct CreateAgentHttpRequest {
420    /// Optional: Attach to existing persona ID
421    pub persona_id: Option<String>,
422
423    /// Optional: Behavior policy type
424    pub behavior_policy: Option<String>,
425
426    /// If true, generate new persona if needed
427    pub generate_persona: bool,
428
429    /// Optional workspace ID
430    pub workspace_id: Option<String>,
431}
432
433/// Response for create agent endpoint
434#[derive(Debug, Serialize)]
435pub struct CreateAgentResponse {
436    /// Created agent
437    pub agent: mockforge_foundation::ai_studio_types::NarrativeAgent,
438}
439
440/// Request body for simulate behavior endpoint
441#[derive(Debug, Deserialize)]
442pub struct SimulateBehaviorHttpRequest {
443    /// Optional: Use existing agent ID
444    pub agent_id: Option<String>,
445
446    /// Optional: Attach to existing persona
447    pub persona_id: Option<String>,
448
449    /// Current app state
450    pub current_state: mockforge_foundation::ai_studio_types::AppState,
451
452    /// Trigger event
453    pub trigger_event: Option<String>,
454
455    /// Optional workspace ID
456    pub workspace_id: Option<String>,
457}
458
459/// Create a new narrative agent
460///
461/// POST /api/v1/ai-studio/simulate-behavior/create-agent
462#[axum::debug_handler]
463pub async fn create_agent_handler(
464    State(_state): State<AiStudioState>,
465    Json(_request): Json<CreateAgentHttpRequest>,
466) -> Result<Json<CreateAgentResponse>, StatusCode> {
467    info!("Create agent request received");
468    let state = _state;
469    let request = _request;
470
471    let create_request = CreateAgentRequest {
472        persona_id: request.persona_id,
473        behavior_policy: request.behavior_policy,
474        generate_persona: request.generate_persona,
475        workspace_id: request.workspace_id.or_else(|| state.workspace_id.clone()),
476    };
477
478    let mut simulator = state.behavioral_simulator.lock().await;
479    let agent = match simulator.create_agent(&create_request).await {
480        Ok(a) => a,
481        Err(e) => {
482            error!("Failed to create agent: {}", e);
483            return Err(StatusCode::INTERNAL_SERVER_ERROR);
484        }
485    };
486
487    info!("Agent created: {}", agent.agent_id);
488
489    Ok(Json(CreateAgentResponse { agent }))
490}
491
492/// Simulate behavior based on current state
493///
494/// POST /api/v1/ai-studio/simulate-behavior
495#[axum::debug_handler]
496pub async fn simulate_behavior_handler(
497    State(_state): State<AiStudioState>,
498    Json(_request): Json<SimulateBehaviorHttpRequest>,
499) -> Result<Json<mockforge_foundation::ai_studio_types::SimulateBehaviorResponse>, StatusCode> {
500    info!("Simulate behavior request received");
501    let state = _state;
502    let request = _request;
503
504    let simulate_request = SimulateBehaviorRequest {
505        agent_id: request.agent_id,
506        persona_id: request.persona_id,
507        current_state: request.current_state,
508        trigger_event: request.trigger_event,
509        workspace_id: request.workspace_id.or_else(|| state.workspace_id.clone()),
510    };
511
512    let mut simulator = state.behavioral_simulator.lock().await;
513    let response = match simulator.simulate_behavior(&simulate_request).await {
514        Ok(r) => r,
515        Err(e) => {
516            error!("Failed to simulate behavior: {}", e);
517            return Err(StatusCode::INTERNAL_SERVER_ERROR);
518        }
519    };
520
521    info!(
522        "Behavior simulation completed. Intention: {:?}, Tokens: {:?}, Cost: ${:.4}",
523        response.intention,
524        response.tokens_used,
525        response.cost_usd.unwrap_or(0.0)
526    );
527
528    Ok(Json(response))
529}
530
531/// Build AI Studio router
532pub fn ai_studio_router(state: AiStudioState) -> Router {
533    let router = Router::new()
534        .route("/api-critique", post(api_critique_handler))
535        .route("/generate-system", post(generate_system_handler))
536        .route("/system/{system_id}/apply", post(apply_system_handler))
537        .route("/system/{system_id}/freeze", post(freeze_artifacts_handler))
538        .route("/simulate-behavior/create-agent", post(create_agent_handler))
539        .route("/simulate-behavior", post(simulate_behavior_handler));
540    router.with_state(state)
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use mockforge_foundation::intelligent_behavior::BehaviorModelConfig;
547
548    fn create_test_state() -> AiStudioState {
549        let config = IntelligentBehaviorConfig {
550            behavior_model: BehaviorModelConfig {
551                llm_provider: "ollama".to_string(),
552                model: "llama2".to_string(),
553                api_endpoint: Some("http://localhost:11434/api/chat".to_string()),
554                api_key: None,
555                temperature: 0.7,
556                max_tokens: 2000,
557                rules: crate::intelligent_behavior::types::BehaviorRules::default(),
558            },
559            ..Default::default()
560        };
561        AiStudioState::new(config)
562    }
563
564    #[test]
565    fn test_ai_studio_state_creation() {
566        let state = create_test_state();
567        assert!(state.workspace_id.is_none());
568        assert!(state.deterministic_config.is_none());
569    }
570
571    #[test]
572    fn test_api_critique_request_serialization() {
573        let request = ApiCritiqueRequest {
574            schema: serde_json::json!({"openapi": "3.0.0"}),
575            schema_type: "openapi".to_string(),
576            focus_areas: vec!["anti-patterns".to_string()],
577            workspace_id: None,
578        };
579
580        let json = serde_json::to_string(&request).unwrap();
581        assert!(json.contains("openapi"));
582        assert!(json.contains("anti-patterns"));
583    }
584}