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:
+41
-41
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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")]
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user