aurabase 0.1.1

Official Rust SDK for Aurabase: high-performance open-source Backend-as-a-Service (BaaS)
Documentation
use crate::client::{AuraClient, RequestBody};
use crate::error::AuraError;
use crate::types::{
    AuraResponse, ChatMessage, ChatResponse, EmbedRequest, EmbedResponse, RagIngestRequest,
    RagIngestResponse, RagRequest, RagResponse, SemanticSearchResult,
};
use serde::{Deserialize, Serialize};

pub struct AiService {
    client: AuraClient,
}

#[derive(Debug, Clone, Default)]
pub struct ChatOptions {
    pub model: Option<String>,
    pub temperature: Option<f64>,
    pub max_tokens: Option<u32>,
}

#[derive(Debug, Clone, Default)]
pub struct Nl2SqlOptions {
    pub dialect: Option<String>,
    pub examples: Option<Vec<Nl2SqlExample>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Nl2SqlExample {
    pub question: String,
    pub sql: String,
}

impl AiService {
    pub fn new(client: AuraClient) -> Self {
        Self { client }
    }

    fn prefix(&self) -> String {
        "/v1/ai".to_string()
    }

    /// Envoie une requête de complétion de chat.
    pub async fn chat(
        &self,
        messages: Vec<ChatMessage>,
        options: Option<ChatOptions>,
    ) -> Result<AuraResponse<ChatResponse>, AuraError> {
        let opts = options.unwrap_or_default();
        let mut body = serde_json::json!({
            "messages": messages,
        });

        if let Some(m) = opts.model {
            body["model"] = serde_json::Value::String(m);
        }
        if let Some(t) = opts.temperature {
            body["temperature"] = serde_json::json!(t);
        }
        if let Some(mt) = opts.max_tokens {
            body["max_tokens"] = serde_json::json!(mt);
        }

        self.client
            .request(
                reqwest::Method::POST,
                &format!("{}/chat", self.prefix()),
                RequestBody::Json(body),
            )
            .await
    }

    /// Génère des embeddings pour l'entrée fournie.
    pub async fn embed(&self, req: EmbedRequest) -> Result<AuraResponse<EmbedResponse>, AuraError> {
        self.client
            .request(
                reqwest::Method::POST,
                &format!("{}/embed", self.prefix()),
                RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
            )
            .await
    }

    /// Effectue une requête RAG (Retrieval-Augmented Generation).
    pub async fn rag(&self, req: RagRequest) -> Result<AuraResponse<RagResponse>, AuraError> {
        self.client
            .request(
                reqwest::Method::POST,
                &format!("{}/rag/query", self.prefix()),
                RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
            )
            .await
    }

    /// Ingeste un document dans un namespace RAG.
    pub async fn rag_ingest(
        &self,
        req: RagIngestRequest,
    ) -> Result<AuraResponse<RagIngestResponse>, AuraError> {
        self.client
            .request(
                reqwest::Method::POST,
                &format!("{}/rag/ingest", self.prefix()),
                RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
            )
            .await
    }

    /// Effectue une recherche sémantique.
    pub async fn semantic_search(
        &self,
        query: &str,
        namespace: &str,
        top_k: Option<u32>,
        threshold: Option<f64>,
    ) -> Result<AuraResponse<Vec<SemanticSearchResult>>, AuraError> {
        let mut params = Vec::new();
        params.push(format!("query={}", urlencoding::encode(query)));
        params.push(format!("namespace={}", urlencoding::encode(namespace)));
        if let Some(k) = top_k {
            params.push(format!("top_k={}", k));
        }
        if let Some(t) = threshold {
            params.push(format!("threshold={}", t));
        }

        self.client
            .request(
                reqwest::Method::GET,
                &format!("{}/search?{}", self.prefix(), params.join("&")),
                RequestBody::None,
            )
            .await
    }

    /// Convertit une question en langage naturel en requête SQL.
    ///
    /// ⚠️ **Le paramètre `schema` a été RETIRÉ** (B16). Le serveur introspecte
    /// lui-même le schéma depuis la base du projet — lui seul fait autorité sur ce
    /// qui existe réellement — et **refuse désormais en `400`** toute requête portant
    /// `schema`, `allowed_schema` ou `schema_context` (garde
    /// `reject_server_owned_fields`, premier acte du handler `nl2sql`). Tant que ce
    /// SDK l'émettait, **100 % des appels `nl2sql` échouaient**.
    ///
    /// Migration : `nl2sql("question", "todos(id uuid)", opts)` →
    /// `nl2sql("question", opts)`. L'ancien appel ne compile plus, ce qui est voulu :
    /// l'accepter puis le jeter côté SDK aurait simplement déplacé le mensonge de
    /// contrat d'un cran.
    ///
    /// `dialect` n'accepte que PostgreSQL (`"postgresql"` ou son alias `"postgres"`) ;
    /// toute autre valeur est refusée en `400`.
    pub async fn nl2sql(
        &self,
        question: &str,
        options: Option<Nl2SqlOptions>,
    ) -> Result<AuraResponse<serde_json::Value>, AuraError> {
        let opts = options.unwrap_or_default();
        let body = serde_json::json!({
            "question": question,
            "dialect": opts.dialect,
            "examples": opts.examples,
        });

        self.client
            .request(
                reqwest::Method::POST,
                &format!("{}/nl2sql", self.prefix()),
                RequestBody::Json(body),
            )
            .await
    }
}