openserve 2.0.3

A modern, high-performance, AI-enhanced file server built in Rust
Documentation
//! AI handlers

use axum::{
    extract::State,
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use tracing::{debug, error};
use serde_json;

use crate::{
    server::AppState,
    models::{AnalysisRequest, ChatRequest, ChatResponse},
    handlers::{error_response, success_response},
};

/// Analyze file content
pub async fn analyze_content(
    State(state): State<AppState>,
    Json(request): Json<AnalysisRequest>,
) -> impl IntoResponse {
    debug!("Analyzing content for: {}", request.path);

    let ai_service = match &state.ai_service {
        Some(service) => service,
        None => return error_response(StatusCode::SERVICE_UNAVAILABLE, "AI service not available"),
    };

    // Read file content
    let content = match state.file_service.read_file(&request.path).await {
        Ok(data) => String::from_utf8_lossy(&data).to_string(),
        Err(e) => {
            error!("Failed to read file for analysis: {}", e);
            return error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file");
        }
    };

    match ai_service.analyze_content(&content, &request.path).await {
        Ok(analysis) => success_response(StatusCode::OK, &serde_json::to_string(&analysis).unwrap_or_default()),
        Err(e) => {
            error!("Content analysis failed: {}", e);
            error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
        }
    }
}

/// Summarize file content
pub async fn summarize(
    State(state): State<AppState>,
    Json(request): Json<AnalysisRequest>,
) -> impl IntoResponse {
    debug!("Summarizing content for: {}", request.path);

    let ai_service = match &state.ai_service {
        Some(service) => service,
        None => return error_response(StatusCode::SERVICE_UNAVAILABLE, "AI service not available"),
    };

    let content = match state.file_service.read_file(&request.path).await {
        Ok(data) => String::from_utf8_lossy(&data).to_string(),
        Err(e) => {
            error!("Failed to read file for summarization: {}", e);
            return error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file");
        }
    };

    match ai_service.analyze_content(&content, &request.path).await {
        Ok(analysis) => success_response(StatusCode::OK, &analysis.summary),
        Err(e) => {
            error!("Summarization failed: {}", e);
            error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
        }
    }
}

/// Get organization suggestions
pub async fn suggest_organization(
    State(state): State<AppState>,
) -> impl IntoResponse {
    debug!("Getting organization suggestions");

    let ai_service = match &state.ai_service {
        Some(service) => service,
        None => return error_response(StatusCode::SERVICE_UNAVAILABLE, "AI service not available"),
    };

    // Get file list from root directory
    let directory = match state.file_service.list_directory("/").await {
        Ok(dir) => dir,
        Err(e) => {
            error!("Failed to list files for organization: {}", e);
            return error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to list files");
        }
    };

    let file_infos: Vec<crate::ai::FileInfo> = directory.entries.iter()
        .filter_map(|entry| {
            if let crate::services::file::DirectoryEntry::File(file) = entry {
                Some(crate::ai::FileInfo {
                    name: file.name.clone(),
                    size: file.size,
                    size_human: format_bytes(file.size),
                    modified: file.modified,
                })
            } else {
                None
            }
        })
        .collect();

    match ai_service.suggest_organization(&file_infos).await {
        Ok(suggestion) => success_response(StatusCode::OK, &serde_json::to_string(&suggestion).unwrap_or_default()),
        Err(e) => {
            error!("Organization suggestion failed: {}", e);
            error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
        }
    }
}

/// Chat with files
pub async fn chat(
    State(state): State<AppState>,
    Json(request): Json<ChatRequest>,
) -> impl IntoResponse {
    debug!("Processing chat request: {}", request.message);

    let ai_service = match &state.ai_service {
        Some(service) => service,
        None => return error_response(StatusCode::SERVICE_UNAVAILABLE, "AI service not available"),
    };

    // Build context from specified paths
    let mut context = String::new();
    let mut context_used = Vec::new();

    if let Some(paths) = &request.context_paths {
        for path in paths {
            if let Ok(content_bytes) = state.file_service.read_file(path).await {

                let content = String::from_utf8_lossy(&content_bytes);
                let max_length = request.max_context_length.unwrap_or(2000);
                let truncated_content = if content.len() > max_length {
                    format!("{}...", &content[..max_length])
                } else {
                    content.to_string()
                };
                
                context.push_str(&format!("File: {}\n{}\n\n", path, truncated_content));
                context_used.push(path.clone());
            }
        }
    }

    match ai_service.chat(&request.message, &context).await {
        Ok(response) => success_response(StatusCode::OK, &serde_json::to_string(&ChatResponse {
            response,
            context_used,
            tokens_used: None, 
        }).unwrap_or_default()),
        Err(e) => {
            error!("Chat failed: {}", e);
            error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
        }
    }
}

/// Format bytes to human readable format
fn format_bytes(bytes: u64) -> String {
    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
    let mut size = bytes as f64;
    let mut unit_index = 0;

    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
        size /= 1024.0;
        unit_index += 1;
    }

    format!("{:.2} {}", size, UNITS[unit_index])
}