feat: Translate various strings and comments to Spanish for better localization

- Updated error messages and comments in main.rs, openapi.rs, portfolio.rs, predictive.rs, ai.rs, health.rs, middleware.rs, models.rs, token_limits.rs, and webhooks.rs to Spanish.
- Enhanced user experience by providing localized content for Spanish-speaking users.
This commit is contained in:
2026-04-10 10:26:26 -04:00
parent 7c48b3b1a9
commit 53e5ef4d0b
35 changed files with 1135 additions and 1144 deletions
+41 -41
View File
@@ -1,33 +1,33 @@
//! AI Utilities for OpenCCB
//! Provides embedding generation and other AI helper functions
//! Utilidades de IA para OpenCCB
//! Proporciona generación de embeddings y otras funciones de ayuda de IA
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Default embedding model for Ollama
/// Modelo de embedding por defecto para Ollama
pub const DEFAULT_EMBEDDING_MODEL: &str = "nomic-embed-text";
/// Default Ollama URL
/// URL de Ollama por defecto
pub const DEFAULT_OLLAMA_URL: &str = "http://localhost:11434";
/// Embedding dimensions for nomic-embed-text
/// Dimensiones del embedding para nomic-embed-text
pub const EMBEDDING_DIMENSIONS: usize = 768;
/// Model selection for different use cases
/// Selección de modelo para diferentes casos de uso
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelType {
/// Fast conversational AI (chat, tutor, Q&A)
/// IA conversacional rápida (chat, tutor, preguntas y respuestas)
Chat,
/// Complex reasoning (analysis, recommendations, feedback)
/// Razonamiento complejo (análisis, recomendaciones, retroalimentación)
Complex,
/// Advanced tasks (course generation, detailed analysis)
/// Tareas avanzadas (generación de cursos, análisis detallado)
Advanced,
/// Embedding generation
/// Generación de embeddings
Embedding,
}
impl ModelType {
/// Get the model name for this type from environment
/// Obtener el nombre del modelo para este tipo desde el entorno
pub fn get_model(&self) -> String {
match self {
ModelType::Chat => {
@@ -45,34 +45,34 @@ impl ModelType {
}
}
/// Get recommended temperature for this model type
/// Obtener la temperatura recomendada para este tipo de modelo
pub fn get_temperature(&self) -> f32 {
match self {
ModelType::Chat => 0.7, // Balanced creativity/accuracy
ModelType::Complex => 0.5, // More focused reasoning
ModelType::Advanced => 0.6, // Balanced for analysis
ModelType::Embedding => 0.0, // Deterministic for embeddings
ModelType::Chat => 0.7, // Equilibrio entre creatividad y precisión
ModelType::Complex => 0.5, // Razonamiento más enfocado
ModelType::Advanced => 0.6, // Equilibrado para análisis
ModelType::Embedding => 0.0, // Determinista para embeddings
}
}
/// Get max tokens for this model type
/// Obtener los tokens máximos para este tipo de modelo
pub fn get_max_tokens(&self) -> u32 {
match self {
ModelType::Chat => 1024,
ModelType::Complex => 2048,
ModelType::Advanced => 4096,
ModelType::Embedding => 0, // Not applicable
ModelType::Embedding => 0, // No aplicable
}
}
}
#[derive(Error, Debug)]
pub enum AiError {
#[error("Ollama request failed: {0}")]
#[error("Solicitud a Ollama fallida: {0}")]
OllamaRequest(String),
#[error("Invalid embedding response: {0}")]
#[error("Respuesta de embedding inválida: {0}")]
InvalidResponse(String),
#[error("Model not available: {0}")]
#[error("Modelo no disponible: {0}")]
ModelNotAvailable(String),
}
@@ -83,19 +83,19 @@ pub struct EmbeddingResponse {
pub model: String,
}
/// Get Ollama URL from environment or default
/// Obtener la URL de Ollama desde el entorno o por defecto
pub fn get_ollama_url() -> String {
std::env::var("LOCAL_OLLAMA_URL").unwrap_or_else(|_| DEFAULT_OLLAMA_URL.to_string())
}
/// Get embedding model from environment or default
/// Obtener el modelo de embedding desde el entorno o por defecto
pub fn get_embedding_model() -> String {
std::env::var("EMBEDDING_MODEL").unwrap_or_else(|_| DEFAULT_EMBEDDING_MODEL.to_string())
}
/// Get the best model for a specific task
/// Obtener el mejor modelo para una tarea específica
pub fn get_model_for_task(task: &str) -> String {
// Task-based model selection
// Selección de modelo basada en la tarea
match task.to_lowercase().as_str() {
t if t.contains("chat") || t.contains("tutor") || t.contains("conversation") => {
ModelType::Chat.get_model()
@@ -116,22 +116,22 @@ pub fn get_model_for_task(task: &str) -> String {
}
}
/// Create a reqwest client that accepts invalid certificates (for dev with self-signed certs)
/// Crear un cliente reqwest que acepte certificados inválidos (para desarrollo con certificados autofirmados)
fn create_insecure_client() -> Result<reqwest::Client, AiError> {
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.map_err(|e| AiError::OllamaRequest(format!("Failed to create HTTP client: {}", e)))
.map_err(|e| AiError::OllamaRequest(format!("Error al crear el cliente HTTP: {}", e)))
}
/// Generate embedding for text using Ollama
/// Generar embedding para texto usando Ollama
///
/// # Arguments
/// * `client` - reqwest::Client instance
/// * `ollama_url` - Base URL for Ollama (e.g., "http://localhost:11434")
/// * `model` - Embedding model name (default: "nomic-embed-text")
/// * `text` - Text to embed
/// # Argumentos
/// * `client` - instancia de reqwest::Client
/// * `ollama_url` - URL base para Ollama (e.g., "http://localhost:11434")
/// * `model` - Nombre del modelo de embedding (por defecto: "nomic-embed-text")
/// * `text` - Texto a incrustar
pub async fn generate_embedding(
client: &reqwest::Client,
ollama_url: &str,
@@ -148,25 +148,25 @@ pub async fn generate_embedding(
}))
.send()
.await
.map_err(|e| AiError::OllamaRequest(format!("Request failed: {}", e)))?;
.map_err(|e| AiError::OllamaRequest(format!("Solicitud fallida: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(AiError::OllamaRequest(
format!("Ollama API error ({}): {}", status, error_text)
format!("Error de la API de Ollama ({}): {}", status, error_text)
));
}
let embedding_response: EmbeddingResponse = response
.json()
.await
.map_err(|e| AiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
.map_err(|e| AiError::InvalidResponse(format!("Error al analizar la respuesta: {}", e)))?;
Ok(embedding_response)
}
/// Generate embeddings for multiple texts in batch
/// Generar embeddings para múltiples textos en lote
pub async fn generate_embeddings_batch(
client: &reqwest::Client,
ollama_url: &str,
@@ -183,8 +183,8 @@ pub async fn generate_embeddings_batch(
Ok(embeddings)
}
/// Convert a vector of f32 to pgvector-compatible format
/// PostgreSQL vector format: "[0.1,0.2,0.3,...]"
/// Convertir un vector de f32 a un formato compatible con pgvector
/// Formato de vector de PostgreSQL: "[0.1,0.2,0.3,...]"
pub fn embedding_to_pgvector(embedding: &[f32]) -> String {
let formatted: Vec<String> = embedding
.iter()
@@ -193,12 +193,12 @@ pub fn embedding_to_pgvector(embedding: &[f32]) -> String {
format!("[{}]", formatted.join(","))
}
/// Parse pgvector format back to Vec<f32>
/// Analizar el formato pgvector de vuelta a Vec<f32>
pub fn pgvector_to_embedding(pgvector: &str) -> Result<Vec<f32>, String> {
let trimmed = pgvector.trim().trim_start_matches('[').trim_end_matches(']');
trimmed
.split(',')
.map(|s| s.trim().parse::<f32>().map_err(|e| format!("Parse error: {}", e)))
.map(|s| s.trim().parse::<f32>().map_err(|e| format!("Error de análisis: {}", e)))
.collect()
}
+6 -6
View File
@@ -1,11 +1,11 @@
//! Health check endpoints for monitoring and observability
//! Endpoints de verificación de salud para monitoreo y observabilidad
use axum::{Json, Router, routing::get, extract::State};
use serde_json::json;
use sqlx::PgPool;
use std::time::Instant;
/// Health check state shared across requests
/// Estado de verificación de salud compartido entre peticiones
#[derive(Clone)]
pub struct HealthState {
pub start_time: Instant,
@@ -21,7 +21,7 @@ impl Default for HealthState {
}
}
/// Basic health check endpoint
/// Endpoint básico de verificación de salud
pub async fn health_check() -> Json<serde_json::Value> {
Json(json!({
"status": "healthy",
@@ -29,7 +29,7 @@ pub async fn health_check() -> Json<serde_json::Value> {
}))
}
/// Detailed readiness check including database connectivity
/// Verificación detallada de disponibilidad que incluye la conectividad con la base de datos
pub async fn readiness_check(pool: PgPool) -> Json<serde_json::Value> {
let db_status = match pool.acquire().await {
Ok(_) => "connected",
@@ -45,7 +45,7 @@ pub async fn readiness_check(pool: PgPool) -> Json<serde_json::Value> {
}))
}
/// Liveness check with uptime information
/// Verificación de vitalidad (liveness) con información de tiempo de actividad (uptime)
pub async fn liveness_check(state: State<HealthState>) -> Json<serde_json::Value> {
let uptime = state.start_time.elapsed();
@@ -57,7 +57,7 @@ pub async fn liveness_check(state: State<HealthState>) -> Json<serde_json::Value
}))
}
/// Create health routes
/// Crear rutas de salud
pub fn health_routes(pool: PgPool) -> Router<HealthState> {
Router::new()
.route("/health", get(health_check))
+1 -1
View File
@@ -28,7 +28,7 @@ pub async fn org_extractor_middleware(
let token = if let Some(token_str) = auth_header.and_then(|s: &str| s.strip_prefix("Bearer ")) {
token_str.to_string()
} else {
// Check for preview_token in query string
// Verificar si hay preview_token en la cadena de consulta
let query = req.uri().query().unwrap_or_default();
let preview_token = query
.split('&')
+56 -56
View File
@@ -11,7 +11,7 @@ pub struct Course {
pub title: String,
pub description: Option<String>,
pub instructor_id: Uuid,
pub pacing_mode: String, // "self_paced" or "instructor_led"
pub pacing_mode: String, // "self_paced" o "instructor_led"
pub start_date: Option<DateTime<Utc>>,
pub end_date: Option<DateTime<Utc>>,
pub passing_percentage: i32,
@@ -121,7 +121,7 @@ pub struct GradingCategory {
pub name: String,
pub weight: i32, // 0-100
pub drop_count: i32,
pub tipo_nota_id: Option<i32>, // Maps to idTipoNota in external MySQL system
pub tipo_nota_id: Option<i32>, // Se mapea con idTipoNota en el sistema MySQL externo
pub created_at: DateTime<Utc>,
}
@@ -131,7 +131,7 @@ pub struct UserGrade {
pub user_id: Uuid,
pub course_id: Uuid,
pub lesson_id: Uuid,
pub score: f32, // 0.0 to 1.0
pub score: f32, // 0.0 a 1.0
pub attempts_count: i32,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
@@ -187,7 +187,7 @@ pub struct Enrollment {
pub user_id: Uuid,
pub organization_id: Uuid,
pub course_id: Uuid,
pub external_id: Option<i32>, // idDetalleContrato from the external system
pub external_id: Option<i32>, // idDetalleContrato del sistema externo
pub enrolled_at: DateTime<Utc>,
}
@@ -242,7 +242,7 @@ pub struct LtiLaunchClaims {
#[serde(rename = "sub")]
pub subject: String,
#[serde(rename = "aud")]
pub audience: serde_json::Value, // Can be string or array
pub audience: serde_json::Value, // Puede ser una cadena o un array
#[serde(rename = "exp")]
pub expires_at: i64,
#[serde(rename = "iat")]
@@ -363,7 +363,7 @@ pub struct Transaction {
pub course_id: Uuid,
pub amount: f64,
pub currency: String,
pub status: String, // "pending", "success", "failure"
pub status: String, // "pending", "success", "failure" (pendiente, éxito, falla)
pub provider_reference: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -377,7 +377,7 @@ pub struct User {
pub email: String,
pub password_hash: String,
pub full_name: String,
pub role: String, // admin, instructor, student
pub role: String, // admin (administrador), instructor, student (estudiante)
pub xp: i32,
pub level: i32,
pub avatar_url: Option<String>,
@@ -538,7 +538,7 @@ pub struct Recommendation {
pub title: String,
pub description: String,
pub lesson_id: Option<Uuid>,
pub priority: String, // "high", "medium", "low"
pub priority: String, // "high", "medium", "low" (alta, media, baja)
pub reason: String,
}
@@ -596,7 +596,7 @@ pub struct AuditLog {
}
// Discussion Forums Models
// Modelos de Foros de Discusión
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct DiscussionThread {
pub id: Uuid,
@@ -633,7 +633,7 @@ pub struct DiscussionVote {
pub organization_id: Uuid,
pub post_id: Uuid,
pub user_id: Uuid,
pub vote_type: String, // 'upvote' or 'downvote'
pub vote_type: String, // 'upvote' o 'downvote'
pub created_at: DateTime<Utc>,
}
@@ -646,10 +646,10 @@ pub struct DiscussionSubscription {
pub created_at: DateTime<Utc>,
}
// Response DTOs for Discussion APIs
// DTOs de respuesta para las APIs de Discusión
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct ThreadWithAuthor {
// Thread fields
// Campos del hilo
pub id: Uuid,
pub organization_id: Uuid,
pub course_id: Uuid,
@@ -662,17 +662,17 @@ pub struct ThreadWithAuthor {
pub view_count: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// Author info
// Información del autor
pub author_name: String,
pub author_avatar: Option<String>,
// Aggregated data
// Datos agregados
pub post_count: i64,
pub has_endorsed_answer: bool,
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct PostWithAuthor {
// Post fields
// Campos de la publicación
pub id: Uuid,
pub organization_id: Uuid,
pub thread_id: Uuid,
@@ -686,14 +686,14 @@ pub struct PostWithAuthor {
// Author info
pub author_name: String,
pub author_avatar: Option<String>,
// User interaction
pub user_vote: Option<String>, // 'upvote', 'downvote', or null
// Nested replies (not from DB, populated manually)
// Interacción del usuario
pub user_vote: Option<String>, // 'upvote', 'downvote' o nulo
// Respuestas anidadas (no desde la BD, pobladas manualmente)
#[sqlx(skip)]
pub replies: Vec<PostWithAuthor>,
}
// Course Announcements
// Anuncios del Curso
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct CourseAnnouncement {
pub id: Uuid,
@@ -711,7 +711,7 @@ pub struct CourseAnnouncement {
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct AnnouncementWithAuthor {
// Announcement fields
// Campos del anuncio
pub id: Uuid,
pub organization_id: Uuid,
pub course_id: Uuid,
@@ -728,7 +728,7 @@ pub struct AnnouncementWithAuthor {
pub cohort_ids: Option<Vec<Uuid>>,
}
// Student Notes
// Notas del Estudiante
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct StudentNote {
pub id: Uuid,
@@ -744,7 +744,7 @@ pub struct SaveNotePayload {
pub content: String,
}
// Cohorts & Groups
// Cohortes y Grupos
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct Cohort {
pub id: Uuid,
@@ -784,7 +784,7 @@ pub struct StudentGradeReport {
pub last_active_at: Option<DateTime<Utc>>,
}
// Peer Assessment
// Evaluación por Pares
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct CourseSubmission {
pub id: Uuid,
@@ -832,7 +832,7 @@ pub struct SubmissionWithReviews {
pub average_score: Option<f64>,
}
// Content Libraries
// Librerías de Contenido
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct LibraryBlock {
pub id: Uuid,
@@ -894,8 +894,8 @@ pub struct DropoutRisk {
pub course_id: Uuid,
pub user_id: Uuid,
pub risk_level: DropoutRiskLevel,
pub score: f32, // 0.0 to 1.0 (Higher means higher risk)
pub reasons: Option<serde_json::Value>, // e.g., ["low_grades", "inactivity"]
pub score: f32, // 0.0 a 1.0 (Más alto significa mayor riesgo)
pub reasons: Option<serde_json::Value>, // ej., ["low_grades", "inactivity"]
pub last_calculated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -921,7 +921,7 @@ mod tests {
let lesson = Lesson {
id: lesson_id,
organization_id: course_id, // Use course_id as proxy for org_id in test
organization_id: course_id, // Usar course_id como proxi para org_id en la prueba
module_id,
title: "Test Lesson".to_string(),
content_type: "activity".to_string(),
@@ -1040,7 +1040,7 @@ mod tests {
}
}
// ==================== Advanced Grading / Rubrics ====================
// ==================== Calificación Avanzada / Rúbricas ====================
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct Rubric {
@@ -1112,7 +1112,7 @@ pub struct AssessmentScore {
pub feedback: Option<String>,
pub created_at: DateTime<Utc>,
}
// ==================== Learning Sequences / Dependencies ====================
// ==================== Secuencias de Aprendizaje / Dependencias ====================
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct LessonDependency {
@@ -1124,7 +1124,7 @@ pub struct LessonDependency {
pub created_at: DateTime<Utc>,
}
// ==================== Live Learning (Meetings) ====================
// ==================== Aprendizaje en Vivo (Reuniones) ====================
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct Meeting {
@@ -1134,7 +1134,7 @@ pub struct Meeting {
pub title: String,
pub description: Option<String>,
pub provider: String, // "jitsi" | "bbb"
pub meeting_id: String, // Room name or external ID
pub meeting_id: String, // Nombre de la sala o ID externo
pub start_at: DateTime<Utc>,
pub duration_minutes: i32,
pub join_url: Option<String>,
@@ -1143,7 +1143,7 @@ pub struct Meeting {
pub updated_at: DateTime<Utc>,
}
// ==================== Student portfolio & Badges ====================
// ==================== Portafolio del Estudiante e Insignias ====================
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)]
pub struct Badge {
@@ -1177,7 +1177,7 @@ pub struct PublicProfile {
pub completed_courses_count: i64,
}
// ==================== Test Templates ====================
// ==================== Plantillas de Examen ====================
#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq)]
#[sqlx(type_name = "course_level", rename_all = "lowercase")]
@@ -1206,15 +1206,15 @@ pub enum CourseType {
#[sqlx(type_name = "test_type")]
pub enum TestType {
#[sqlx(rename = "CA")]
CA, // Continuous Assessment
CA, // Evaluación Continua
#[sqlx(rename = "MWT")]
MWT, // Midterm Written Test
MWT, // Examen Escrito de Mitad de Ciclo
#[sqlx(rename = "MOT")]
MOT, // Midterm Oral Test
MOT, // Examen Oral de Mitad de Ciclo
#[sqlx(rename = "FOT")]
FOT, // Final Oral Test
FOT, // Examen Oral Final
#[sqlx(rename = "FWT")]
FWT, // Final Written Test
FWT, // Examen Escrito Final
}
impl std::fmt::Display for TestType {
@@ -1233,17 +1233,17 @@ impl std::fmt::Display for TestType {
pub struct TestTemplate {
pub id: Uuid,
pub organization_id: Uuid,
pub mysql_course_id: Option<i32>, // Reference to imported MySQL course
pub mysql_course_id: Option<i32>, // Referencia al curso de MySQL importado
pub name: String,
pub description: Option<String>,
pub level: Option<CourseLevel>, // Deprecated: use mysql_course_id instead
pub course_type: Option<CourseType>, // Deprecated: use mysql_course_id instead
pub level: Option<CourseLevel>, // Depreciado: use mysql_course_id en su lugar
pub course_type: Option<CourseType>, // Depreciado: use mysql_course_id en su lugar
pub test_type: TestType,
pub duration_minutes: i32,
pub passing_score: i32, // 0-100 percentage
pub passing_score: i32, // Porcentaje 0-100
pub total_points: i32,
pub instructions: Option<String>,
pub template_data: serde_json::Value, // Complete test structure with sections and questions
pub template_data: serde_json::Value, // Estructura completa del examen con secciones y preguntas
pub tags: Option<Vec<String>>,
pub is_active: bool,
pub usage_count: i32,
@@ -1261,7 +1261,7 @@ pub struct TestTemplateSection {
pub section_order: i32,
pub points: i32,
pub instructions: Option<String>,
pub section_data: Option<serde_json::Value>, // Section-specific configuration
pub section_data: Option<serde_json::Value>, // Configuración específica de la sección
pub created_at: DateTime<Utc>,
}
@@ -1277,7 +1277,7 @@ pub struct TestTemplateQuestion {
pub correct_answer: Option<serde_json::Value>, // Can be index, array of indices, or text
pub explanation: Option<String>,
pub points: i32,
pub metadata: Option<serde_json::Value>, // Additional question metadata
pub metadata: Option<serde_json::Value>, // Metadatos adicionales de la pregunta
pub created_at: DateTime<Utc>,
}
@@ -1285,9 +1285,9 @@ pub struct TestTemplateQuestion {
pub struct CreateTestTemplatePayload {
pub name: String,
pub description: Option<String>,
pub mysql_course_id: Option<i32>, // Reference to imported MySQL course (preferred)
pub level: Option<CourseLevel>, // Fallback if mysql_course_id not provided
pub course_type: Option<CourseType>, // Fallback if mysql_course_id not provided
pub mysql_course_id: Option<i32>, // Referencia al curso de MySQL importado (preferido)
pub level: Option<CourseLevel>, // Alternativa si no se proporciona mysql_course_id
pub course_type: Option<CourseType>, // Alternativa si no se proporciona mysql_course_id
pub test_type: TestType,
pub duration_minutes: i32,
pub passing_score: i32,
@@ -1327,7 +1327,7 @@ pub struct ApplyTemplatePayload {
pub grading_category_id: Option<Uuid>,
}
// ==================== Question Bank ====================
// ==================== Banco de Preguntas ====================
#[derive(Debug, Serialize, Deserialize, Clone, Copy, sqlx::Type, PartialEq)]
#[sqlx(type_name = "question_bank_type")]
@@ -1389,7 +1389,7 @@ pub struct QuestionBank {
pub points: i32,
pub difficulty: Option<String>,
pub tags: Option<Vec<String>>,
pub skill_assessed: Option<String>, // reading, listening, speaking, writing
pub skill_assessed: Option<String>, // lectura (reading), escucha (listening), habla (speaking), escritura (writing)
pub source: Option<String>,
pub source_metadata: Option<serde_json::Value>,
pub imported_mysql_id: Option<i32>,
@@ -1401,10 +1401,10 @@ pub struct QuestionBank {
pub created_by: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub embedding: Option<String>, // PGVector embedding for semantic search
pub embedding: Option<String>, // Embedding de PGVector para búsqueda semántica
pub embedding_updated_at: Option<chrono::DateTime<chrono::Utc>>,
pub source_asset_id: Option<Uuid>, // audio/video asset that originated this RAG chunk
pub unit_number: Option<i32>, // syllabus unit number from ZIP folder structure
pub source_asset_id: Option<Uuid>, // Activo de audio/video que originó este fragmento RAG
pub unit_number: Option<i32>, // Número de unidad del sílabo desde la estructura de carpetas ZIP
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -1419,7 +1419,7 @@ pub struct CreateQuestionBankPayload {
pub tags: Option<Vec<String>>,
pub media_url: Option<String>,
pub media_type: Option<String>,
pub skill_assessed: Option<String>, // reading, listening, speaking, writing
pub skill_assessed: Option<String>, // lectura (reading), escucha (listening), habla (speaking), escritura (writing)
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -1453,8 +1453,8 @@ pub struct QuestionBankFilters {
pub has_audio: Option<bool>,
}
// ==================== AUDIO RESPONSE MODELS ====================
// For speaking practice exercises with AI + Teacher evaluation
// ==================== MODELOS DE RESPUESTA DE AUDIO ====================
// Para ejercicios de práctica de habla con evaluación de IA + Profesor
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
+15 -15
View File
@@ -1,5 +1,5 @@
//! AI Token Limit Utilities
//! Provides functions to check and enforce monthly token limits
//! Utilidades de Límite de Tokens de IA
//! Proporciona funciones para verificar y hacer cumplir los límites mensuales de tokens
use sqlx::{PgPool, FromRow};
use uuid::Uuid;
@@ -23,16 +23,16 @@ pub struct TokenLimitError {
pub reset_date: DateTime<Utc>,
}
/// Verify if user has available tokens for AI operations
/// Verificar si el usuario tiene tokens disponibles para operaciones de IA
///
/// # Arguments
/// * `pool` - Database connection pool
/// * `user_id` - User UUID
/// * `estimated_tokens` - Estimated tokens for this operation (default: 1000)
/// # Argumentos
/// * `pool` - Pool de conexión a la base de datos
/// * `user_id` - UUID del usuario
/// * `estimated_tokens` - Tokens estimados para esta operación (por defecto: 1000)
///
/// # Returns
/// * `Ok(TokenLimitCheck)` - User has available tokens
/// * `Err(TokenLimitError)` - User exceeded limit
/// # Retornos
/// * `Ok(TokenLimitCheck)` - El usuario tiene tokens disponibles
/// * `Err(TokenLimitError)` - El usuario excedió el límite
pub async fn check_ai_token_limit(
pool: &PgPool,
user_id: Uuid,
@@ -46,9 +46,9 @@ pub async fn check_ai_token_limit(
.fetch_one(pool)
.await
.map_err(|e| {
tracing::error!("Failed to check token limit: {}", e);
tracing::error!("Error al verificar el límite de tokens: {}", e);
TokenLimitError {
error: "Failed to verify token limit".to_string(),
error: "Error al verificar el límite de tokens".to_string(),
monthly_limit: 0,
used_tokens: 0,
remaining_tokens: 0,
@@ -58,7 +58,7 @@ pub async fn check_ai_token_limit(
if !result.has_available_tokens {
tracing::warn!(
"User {} exceeded token limit: {}/{} (reset: {})",
"El usuario {} excedió el límite de tokens: {}/{} (reinicio: {})",
user_id,
result.used_tokens,
result.monthly_limit,
@@ -67,7 +67,7 @@ pub async fn check_ai_token_limit(
return Err(TokenLimitError {
error: format!(
"Monthly AI token limit exceeded. Used: {} / Limit: {}. Reset date: {}",
"Límite mensual de tokens de IA excedido. Usados: {} / Límite: {}. Fecha de reinicio: {}",
result.used_tokens,
result.monthly_limit,
result.reset_date.format("%Y-%m-%d %H:%M UTC")
@@ -82,7 +82,7 @@ pub async fn check_ai_token_limit(
Ok(result)
}
/// Get formatted token usage message for API responses
/// Obtener el mensaje formateado de uso de tokens para las respuestas de la API
pub fn format_token_limit_message(used: i64, limit: i32, reset: DateTime<Utc>) -> String {
let percentage = (used as f64 / limit as f64 * 100.0).round();
format!(
+3 -3
View File
@@ -23,7 +23,7 @@ impl WebhookService {
.await {
Ok(w) => w,
Err(e) => {
tracing::error!("Failed to fetch webhooks for org {}: {}", org_id, e);
tracing::error!("Error al obtener los webhooks para la org {}: {}", org_id, e);
return;
}
};
@@ -54,7 +54,7 @@ impl WebhookService {
Ok(response) => {
if !response.status().is_success() {
tracing::warn!(
"Webhook delivery to {} (event: {}) failed with status {}",
"La entrega del webhook a {} (evento: {}) falló con el estado {}",
url,
event_type,
response.status()
@@ -63,7 +63,7 @@ impl WebhookService {
}
Err(e) => {
tracing::error!(
"Failed to deliver webhook to {} (event: {}): {}",
"Error al entregar el webhook a {} (evento: {}): {}",
url,
event_type,
e