Skip to main content

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