Versión 1.0

This commit is contained in:
lansan69
2026-03-31 20:00:04 -06:00
parent bc513ad2c4
commit e882442f8b
30 changed files with 778 additions and 89 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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.