/// HTTP handler for POST /memory/visualize endpoint. /// /// Serves graph visualization queries with pagination support. /// Used for testing queries and understanding context depth impact. use actix_web::{web, HttpRequest, HttpResponse}; use serde_json::json; use crate::query::visualize::{VisualizeRequest, GraphVisualizer}; use crate::jwt_validator::validate_token; use crate::rate_limiter::RateLimiter; /// POST /memory/visualize /// /// Query knowledge graph around a search query, with pagination. /// /// # Request /// ```json /// { /// "project": "poimen", /// "query": "kubernetes troubleshooting", /// "depth": 2, /// "limit": 50, /// "page": 1, /// "include_low_confidence": false /// } /// ``` /// /// # Response (200 OK) /// ```json /// { /// "query": "kubernetes troubleshooting", /// "project": "poimen", /// "pagination": { /// "page": 1, /// "limit": 50, /// "total_nodes": 487, /// "total_pages": 10, /// "has_next": true, /// "has_prev": false /// }, /// "depth_breakdown": { /// "depth_0": 12, /// "depth_1": 234, /// "depth_2": 241 /// }, /// "nodes": [...], /// "edges": [...], /// "performance": { /// "query_time_ms": 145, /// "depth_1_time_ms": 45, /// "depth_2_time_ms": 100, /// "total_time_ms": 157 /// }, /// "recommendations": [ /// { /// "issue": "high_result_count", /// "suggestion": "Try depth=1 to reduce from 487→234 nodes", /// "expected_latency_ms": 95 /// } /// ] /// } /// ``` pub async fn handle_visualize( req: HttpRequest, body: web::Json, rate_limiter: web::Data, ) -> HttpResponse { // 1. Extract and validate token let token = match extract_bearer_token(&req) { Ok(t) => t, Err(e) => { return error_response(401, "unauthorized", &format!("Missing token: {}", e)); } }; let claims = match validate_token(&token) { Ok(c) => c, Err(e) => { return error_response(401, "unauthorized", &format!("Invalid token: {}", e)); } }; // 2. Check rate limit (visualize: 100/hour) let user_id = &claims.sub; if !rate_limiter.check_limit(user_id, "visualize", 100) { return error_response( 429, "rate_limit_exceeded", "Visualize limit: 100/hour", ); } // 3. Validate request if body.project.is_empty() || body.query.is_empty() { return error_response(400, "invalid_request", "Missing project or query"); } // 4. Check project access // TODO: Verify user has access to this project via RBAC // 5. Execute visualization query let response = match GraphVisualizer::visualize(&body, "").await { Ok(r) => r, Err(e) => { return error_response(500, "internal_error", &format!("Visualization failed: {}", e)); } }; // 6. Return response HttpResponse::Ok().json(json!({ "query": response.query, "project": response.project, "pagination": response.pagination, "depth_breakdown": { "depth_0": response.depth_breakdown.depth_0, "depth_1": response.depth_breakdown.depth_1, "depth_2": response.depth_breakdown.depth_2, "depth_3": response.depth_breakdown.depth_3, }, "nodes": response.nodes, "edges": response.edges, "performance": { "query_time_ms": response.performance.query_time_ms, "depth_times_ms": response.performance.depth_times_ms, "total_time_ms": response.performance.total_time_ms, }, "recommendations": response.recommendations, })) } /// GET /memory/visualize/layouts /// /// Return available layout algorithms for graph visualization. pub async fn handle_visualize_layouts() -> HttpResponse { HttpResponse::Ok().json(json!({ "layouts": [ { "id": "force", "name": "Force-Directed", "description": "Physics simulation (good for dense graphs)", "params": { "strength": -30, "distance": 100 } }, { "id": "hierarchy", "name": "Hierarchical", "description": "Top-down tree layout", "params": { "gap": 40 } }, { "id": "circular", "name": "Circular", "description": "Nodes on a circle", "params": { "radius": 200 } } ] })) } /// GET /memory/visualize/styles /// /// Return node/edge styling presets. pub async fn handle_visualize_styles() -> HttpResponse { HttpResponse::Ok().json(json!({ "node_colors": { "person": "#4CAF50", "tool": "#2196F3", "concept": "#9C27B0", "location": "#FF9800", "organization": "#F44336", "event": "#FFC107" }, "edge_styles": { "high_confidence": { "stroke": "#333", "strokeWidth": 3, "animated": true }, "medium_confidence": { "stroke": "#666", "strokeWidth": 2, "animated": false }, "low_confidence": { "stroke": "#999", "strokeWidth": 1, "strokeDasharray": "5,5" }, "contradiction": { "stroke": "#F44336", "strokeWidth": 3, "animated": true }, "under_review": { "stroke": "#FFC107", "strokeWidth": 2, "strokeDasharray": "3,3" } } })) } // ───────────────────────────────────────────────────────────────────────────── fn extract_bearer_token(req: &HttpRequest) -> Result { let header = req .headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .ok_or("Missing Authorization header")?; if !header.starts_with("Bearer ") { return Err("Invalid Authorization format".to_string()); } Ok(header[7..].to_string()) } fn error_response(status: u16, code: &str, message: &str) -> HttpResponse { let status_code = actix_web::http::StatusCode::from_u16(status) .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR); HttpResponse::build(status_code).json(json!({ "error": code, "message": message, "request_id": uuid::Uuid::new_v4().to_string(), })) } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_bearer_token_valid() { // Mock request with Bearer token // Note: This is simplified; real test would use actix test utilities let token = "eyJ0eXAiOiJKV1QiLCJhbGc..."; let header = format!("Bearer {}", token); assert!(header.starts_with("Bearer ")); } #[test] fn test_error_response_400() { let resp = error_response(400, "bad_request", "Invalid input"); assert_eq!(resp.status(), actix_web::http::StatusCode::BAD_REQUEST); } #[test] fn test_error_response_401() { let resp = error_response(401, "unauthorized", "Invalid token"); assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED); } }