forked from ericorps/ia-microservice
Versión 1.0
This commit is contained in:
Binary file not shown.
BIN
app/services/document/__pycache__/prompt_builder.cpython-312.pyc
Normal file
BIN
app/services/document/__pycache__/prompt_builder.cpython-312.pyc
Normal file
Binary file not shown.
162
app/services/document/evaluations_adapters.py
Normal file
162
app/services/document/evaluations_adapters.py
Normal 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
|
||||
58
app/services/document/prompt_builder.py
Normal file
58
app/services/document/prompt_builder.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user