diff --git a/.gitignore b/.gitignore index a1e5ac6..7d04269 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +.venv \ No newline at end of file diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 40141ef..f9d9393 100644 Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc index ad1858e..14f1453 100644 Binary files a/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc and b/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc b/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc index 8b36043..56cbc09 100644 Binary files a/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc and b/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/audio/transcription.py b/app/api/v1/endpoints/audio/transcription.py index 572c84e..5c87158 100644 --- a/app/api/v1/endpoints/audio/transcription.py +++ b/app/api/v1/endpoints/audio/transcription.py @@ -18,7 +18,7 @@ from app.services.audio.transcription_adapters import transcribe_audio_with_prov # Inicializar el router de FastAPI para este módulo audio_router_transcription = APIRouter() -@audio_router_transcription.post("/transcripts/", response_model=StandardTranscriptionResult) +@audio_router_transcription.post("/transcripts", response_model=StandardTranscriptionResult) async def transcribe_audio(audio_request: AudioRequestFile = Depends()) -> StandardTranscriptionResult: """ Endpoint para transcribir audio simple infiriendo el proveedor de IA diff --git a/app/api/v1/endpoints/document/__pycache__/rubricated_analysis.cpython-312.pyc b/app/api/v1/endpoints/document/__pycache__/rubricated_analysis.cpython-312.pyc new file mode 100644 index 0000000..cde56cd Binary files /dev/null and b/app/api/v1/endpoints/document/__pycache__/rubricated_analysis.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/document/rubricated_analysis.py b/app/api/v1/endpoints/document/rubricated_analysis.py new file mode 100644 index 0000000..625d69d --- /dev/null +++ b/app/api/v1/endpoints/document/rubricated_analysis.py @@ -0,0 +1,37 @@ +""" +Gateway de IA de Qualidot - Módulo de Procesamiento de Documentos + +Propósito: + Este endpoint recibe un documento y lo analiza usando inteligencia artificial, + extrayendo información relevante y estructurada. + +Homologación: + Sin importar qué proveedor de IA se utilice, el resultado siempre se entrega en el mismo formato estándar para Qualidot. +""" + +from dotenv import load_dotenv +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException +from app.schemas.document_standard import DocumentRequestFile, StandardDocumentAnalysisResult +from app.services.document.evaluations_adapters import evaluate_document_with_provider + +# Inicializar el router de FastAPI para este módulo +document_router_analysis = APIRouter() + +@document_router_analysis.post("/evaluations", response_model=StandardDocumentAnalysisResult) +async def evaluate_document(document_request: DocumentRequestFile = Depends()) -> StandardDocumentAnalysisResult: + """ + Endpoint para analizar documentos usando inteligencia artificial + + Args: + document_request: Objeto DocumentRequestFile que contiene el archivo de documento, + el proveedor, el modelo y la rúbrica de evaluación. + + Returns: + StandardDocumentAnalysisResult: Resultado del análisis de documentos en formato estándar de Qualidot + """ + try: + # Analizar el documento usando el adaptador de análisis que infiere el proveedor + analysis_result = await evaluate_document_with_provider(document_request) + return analysis_result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/app/api/v1/endpoints/router.py b/app/api/v1/endpoints/router.py index a1163f8..09c0875 100644 --- a/app/api/v1/endpoints/router.py +++ b/app/api/v1/endpoints/router.py @@ -17,18 +17,19 @@ from fastapi import APIRouter, UploadFile, File, HTTPException from fastapi.security import APIKeyHeader # Importar los routers específicos de cada módulo -from app.api.v1.endpoints.audio.transcription import audio_router_transcription # Endpoint de transcripción de audio -from app.api.v1.endpoints.texto.resume import text_router_summary # Endpoint de resumen de texto -from app.api.v1.endpoints.texto.rubricated_analysis import text_router_analysis # Endpoint de análisis rubricado de texto -from app.api.v1.endpoints.video.transcription import video_router_transcription # Endpoint de transcripción de video -from app.api.v1.endpoints.image.rubricated_analysis import image_router_analysis # Endpoint de análisis rubricado de imágenes +from app.api.v1.endpoints.audio.transcription import audio_router_transcription # Endpoint de transcripción de audio +from app.api.v1.endpoints.texto.resume import text_router_summary # Endpoint de resumen de texto +from app.api.v1.endpoints.texto.rubricated_analysis import text_router_analysis # Endpoint de análisis rubricado de texto +from app.api.v1.endpoints.video.transcription import video_router_transcription # Endpoint de transcripción de video +from app.api.v1.endpoints.image.rubricated_analysis import image_router_analysis # Endpoint de análisis rubricado de imágenes +from app.api.v1.endpoints.document.rubricated_analysis import document_router_analysis # Endpoint de análisis rubricado de documentos # Inicializar el router de FastAPI para los módulos de procesamiento api_router_audio = APIRouter() api_router_text = APIRouter() api_router_video = APIRouter() api_router_image = APIRouter() - +api_router_document = APIRouter() api_router_audio.include_router( audio_router_transcription, @@ -58,4 +59,10 @@ api_router_image.include_router( image_router_analysis, prefix="/image", tags=["Procesamiento de Imágenes"] -) \ No newline at end of file +) + +api_router_document.include_router( + document_router_analysis, + prefix="/document", + tags=["Procesamiento de Documentos"] +) \ No newline at end of file diff --git a/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc b/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc index fa6687b..bb26e2d 100644 Binary files a/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc and b/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc b/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc index f40e7e3..7c77410 100644 Binary files a/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc and b/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/texto/resume.py b/app/api/v1/endpoints/texto/resume.py index 8b7f4c7..3ca3e65 100644 --- a/app/api/v1/endpoints/texto/resume.py +++ b/app/api/v1/endpoints/texto/resume.py @@ -19,7 +19,7 @@ from app.services.text.resume_adapters import summarize_text_with_provider # Inicializar el router de FastAPI para este módulo text_router_summary = APIRouter() -@text_router_summary.post("/summaries/", response_model=StandardTextAnalysisResult) +@text_router_summary.post("/summaries", response_model=StandardTextAnalysisResult) async def summarize_text(text_request: TextRequestFile = Depends()) -> StandardTextAnalysisResult: """ Endpoint para resumir texto simple infiriendo el proveedor de IA diff --git a/app/api/v1/endpoints/texto/rubricated_analysis.py b/app/api/v1/endpoints/texto/rubricated_analysis.py index a0200ef..9af8bd5 100644 --- a/app/api/v1/endpoints/texto/rubricated_analysis.py +++ b/app/api/v1/endpoints/texto/rubricated_analysis.py @@ -19,7 +19,7 @@ from app.services.text.evaluations_adapters import evaluate_text_with_provider # Inicializar el router de FastAPI para este módulo text_router_analysis = APIRouter() -@text_router_analysis.post("/evaluations/", response_model=StandardTextAnalysisResult) +@text_router_analysis.post("/evaluations", response_model=StandardTextAnalysisResult) async def evaluate_text(text_request: TextRequestFile = Depends()) -> StandardTextAnalysisResult: """ Endpoint para analizar texto usando una rúbrica de evaluación infiriendo el proveedor de IA diff --git a/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc b/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc index 129eba5..8e80452 100644 Binary files a/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc and b/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/video/transcription.py b/app/api/v1/endpoints/video/transcription.py index e7b60b0..a1ea182 100644 --- a/app/api/v1/endpoints/video/transcription.py +++ b/app/api/v1/endpoints/video/transcription.py @@ -16,7 +16,7 @@ from app.services.video.transcription_adapters import transcribe_video_with_prov # Inicializar el router de FastAPI para este módulo video_router_transcription = APIRouter() -@video_router_transcription.post("/transcripts/", response_model=StandardTranscriptionResult) +@video_router_transcription.post("/transcripts", response_model=StandardTranscriptionResult) async def transcribe_video(video_request: VideoRequestFile = Depends()) -> StandardTranscriptionResult: """ Endpoint para transcribir video simple infiriendo el proveedor de IA diff --git a/app/main.py b/app/main.py index ba7c11d..7d7c2f3 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from app.api.v1.endpoints.router import api_router_audio, api_router_text, api_router_image, api_router_video +from app.api.v1.endpoints.router import api_router_audio, api_router_text, api_router_image, api_router_video, api_router_document app = FastAPI( title="Template de API de Procesamiento general", @@ -11,6 +11,8 @@ app.include_router(api_router_audio, prefix="/api/v1", tags=["Procesamiento de A app.include_router(api_router_text, prefix="/api/v1", tags=["Procesamiento de Texto"]) app.include_router(api_router_image, prefix="/api/v1", tags=["Procesamiento de Imágenes"]) app.include_router(api_router_video, prefix="/api/v1", tags=["Procesamiento de Video"]) +app.include_router(api_router_document, prefix="/api/v1", tags=["Procesamiento de Documentos"]) + @app.get("/") def root(): @@ -26,18 +28,22 @@ def root(): "docs": "/docs", "endpoints": { "audio": { - "transcripción de audio": "/api/v1/audio/transcripts/", + "transcripción de audio": "/api/v1/audio/transcripts", }, "texto": { - "resumen de texto": "/api/v1/text/summaries/", - "análisis rubricado": "/api/v1/text/evaluations/" + "resumen de texto": "/api/v1/text/summaries", + "análisis rubricado": "/api/v1/text/evaluations" }, "imágenes": { - "análisis rubricado": "/api/v1/image/evaluations/", + "análisis rubricado": "/api/v1/image/evaluations", }, "video": { - "transcripción de video": "/api/v1/video/transcripts/" + "transcripción de video": "/api/v1/video/transcripts" + }, + "documentos": { + "análisis rubricado": "/api/v1/document/evaluations" } + }, "modelos_disponibles": { "audio": { diff --git a/app/schemas/__pycache__/document_standard.cpython-312.pyc b/app/schemas/__pycache__/document_standard.cpython-312.pyc new file mode 100644 index 0000000..723c60e Binary files /dev/null and b/app/schemas/__pycache__/document_standard.cpython-312.pyc differ diff --git a/app/schemas/document_standard.py b/app/schemas/document_standard.py new file mode 100644 index 0000000..94cfb53 --- /dev/null +++ b/app/schemas/document_standard.py @@ -0,0 +1,60 @@ +""" +Esquema de resultado esperado de modelos de análisis de documentos. + +Propósito: + Define el formato estándar de los resultados devueltos por los modelos de análisis + de documentos, independientemente del proveedor de IA utilizado. + +Homologación: + Garantiza que el resultado del análisis de documentos siempre se entregue en el mismo + formato estándar para Qualidot. +""" +from dataclasses import dataclass + +from fastapi import UploadFile +from fastapi.params import File, Form +from pydantic import BaseModel, Field +from typing import List, Optional + +class DocumentEvaluationCriteria(BaseModel): + """Modelo que representa un criterio de evaluación específico dentro de una rúbrica de análisis de imágenes.""" + name: str = Field(..., description="Nombre del criterio de evaluación") + description: str = Field(None, description="Descripción detallada del criterio") + score: float = Field(None, description="Puntuación asignada al criterio después del análisis") + subcriteria: Optional[List["DocumentEvaluationCriteria"]] = Field(None, description="Lista de subcriterios de evaluación") + +class DocumentEvaluationRubric(BaseModel): + """Modelo que representa una rúbrica de evaluación personalizada para análisis de documentos.""" + name: str = Field(..., description="Nombre de la rúbrica de evaluación") + description: str = Field(None, description="Descripción detallada de la rúbrica") + category: Optional[str] = Field( + None, + description="Contexto jerárquico de la categoría para especializar a la IA (ej. Audio > Education > English > B2)" + ) + criteria: List[DocumentEvaluationCriteria] = Field(None, description="Lista de criterios de evaluación específicos") + +@dataclass +class DocumentRequestFile: + """Modelo de solicitud para análisis de documentos.""" + file: UploadFile = File(..., description="Archivo de documento a procesar (ej. pdf, docx)") + provider: str = Form(..., description="Proveedor de IA a utilizar (ej. openai, google)") + model: str = Form(..., description="Modelo de IA a utilizar (ej. vision-1)") + rubric: UploadFile = File(..., description="Archivo JSON de evaluación") + +class StandardDocumentAnalysisResult(BaseModel): + """Modelo que representa el resultado estándar de un análisis de documentos para Qualidot.""" + status: str = Field(..., description="Estado del análisis (ej. 'success', 'error')") + original_filename: str = Field(..., description="Nombre original del archivo de documento procesado") + provider_used: str = Field(..., description="Proveedor de IA utilizado para el análisis (ej. 'OpenAI')") + model_used: str = Field(..., description="Modelo específico utilizado para el análisis (ej. 'vision-1')") + score: float = Field(None, description="Puntuación general asignada al documento después del análisis") + feedback: str = Field(None, description="Comentarios o retroalimentación generada por el modelo de IA sobre el documento") + detailed_criteria: List[DocumentEvaluationCriteria] = Field(None, description="Lista de criterios de evaluación detallados con sus respectivas puntuaciones y descripciones") + + +class StandardDocumentAnalysis(BaseModel): + """Modelo que representa el resultado estándar de un análisis de documentos para Qualidot.""" + score: float = Field(None, description="Puntuación general asignada al documento después del análisis") + feedback: str = Field(None, description="Comentarios o retroalimentación generada por el modelo de IA sobre la imagen") + detailed_criteria: List[DocumentEvaluationCriteria] = Field(None, description="Lista de criterios de evaluación detallados con sus respectivas puntuaciones y descripciones") + \ No newline at end of file diff --git a/app/services/document/__pycache__/evaluations_adapters.cpython-312.pyc b/app/services/document/__pycache__/evaluations_adapters.cpython-312.pyc new file mode 100644 index 0000000..840c5c0 Binary files /dev/null and b/app/services/document/__pycache__/evaluations_adapters.cpython-312.pyc differ diff --git a/app/services/document/__pycache__/prompt_builder.cpython-312.pyc b/app/services/document/__pycache__/prompt_builder.cpython-312.pyc new file mode 100644 index 0000000..59fe084 Binary files /dev/null and b/app/services/document/__pycache__/prompt_builder.cpython-312.pyc differ diff --git a/app/services/document/evaluations_adapters.py b/app/services/document/evaluations_adapters.py new file mode 100644 index 0000000..a6d4088 --- /dev/null +++ b/app/services/document/evaluations_adapters.py @@ -0,0 +1,162 @@ +""" +Gateway de IA de Qualidot - Módulo de Adaptadores de Evaluación de Imágenes + +Propósito: + Este módulo contiene funciones de adaptadores que permiten evaluar imágenes usando diferentes proveedores de IA. + Cada función de adaptador se encarga de interactuar con un proveedor específico (como OpenAI, AssemblyAI, Deepgram, etc.) + y de convertir la respuesta del proveedor al formato estándar de evaluación de imágenes de Qualidot. + +""" + +import json +import mimetypes +import mimetypes +import tempfile +import os +from fastapi import HTTPException +from matplotlib import image +from openai import OpenAI, AsyncOpenAI +import assemblyai as aai +from pyparsing.common import Any +from app.core.config import settings +from app.schemas.document_standard import DocumentRequestFile, StandardDocumentAnalysisResult +from app.core import config +from app.utilities.document_utilities import json_to_rubric, encode_document_from_bytes +from app.services.document.prompt_builder import build_document_evaluation_prompt +from anthropic import Anthropic + +from app.utilities.image_utilities import encode_image_from_bytes + +# Función de adaptador principal que infiere el proveedor y llama al adaptador específico +async def evaluate_document_with_provider(document_request: DocumentRequestFile) -> StandardDocumentAnalysisResult: + """ + Función de adaptador para evaluar documentos usando el proveedor de IA configurado. + """ + provider = document_request.provider.lower() + + content = await document_request.rubric.read() + rubric_dict = json.loads(content) + rubric = json_to_rubric(rubric_dict) + prompt = build_document_evaluation_prompt(rubric) + + match provider: + case "openai": + return await evaluate_with_openai(document_request, prompt) + case "claude": + return await evaluate_with_claude(document_request, prompt) + case "gemini": + return await evaluate_with_gemini(document_request, prompt) + case _: + raise ValueError(f"Proveedor de IA no soportado: {document_request.provider}") + +# Función de adaptador para evaluar documentos usando OpenAI +async def evaluate_with_openai(document_request: DocumentRequestFile, prompt: str) -> StandardDocumentAnalysisResult: + """ + Función de adaptador para evaluar documentos usando OpenAI. + """ + + client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + + document_bytes = await document_request.file.read() + + base64_document = encode_document_from_bytes(document_bytes) + media_type = document_request.file.content_type + + try: + response = await client.chat.completions.create( + model=document_request.model, + messages=[ + {"role": "user", "content": [ + {"type": "text", "text": prompt}, + { + "type": "file", + "file": { + "file_data": f"data:{media_type};base64,{base64_document}", + "filename": document_request.file.filename + } + } + ]} + ], + response_format={"type": "json_object"} + ) + + resultado = json.loads(response.choices[0].message.content) + + return StandardDocumentAnalysisResult( + status="success", + original_filename=document_request.file.filename, + provider_used="openai", + model_used=document_request.model, + **resultado + ) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error evaluando el documento: {str(e)}" + ) + +# Función de adaptador para evaluar documentos usando Claude +async def evaluate_with_claude(document_request: DocumentRequestFile, prompt: str) -> StandardDocumentAnalysisResult: + """ + Función de adaptador para evaluar documentos usando Claude. + """ + client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + + document_bytes = await document_request.file.read() + + base64_document = encode_document_from_bytes(document_bytes) + media_type = document_request.file.content_type + + if media_type not in ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]: + raise ValueError(f"Tipo de documento no soportado por Anthropic: {media_type}") + + try: + messages = [ + { + "role": "user", + "content": [ + { + "type": "document", + "source": { + "type": "base64", + "media_type": media_type, + "data": base64_document + }, + }, + {"type": "text", "text": prompt} + ], + } + ] + + response = client.messages.create( + model=document_request.model, + max_tokens=4096, + messages=messages, + ) + + json_string = response.content[0].text + parsed_data = json.loads(json_string) + + return StandardDocumentAnalysisResult( + status="success", + original_filename=document_request.file.filename, + provider_used="Claude", + model_used=document_request.model, + **parsed_data + ) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error evaluando el documento: {str(e)}" + ) + +# Función de adaptador para evaluar documentos usando Gemini +async def evaluate_with_gemini(document_request: DocumentRequestFile, prompt: str) -> StandardDocumentAnalysisResult: + """ + Función de adaptador para evaluar documentos usando Gemini. + (Plantilla para futuras implementaciones) + """ + # Aquí iría la implementación específica para Gemini + pass \ No newline at end of file diff --git a/app/services/document/prompt_builder.py b/app/services/document/prompt_builder.py new file mode 100644 index 0000000..7771593 --- /dev/null +++ b/app/services/document/prompt_builder.py @@ -0,0 +1,58 @@ +""" +Módulo encargado de construir el prompt estandarizado para la evaluación de imágenes, +inyectando la rúbrica, el contexto de especialidad y el esquema JSON esperado. +Este módulo es crucial para asegurar que los evaluadores expertos reciban instrucciones +claras y consistentes, y que sus respuestas se ajusten al formato requerido para su posterior +procesamiento y análisis. +""" + + +import json +from app.schemas.document_standard import DocumentEvaluationRubric, StandardDocumentAnalysis + +def build_document_evaluation_prompt(rubric: DocumentEvaluationRubric) -> str: + """ + Construye el prompt estandarizado inyectando la rúbrica, + el contexto de especialidad y el esquema JSON esperado. + """ + + # Extraemos el JSON de la rúbrica de forma limpia (ignorando nulos) + rubric_json = rubric.model_dump_json(exclude_none=True, indent=2) + + # Extraemos el esquema dinámico de salida basado en Pydantic + expected_output_schema = json.dumps(StandardDocumentAnalysis.model_json_schema(), indent=2) + + # Obtenemos el path de especialización + # Si por alguna razón viene vacío, le damos un rol genérico por defecto + specialization_path = rubric.category if rubric.category else "General Document Analysis" + + # Ensamblamos el prompt con f-strings + prompt = f""" +# ROLE +You are a highly specialized Expert Evaluator with deep domain expertise in the following area: **{specialization_path}**. Your objective is to perform a highly accurate, objective, and domain-calibrated analysis of the provided input based STRICTLY on your specialization and the provided evaluation rubric. + +# TASK +I will provide you with an input and a JSON object representing an `EvaluationRubric`. +Your task is to analyze the input and score it against every criterion and subcriterion defined in the rubric. You must justify your scores with objective feedback based on evidence. + +# DOMAIN CONTEXT +Specialization Path: {specialization_path} + +# RUBRIC +{rubric_json} + +# EVALUATION RULES +1. **Domain Calibration (CRITICAL):** Calibrate your expectations, strictness, and feedback entirely according to the **{specialization_path}** context. Do not evaluate using generalized standards; apply the specific standards expected at this exact level and category. +2. **Strict Adherence:** Evaluate ONLY the criteria and subcriteria listed in the rubric. Do not invent new metrics. +3. **Scoring:** Assign a numeric `score` to each criterion and subcriterion. The score must reflect how well the input meets the description of that specific metric. +4. **Objective Feedback:** Generate constructive, evidence-grounded `feedback` for the overall evaluation. Mention specific elements or patterns observed in the input that justify the scores within the context of the specialization. +5. **Subcriteria Handling:** If a criterion has `subcriteria`, evaluate and score each subcriterion individually. The parent criterion's `score` MUST be the exact mathematical **sum** of its subcriteria scores. +6. **Final Score Calculation:** The overall final `score` of the evaluation MUST be the exact mathematical **sum** of all the main criteria scores. + +# OUTPUT FORMAT +You MUST return your response EXCLUSIVELY as a raw, valid JSON object that strictly adheres to the following JSON Schema definition. Do NOT include markdown blocks (```json), explanations, or any text outside the JSON object. + +# EXPECTED JSON SCHEMA +{expected_output_schema} +""" + return prompt \ No newline at end of file diff --git a/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc b/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc index 1572b1c..fd9d069 100644 Binary files a/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc and b/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc differ diff --git a/app/services/image/__pycache__/prompt_builder.cpython-312.pyc b/app/services/image/__pycache__/prompt_builder.cpython-312.pyc index e2f87e0..65959f1 100644 Binary files a/app/services/image/__pycache__/prompt_builder.cpython-312.pyc and b/app/services/image/__pycache__/prompt_builder.cpython-312.pyc differ diff --git a/app/services/image/evaluations_adapters.py b/app/services/image/evaluations_adapters.py index 8abdcbf..d754f13 100644 --- a/app/services/image/evaluations_adapters.py +++ b/app/services/image/evaluations_adapters.py @@ -24,6 +24,12 @@ from app.core import config from app.utilities.image_utilities import json_to_rubric, encode_image_from_bytes from app.services.image.prompt_builder import build_image_evaluation_prompt from anthropic import Anthropic +import re + +# Importaciones de Clarifai +from clarifai_grpc.channel.clarifai_channel import ClarifaiChannel +from clarifai_grpc.grpc.api import resources_pb2, service_pb2, service_pb2_grpc +from clarifai_grpc.grpc.api.status import status_code_pb2 # Función de adaptador principal que infiere el proveedor y llama al adaptador específico async def evaluate_image_with_provider(image_request: ImageRequestFile) -> StandardImageAnalysisResult: @@ -91,62 +97,77 @@ async def evaluate_with_openai(image_request: ImageRequestFile, prompt: str) -> detail=f"Error evaluando la imagen: {str(e)}" ) -async def evaluate_with_clarifai(image_request: ImageRequestFile, rubric: ImageEvaluationRubric, prompt: str) -> StandardImageAnalysisResult: +async def evaluate_with_clarifai(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult: """ - Función de adaptador para evaluar imágenes usando Clarifai con un modelo Multimodal. + Función de adaptador para evaluar imágenes usando Clarifai. """ - try: - # 1. Obtener el token de configuración (PAT) - pat = settings.CLARIFAI_API_KEY - if not pat: - raise ValueError("La clave CLARIFAI_API_KEY no está configurada en el entorno.") - # 2. Obtener la URL del modelo enviada en la petición - model_url = image_request.model + + CLARIFAI_API_KEY = settings.CLARIFAI_API_KEY + if not CLARIFAI_API_KEY: + raise HTTPException( + status_code=500, + detail="No se encontró CLARIFAI_API_KEY en las variables de entorno" + ) - # Inicializar el modelo de Clarifai - model = Model(url=model_url, pat=pat) - - # 3. Leer los bytes de la imagen subida + USER_ID = "openai" + APP_ID = "chat-completion" + + try: image_bytes = await image_request.file.read() - if not image_bytes: - raise ValueError("El archivo de imagen recibido está vacío.") + # 2. Preparamos y ejecutamos la llamada a Clarifai (gRPC) + channel = ClarifaiChannel.get_grpc_channel() + stub = service_pb2_grpc.V2Stub(channel) + metadata = (('authorization', 'Key ' + CLARIFAI_API_KEY),) + userDataObject = resources_pb2.UserAppIDSet(user_id=USER_ID, app_id=APP_ID) - # 4. Preparar el input multimodal para Clarifai combinando la imagen y el prompt de evaluación - multimodal_input = Inputs.get_multimodal_input( - input_id="image_evaluation", - image_bytes=image_bytes, - raw_text=prompt + request = service_pb2.PostModelOutputsRequest( + user_app_id=userDataObject, + model_id=image_request.model, + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + image=resources_pb2.Image(base64=image_bytes), + text=resources_pb2.Text(raw=prompt) + ) + ) + ] ) - - # 5. Llamar a la API de Clarifai para evaluar la imagen - predict_response = model.predict([multimodal_input]) - # Extraer el texto crudo de la respuesta del modelo - raw_output = predict_response.outputs[0].data.text.raw - - # 6. Limpiar la respuesta y convertirla a JSON - # Los LLMs suelen devolver el JSON envuelto en bloques de markdown (```json ... ```) - clean_json = raw_output.replace("```json", "").replace("```", "").strip() + response = stub.PostModelOutputs(request, metadata=metadata) - # Convertir el string limpio a un diccionario de Python - parsed_data = json.loads(clean_json) + if response.status.code != status_code_pb2.SUCCESS: + raise Exception(f"Clarifai Error: {response.status.description}") + + # 3. Extraemos la respuesta cruda + raw_output = response.outputs[0].data.text.raw - # 7. Retornar el resultado validado contra tu esquema estándar de Pydantic - return StandardImageAnalysisResult(**parsed_data) - - except json.JSONDecodeError as e: - # Error específico si el modelo alucinó texto extra y no devolvió un JSON válido - raise HTTPException( - status_code=500, - detail=f"El modelo de Clarifai no devolvió un JSON válido. Error: {str(e)} | Respuesta cruda: {raw_output if 'raw_output' in locals() else 'N/A'}" + + + # 4. Limpiamos y parseamos el JSON devuelto + json_match = re.search(r'\{.*\}', raw_output, re.DOTALL) + + if json_match: + clean_json = json_match.group(0) + parsed_data = json.loads(clean_json) + else: + # Respaldo en caso de que el modelo devuelva texto plano en lugar de JSON + parsed_data = {"raw_response": raw_output} + + # 5. Retornamos el modelo estandarizado + return StandardImageAnalysisResult( + status="success", + original_filename=image_request.file.filename, + provider_used="Clarifai", + model_used=image_request.model, + **parsed_data ) + except Exception as e: - # Captura cualquier otro error (problemas de red, token inválido, URL incorrecta, etc.) raise HTTPException( - status_code=500, - detail=f"Error interno al evaluar con Clarifai: {str(e)}" + status_code=500, + detail=f"Error evaluando la imagen con Clarifai: {str(e)}" ) async def evaluate_with_claude(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult: diff --git a/app/services/image/prompt_builder.py b/app/services/image/prompt_builder.py index cddac68..1b62641 100644 --- a/app/services/image/prompt_builder.py +++ b/app/services/image/prompt_builder.py @@ -44,9 +44,10 @@ Specialization Path: {specialization_path} # EVALUATION RULES 1. **Domain Calibration (CRITICAL):** Calibrate your expectations, strictness, and feedback entirely according to the **{specialization_path}** context. Do not evaluate using generalized standards; apply the specific standards expected at this exact level and category. 2. **Strict Adherence:** Evaluate ONLY the criteria and subcriteria listed in the rubric. Do not invent new metrics. -3. **Scoring:** Assign a numeric `score` to each criterion and subcriterion. The score must reflect how well the input meets the description of that specific metric. +3. **Scoring:** Assign a numeric `score` to each criterion and subcriterion. The score must reflect how well the input meets the description of that specific metric. 4. **Objective Feedback:** Generate constructive, evidence-grounded `feedback` for the overall evaluation. Mention specific elements or patterns observed in the input that justify the scores within the context of the specialization. -5. **Subcriteria Handling:** If a criterion has `subcriteria`, evaluate each subcriterion individually. The parent criterion's score should be a logical aggregate (e.g., average) of its subcriteria scores. +5. **Subcriteria Handling:** If a criterion has `subcriteria`, evaluate and score each subcriterion individually. The parent criterion's `score` MUST be the exact mathematical **sum** of its subcriteria scores. +6. **Final Score Calculation:** The overall final `score` of the evaluation MUST be the exact mathematical **sum** of all the main criteria scores. # OUTPUT FORMAT You MUST return your response EXCLUSIVELY as a raw, valid JSON object that strictly adheres to the following JSON Schema definition. Do NOT include markdown blocks (```json), explanations, or any text outside the JSON object. diff --git a/app/tests/rubricas/documentos/text.json b/app/tests/rubricas/documentos/text.json new file mode 100644 index 0000000..4b46b3d --- /dev/null +++ b/app/tests/rubricas/documentos/text.json @@ -0,0 +1,171 @@ +{ + "rubric": { + "id": "e7b9c1d2-4f5a-6b7c-8d9e-0a1b2c3d4e5f", + "name": "Academic Essay Evaluation Rubric", + "description": "Rúbrica detallada para evaluar la redacción de ensayos. Mide la calidad de la argumentación, la cohesión estructural y el dominio de la gramática y el vocabulario.", + "user_id": "112b3fda-e380-4919-9f9d-ff941a3b1938", + "status": true, + "visibility": "private", + "verified": true + }, + "category": { + "id": 14, + "name": "Essay Evaluation", + "status": true, + "hierarchy": [ + { + "id": 11, + "name": "Education", + "level": 0, + "parent_id": null + }, + { + "id": 12, + "name": "Language Arts", + "level": 1, + "parent_id": 11 + }, + { + "id": 13, + "name": "Academic Writing", + "level": 2, + "parent_id": 12 + }, + { + "id": 14, + "name": "Essay Evaluation", + "level": 3, + "parent_id": 13 + } + ] + }, + "criteria": [ + { + "id": 100, + "name": "Content & Argumentation", + "description": "Evalúa la profundidad del contenido, la claridad de la tesis y la solidez de los argumentos presentados.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": null, + "sub_criteria": [ + { + "id": 101, + "name": "Thesis Clarity", + "description": "El ensayo presenta una tesis central clara, específica y debatible en la introducción.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 100 + }, + { + "id": 102, + "name": "Evidence & Support", + "description": "Los argumentos están respaldados por evidencia sólida, ejemplos relevantes o citas bien integradas.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 100 + }, + { + "id": 103, + "name": "Critical Thinking", + "description": "El texto demuestra un análisis profundo del tema, evitando generalidades superficiales o afirmaciones sin fundamento.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 100 + } + ] + }, + { + "id": 200, + "name": "Structure & Organization", + "description": "Evalúa el flujo lógico del texto, la estructuración de párrafos y el uso de conectores.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": null, + "sub_criteria": [ + { + "id": 201, + "name": "Logical Flow", + "description": "Las ideas progresan de manera lógica. La introducción, el desarrollo y la conclusión están claramente definidos y equilibrados.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 200 + }, + { + "id": 202, + "name": "Paragraph Cohesion", + "description": "Cada párrafo se centra en una idea principal única. Las transiciones entre párrafos y oraciones son fluidas y naturales.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 200 + } + ] + }, + { + "id": 300, + "name": "Language Mechanics & Style", + "description": "Evalúa las convenciones ortográficas, gramaticales y la riqueza del vocabulario.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": null, + "sub_criteria": [ + { + "id": 301, + "name": "Grammar & Syntax", + "description": "Las oraciones están construidas correctamente. No hay errores de concordancia, tiempos verbales o sintaxis.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 300 + }, + { + "id": 302, + "name": "Spelling & Punctuation", + "description": "Uso impecable de la ortografía y los signos de puntuación (comas, puntos, comillas, etc.).", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 300 + }, + { + "id": 303, + "name": "Vocabulary & Tone", + "description": "El vocabulario es variado, preciso y adecuado para un contexto académico. El tono se mantiene formal y objetivo.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 300 + } + ] + } + ], + "references": [ + { + "id": 1, + "title": "APA Style Guidelines for Academic Writing", + "url": "https://apastyle.apa.org/style-grammar-guidelines", + "reference_type": "official_doc", + "is_primary": true, + "description": "Guía oficial de estilo APA para redacción, gramática y estructuración de textos académicos.", + "status": true + } + ] +} \ No newline at end of file diff --git a/app/tests/id.json b/app/tests/rubricas/imagenes/id.json similarity index 100% rename from app/tests/id.json rename to app/tests/rubricas/imagenes/id.json diff --git a/app/utilities/__pycache__/document_utilities.cpython-312.pyc b/app/utilities/__pycache__/document_utilities.cpython-312.pyc new file mode 100644 index 0000000..9aa3a31 Binary files /dev/null and b/app/utilities/__pycache__/document_utilities.cpython-312.pyc differ diff --git a/app/utilities/document_utilities.py b/app/utilities/document_utilities.py new file mode 100644 index 0000000..b1d61ec --- /dev/null +++ b/app/utilities/document_utilities.py @@ -0,0 +1,51 @@ +from app.schemas.document_standard import DocumentEvaluationRubric, StandardDocumentAnalysis, DocumentEvaluationCriteria + +def encode_document_from_bytes(document_bytes: bytes) -> str: + """ + Codifica un documento a base64 a partir de sus bytes. + """ + import base64 + return base64.b64encode(document_bytes).decode('utf-8') + +def json_to_rubric(json_data: dict) -> DocumentEvaluationRubric: + """Convierte un diccionario JSON en un objeto DocumentEvaluationRubric mapeando los campos correctos.""" + + def parse_criteria(criteria_list: list) -> list: + parsed_criteria = [] + for item in criteria_list: + # Convertir el valor a float si existe + score_value = float(item["value"]) if item.get("value") is not None else None + description = str(item["description"]) if item.get("description") is not None else None + + # Procesar subcriterios recursivamente si existen + sub_raw = item.get("sub_criteria") + sub_parsed = parse_criteria(sub_raw) if sub_raw else None + + parsed_criteria.append(DocumentEvaluationCriteria( + name=item["name"], + description=description, + score=score_value, + subcriteria=sub_parsed + )) + return parsed_criteria + + # Jerarquía de la categoría + category_info = json_data.get("category", {}) + hierarchy_list = category_info.get("hierarchy", []) + + # Ordenamos por nivel para asegurar la lógica: Nivel 0 -> Nivel 1 -> Nivel 2 + sorted_hierarchy = sorted(hierarchy_list, key=lambda x: x.get("level", 0)) + + # Unimos los nombres con ' > ' + specialization_path = " > ".join([item["name"] for item in sorted_hierarchy]) if sorted_hierarchy else None + + # Extraer la información principal de la rúbrica + rubric_info = json_data.get("rubric", {}) + + return DocumentEvaluationRubric( + name=rubric_info.get("name", "Sin nombre"), + description=str(rubric_info["description"]) if rubric_info.get("description") is not None else None, + category=specialization_path, + criteria=parse_criteria(json_data.get("criteria", [])) + ) + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e220608..6bd94e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,29 +1,143 @@ -# Dependencias principales -annotated-doc==0.0.4 -annotated-types==0.7.0 -anyio==4.11.0 -fastapi[standard]==0.121.3 -idna==3.11 -pydantic==2.12.4 -pydantic_core==2.41.5 -sniffio==1.3.1 -starlette==0.50.0 -typing-inspection==0.4.2 -typing_extensions==4.15.0 -uvicorn[standard] -python-multipart -python-dotenv +# ========================================== +# Web & API Framework (FastAPI) +# ========================================== +fastapi==0.121.3 # Framework principal para crear la API +fastapi-cli==0.0.24 # Interfaz de línea de comandos para FastAPI +fastapi-cloud-cli==0.15.0 # Herramientas de despliegue para FastAPI +starlette==0.50.0 # Toolkit ASGI base de FastAPI +uvicorn==0.41.0 # Servidor ASGI para ejecutar la aplicación +uvloop==0.22.1 # Ciclo de eventos rápido para Uvicorn +anyio==4.11.0 # Soporte asíncrono para concurrencia +sniffio==1.3.1 # Detección de la librería asíncrona en uso +websockets==16.0 # Soporte para conexiones WebSockets +watchfiles==1.1.1 # Recarga automática del servidor al detectar cambios +python-multipart==0.0.22 # Manejo de datos de formularios y subida de archivos -# Procesamiento de audio -noisereduce -librosa -soundfile -praat-parselmouth -numpy +# ========================================== +# Validación de Datos & Tipado +# ========================================== +pydantic==2.12.4 # Validación de datos y gestión de esquemas +pydantic_core==2.41.5 # Núcleo en Rust para Pydantic (rendimiento) +pydantic-extra-types==2.11.0 # Tipos adicionales para Pydantic +pydantic-settings==2.13.1 # Manejo avanzado de variables de entorno +annotated-types==0.7.0 # Metadatos para el tipado de variables +typing_extensions==4.15.0 # Funciones de tipado para versiones antiguas de Python +typing-inspection==0.4.2 # Inspección de tipos en tiempo de ejecución +email-validator==2.3.0 # Validación de correos electrónicos -# Modelos de IA e integraciones -openai -langchain -langchain-openai -assemblyai -anthropic \ No newline at end of file +# ========================================== +# Modelos de IA, LLMs & Agentes (LangChain) +# ========================================== +openai==2.28.0 # Cliente oficial para la API de OpenAI +langchain==1.2.12 # Framework para aplicaciones con LLMs +langchain-core==1.2.19 # Componentes y abstracciones base de LangChain +langchain-openai==1.1.11 # Integración específica de OpenAI para LangChain +langgraph==1.1.2 # Creación de agentes y flujos cíclicos con LLMs +langgraph-checkpoint==4.0.1 # Gestión de estados y memoria para LangGraph +langgraph-prebuilt==1.0.8 # Componentes preconstruidos para LangGraph +langgraph-sdk==0.3.11 # SDK oficial de LangGraph +langsmith==0.7.17 # Monitoreo, trazas y depuración para LangChain +tiktoken==0.12.0 # Tokenizador rápido usado por OpenAI + +# ========================================== +# Procesamiento de Audio & Voz (Speech-to-Text) +# ========================================== +assemblyai==0.56.0 # SDK de AssemblyAI para transcripción de audio +deepgram-sdk==6.0.1 # SDK de Deepgram para transcripción de audio +librosa==0.11.0 # Análisis de música y señales de audio +noisereduce==3.0.3 # Algoritmos para reducción de ruido en audio +praat-parselmouth==0.4.7 # Interfaz de Python para Praat (análisis fonético) +soundfile==0.13.1 # Lectura y escritura de archivos de audio +audioread==3.1.0 # Decodificación de audio multiplataforma +soxr==1.0.0 # Conversión de frecuencia de muestreo (resampling) de alta calidad + +# ========================================== +# Ciencia de Datos, Matemáticas & Machine Learning +# ========================================== +numpy==2.4.3 # Computación numérica y manejo de arreglos (matrices) +scipy==1.17.1 # Funciones matemáticas, científicas y de ingeniería +scikit-learn==1.8.0 # Herramientas de Machine Learning y análisis de datos +numba==0.64.0 # Compilador JIT (Just-In-Time) para acelerar código matemático +llvmlite==0.46.0 # Motor subyacente para compilar con Numba +joblib==1.5.3 # Procesamiento en paralelo y caché (usado por scikit-learn) +threadpoolctl==3.6.0 # Control de hilos en librerías nativas (C/C++) + +# ========================================== +# Redes & Clientes HTTP +# ========================================== +requests==2.32.5 # Cliente HTTP síncrono estándar +requests-toolbelt==1.0.0 # Utilidades adicionales para la librería requests +httpx==0.28.1 # Cliente HTTP asíncrono (alternativa moderna a requests) +httpcore==1.0.9 # Motor subyacente de red para HTTPX +httptools==0.7.1 # Analizador (parser) de peticiones HTTP ultrarrápido +h11==0.16.0 # Implementación pura de HTTP/1.1 +urllib3==2.6.3 # Cliente HTTP base con gestión de conexiones y reintentos +certifi==2026.2.25 # Colección de certificados SSL/TLS raíz +idna==3.11 # Soporte para nombres de dominio internacionalizados +dnspython==2.8.0 # Herramientas para consultas y manipulación de DNS + +# ========================================== +# Gráficos & Visualización +# ========================================== +matplotlib==3.10.8 # Creación de gráficas y visualizaciones de datos +contourpy==1.3.3 # Cálculo de contornos 2D (dependencia de matplotlib) +cycler==0.12.1 # Creación de iteradores complejos (dependencia de matplotlib) +fonttools==4.62.1 # Manipulación de fuentes tipográficas +kiwisolver==1.5.0 # Solucionador matemático rápido (dependencia de matplotlib) +pillow==12.1.1 # Procesamiento y manipulación de imágenes (PIL) +pyparsing==3.3.2 # Herramienta para crear analizadores de texto sintácticos + +# ========================================== +# CLI (Terminal) & Utilidades de Salida +# ========================================== +click==8.3.1 # Creación rápida de interfaces de línea de comandos (CLI) +typer==0.24.1 # Creación de CLIs basado en Pydantic y tipado +rich==14.3.3 # Texto enriquecido, tablas y colores en la terminal +rich-toolkit==0.19.7 # Componentes adicionales para Rich +tqdm==4.67.3 # Barras de progreso visuales en consola +shellingham==1.5.4 # Herramienta para detectar qué shell se está utilizando + +# ========================================== +# Serialización, Parsing & Utilidades Generales +# ========================================== +python-dotenv==1.2.2 # Carga de variables de entorno desde archivos .env +orjson==3.11.7 # Analizador (parser) de JSON ultrarrápido +jiter==0.13.0 # Parser de JSON eficiente (usado internamente por Pydantic) +ormsgpack==1.12.2 # Serialización de datos en formato MessagePack (rápida) +msgpack==1.1.2 # Serialización de datos en formato MessagePack (estándar) +PyYAML==6.0.3 # Procesamiento de archivos YAML +jsonpatch==1.33 # Aplicación de parches a documentos JSON +jsonpointer==3.0.0 # Identificación de nodos dentro de un JSON +python-dateutil==2.9.0.post0 # Extensiones robustas para el manejo de fechas (datetime) +regex==2026.2.28 # Motor de expresiones regulares alternativo y más potente +uuid_utils==0.14.1 # Utilidades para la generación rápida de UUIDs +charset-normalizer==3.4.5 # Detección automática de codificación de texto +six==1.17.0 # Librería de compatibilidad entre Python 2 y 3 +tenacity==9.1.4 # Reintentos automáticos para código propenso a fallos +decorator==5.2.1 # Simplificación en la creación de decoradores +xxhash==3.6.0 # Algoritmo de hash no criptográfico extremadamente rápido +zstandard==0.25.0 # Compresión de datos rápida (algoritmo zstd) +packaging==26.0 # Manejo y parseo de versiones de paquetes de Python +platformdirs==4.9.4 # Identificación de rutas de directorios estándar del SO +pooch==1.9.0 # Descarga y almacenamiento en caché de archivos de datos +lazy-loader==0.5 # Carga perezosa (lazy) de módulos pesados +cffi==2.0.0 # Interfaz para llamar código en C desde Python (FFI) +pycparser==3.0 # Analizador sintáctico de lenguaje C en Python +rignore==0.7.6 # Herramienta para analizar archivos ignorados (ej. .gitignore) +annotated-doc==0.0.4 # Utilidades para extraer documentación de tipos anotados +fastar==0.8.0 # Utilidad secundaria (generalmente vinculada al framework web) + +# ========================================== +# Plantillas & Procesamiento de Markdown +# ========================================== +Jinja2==3.1.6 # Motor de plantillas (usado comúnmente para renderizar HTML) +MarkupSafe==3.0.3 # Escapado seguro de strings para evitar inyecciones en HTML +markdown-it-py==4.0.0 # Analizador y renderizador de Markdown extensible +mdurl==0.1.2 # Utilidad para parsear URLs dentro de Markdown +Pygments==2.19.2 # Resaltador de sintaxis genérico para código fuente + +# ========================================== +# Monitoreo & Diagnóstico del Sistema +# ========================================== +sentry-sdk==2.54.0 # Integración con Sentry para monitoreo y rastreo de errores +distro==1.9.0 # Extracción de información específica del sistema operativo Linux \ No newline at end of file