modelo / app.py
alexacido's picture
Upload 11 files
479d4dc verified
# ===================== app.py (BLOQUE 1/4) =====================
# Consumertec LLM Backend (OpenAI + FastAPI)
#
# - Frontend: HTML (dentro del Space) => static/index.html
# - Backend: Hugging Face Space
# - Motor: OpenAI (gpt-4.1-mini por defecto)
#
# CARACTERÍSTICAS:
# ✔ Usa el prompt de la sección 3 (question)
# ✔ Usa el resumen numérico de la sección 5 (numeric_summary)
# ✔ NO inventa cifras
# ✔ Menciona productos explícitamente
# ✔ Respuesta larga (configurable por MAX_NEW_TOKENS)
# ✔ Estructura fija con títulos subrayados
# ============================================================
import os
import traceback
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from openai import OpenAI
import uvicorn
# ------------------------------------------------------------
# CONFIGURACIÓN GENERAL
# ------------------------------------------------------------
MODEL_NAME = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")
MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "600"))
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
print("===== Inicio Consumertec LLM Backend (OpenAI) =====")
print("Modelo OpenAI:", MODEL_NAME)
print("Max tokens salida:", MAX_NEW_TOKENS)
print("API Key detectada:", bool(OPENAI_API_KEY))
if not OPENAI_API_KEY:
print("❌ ERROR: OPENAI_API_KEY no configurada en Secrets del Space.")
client = OpenAI(api_key=OPENAI_API_KEY)
# ------------------------------------------------------------
# FastAPI APP
# ------------------------------------------------------------
app = FastAPI(
title="Explorador CSV Consumertec",
description="Explorador CSV Consumertec (Sección 5 determinista + Sección 6 texto continuo)",
version="2.0.0"
)
# CORS (si también sirves HTML desde /static, esto no estorba)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
# ------------------------------------------------------------
# SERVIR FRONTEND HTML (static/index.html)
# - "/" y "/ui" muestran el HTML
# - "/static/..." sirve recursos estáticos si los agregas luego
# ------------------------------------------------------------
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def root():
return FileResponse("static/index.html")
@app.get("/ui")
def ui():
return FileResponse("static/index.html")
# ------------------------------------------------------------
# ESQUEMA DE ENTRADA
# ------------------------------------------------------------
class GenerateRequest(BaseModel):
mode: str
question: str
numeric_summary: str | None = None
# ===================== app.py (BLOQUE 2/4) =====================
# ------------------------------------------------------------
# CONTEXTO DE DOMINIO (GENERAL)
# ------------------------------------------------------------
DOMAIN_CONTEXT = """
Eres un analista técnico senior especializado en pruebas de consumo,
detergencia, blancura y evaluación comparativa de productos en Consumertec.
""".strip()
# ------------------------------------------------------------
# CONTEXTO DE DOMINIO (CONDICIONAL) — RESPONSE
# Úsalo SOLO cuando el modo lo requiera (p.ej. "comparaciones" o "response_rel")
# ------------------------------------------------------------
RESPONSE_CONTEXT = """
La columna RESPONSE codifica el resultado comparativo ordinal entre PRODUCT_TEST y PRODUCT_BENCH para una combinación experimental específica (por ejemplo: MODALITY, SCENARIOS, WASH, PROCESS, MONITOR, THRESHOLD, etc.). No representa una magnitud continua ni una probabilidad directa, sino una clasificación discreta del desenlace relativo del producto evaluado frente al producto de referencia.
Los valores de RESPONSE están ordenados conceptualmente desde el resultado más favorable para PRODUCT_TEST hasta el más desfavorable, con una distinción explícita entre diferencias claras y tendencias (diferencias leves o cercanas al umbral de decisión):
- 01.SUPERIOR: PRODUCT_TEST es claramente superior a PRODUCT_BENCH bajo la condición evaluada. Resultado favorable fuerte.
- 02.SUPERIOR_TREND: PRODUCT_TEST muestra una tendencia a ser superior, indicando una ventaja leve o cercana al criterio de superioridad.
- 03.PARITY: No se detecta una diferencia relevante entre PRODUCT_TEST y PRODUCT_BENCH. Equivalencia práctica.
- 04.INFERIOR_TREND: PRODUCT_TEST muestra una tendencia a ser inferior, indicando una desventaja leve o cercana al criterio de inferioridad.
- 05.INFERIOR: PRODUCT_TEST es claramente inferior a PRODUCT_BENCH. Resultado desfavorable fuerte.
Desde el punto de vista analítico, RESPONSE es una variable ordinal, no intervalar: las distancias entre categorías no son cuantitativas ni necesariamente simétricas. Por tanto, no debe tratarse como un valor numérico continuo ni promediarse directamente.
En análisis de desempeño, suele definirse un subconjunto operativo de resultados favorables agrupando {01.SUPERIOR, 02.SUPERIOR_TREND}, mientras que {04.INFERIOR_TREND, 05.INFERIOR} se interpretan como desfavorables, y 03.PARITY actúa como estado neutro. Esta definición es convencional y depende del objetivo del análisis.
La distribución de RESPONSE suele estar concentrada en PARITY; por ello, pequeñas variaciones en el conteo de resultados favorables pueden producir diferencias relativas marcadas en tasas. Los resultados extremos (01 y 05) deben interpretarse como eventos raros. Las tasas y z-scores derivados deben leerse como indicadores comparativos, no como probabilidades absolutas.
Finalmente, RESPONSE no implica causalidad por sí misma: su lectura debe contextualizarse junto con las variables experimentales asociadas, ya que estas pueden actuar como efectos de confusión que inflan o deprimen la frecuencia observada de resultados favorables o desfavorables.
""".strip()
# ------------------------------------------------------------
# CONTEXTO DE DOMINIO (COMPARACIONES / OUTPUT_MAP_COMPARISONS.csv)
# ------------------------------------------------------------
COMPARISONS_CONTEXT = """
El archivo OUTPUT_MAP_COMPARISONS.csv contiene comparaciones de desempeño entre productos, evaluadas en distintas condiciones experimentales. Cada fila compara un valor de la columna PRODUCT_TEST con un valor de la columna PRODUCT_BENCH, bajo una combinación específica de las variables MODALITY, SCENARIOS, WASH, PROCESS, MONITOR, INDEX y THRESHOLD.
La columna MODALITY indica el tipo de blanco visualizado, siendo BLUISH un blanco con tonalidad azulada y STANDARD un blanco neutro. La variable SCENARIOS representa diferentes entornos visuales en los que se realizó la evaluación. WASH indica la cantidad de lavados aplicados y PROCESS se refiere al tipo de proceso de lavado.
MONITOR describe el tipo de tela utilizada como referencia blanca, incluyendo categorías como a.1.DSPCG para blanco sucio de poliéster algodón, b.1.DSCCG para blanco sucio de algodón, c.1.PPC para blanco original de poliéster algodón, d.1.PCC para blanco original de algodón, e.1.PPP para blanco original de poliéster, f.1.DPCG para blanco percudido de poliéster algodón, g.1.DCCG para blanco percudido de algodón y h.1.DPPG para blanco percudido de poliéster.
La columna INDEX refleja la dimensión perceptual de blancura que se evalúa: 1.2.WI_STw corresponde a la blancura estándar, 2.2.WI_GEw a la blancura verdosa, 3.2.WI_VOw a la blancura violácea y 4.2.WI_LGw a la blancura luminosa.
THRESHOLD señala el umbral o nivel de sensibilidad utilizado para distinguir diferencias entre productos.
El resultado de cada comparación se encuentra en la columna RESPONSE, que muestra si PRODUCT_TEST tuvo peor, igual o mejor desempeño que PRODUCT_BENCH. Estos valores se ordenan desde el menor al mayor desempeño de la siguiente manera: 05.INFERIOR, 04.INFERIOR_TREND, 03.PARITY, 02.SUPERIOR_TREND y 01.SUPERIOR.
Este archivo está estructurado para facilitar el análisis comparativo entre productos en función de la percepción visual de blancura bajo distintos contextos experimentales.
""".strip()
# ------------------------------------------------------------
# CONTEXTO DE DOMINIO (BLANCURA / OUTPUT_WHITES_WI.csv)
# ------------------------------------------------------------
WHITENESS_CONTEXT = """
El archivo OUTPUT_WHITES_WI.csv contiene mediciones de blancura para distintos productos, especificados en la columna “PRODUCT”. Estas mediciones están representadas en cuatro columnas diferentes: “1.2.WI_STw”, que indica la percepción de blancura estándar; “2.2.WI_GEw”, que representa la percepción de blancura con tonalidad verdosa; “3.2.WI_VOw”, que refleja la percepción de blancura con tonalidad violácea; y “4.2.WI_LGw”, que corresponde a la percepción de blancura luminosa.
Los valores registrados en estas columnas dependen de las condiciones bajo las cuales se evaluaron los productos, las cuales se definen a través de las columnas “MODALITY”, “SCENARIOS”, “WASH”, “PROCESS” y “MONITOR”. La columna “REPLICA” indica las repeticiones de cada evaluación para asegurar la consistencia de los datos.
La columna “SCENARIOS” hace referencia a diferentes escenarios visuales en los que se evaluó la blancura, mientras que la columna “WASH” registra el número de lavados aplicados a los productos. La columna “PROCESS” describe los diferentes procesos de lavado utilizados en cada caso.
Finalmente, la columna “MONITOR” identifica el tipo de tela blanca utilizada como referencia para la percepción visual de la blancura, siendo esta una variable clave en el análisis, ya que influye directamente en cómo se perciben las diferencias entre productos.
Los monitores se codifican de la siguiente manera: “a.1.DSPCG” corresponde a un monitor blanco sucio de poliéster-algodón, “b.1.DSCCG” a un monitor blanco sucio de algodón, “c.1.PPC” a un monitor blanco original de poliéster-algodón, “d.1.PCC” a un monitor blanco original de algodón, “e.1.PPP” a un monitor blanco original de poliéster, “f.1.DPCG” a un monitor blanco percudido de poliéster-algodón, “g.1.DCCG” a un monitor blanco percudido de algodón, y “h.1.DPPG” a un monitor blanco percudido de poliéster.
Este conjunto de datos permite analizar el desempeño de blancura de los productos en función de múltiples combinaciones de condiciones, facilitando comparaciones entre productos y evaluaciones específicas del efecto que tienen los diferentes escenarios visuales, procesos de lavado y tipos de monitor sobre la percepción de la blancura.
""".strip()
# ===================== app.py (BLOQUE 3/4) =====================
# ------------------------------------------------------------
# MENSAJES AL MODELO
# ------------------------------------------------------------
SYSTEM_MESSAGE = f"""
{DOMAIN_CONTEXT}
REGLAS ABSOLUTAS:
- Respondes SIEMPRE en español.
- Tono técnico, analítico y sobrio.
- No utilices lenguaje comercial ni promocional.
- No inventes cifras ni porcentajes.
- Solo puedes usar números que aparezcan explícitamente en la evidencia numérica.
- No menciones CSV, columnas, archivos, tablas ni pasos de procedimiento.
- PROHIBIDO usar listas, viñetas, numeraciones o tablas. Todo debe ser texto continuo.
- Debes mencionar explícitamente los productos (códigos tipo 00_000) que aparezcan en la evidencia.
FORMATO OBLIGATORIO DE SALIDA (TEXTO PLANO):
SÍNTESIS EJECUTIVA
==================
INTERPRETACIÓN TÉCNICA
======================
ESTILO:
- Empieza con una lectura general y luego entra en productos.
- Puedes declarar superioridad, empate o resultados mixtos si la evidencia lo soporta.
""".strip()
def build_user_content(mode: str, question: str, numeric_summary: str | None) -> str:
"""
Construye el mensaje de usuario que verá el modelo.
Usa el resumen numérico como EVIDENCIA factual y el prompt como guía analítica.
Además, inyecta contexto de dominio específico según el modo.
"""
evidencia = (numeric_summary or "").strip()
if not evidencia:
evidencia = "No se proporcionó evidencia numérica."
mode_norm = (mode or "").strip().lower()
contexto_especifico = ""
if mode_norm == "whiteness":
contexto_especifico = f"""
CONTEXTO ESPECÍFICO DEL DATASET (BLANCURA)
{WHITENESS_CONTEXT}
""".strip()
elif mode_norm == "comparaciones":
contexto_especifico = f"""
CONTEXTO ESPECÍFICO DEL DATASET (COMPARACIONES)
{COMPARISONS_CONTEXT}
""".strip()
elif mode_norm == "response_rel":
contexto_especifico = f"""
CONTEXTO ESPECÍFICO DE LA COLUMNA RESPONSE (ORDINAL)
{RESPONSE_CONTEXT}
""".strip()
# Inyección adicional del contexto RESPONSE cuando corresponde
if mode_norm in ("comparaciones", "response_rel"):
contexto_especifico = f"""
{contexto_especifico}
CONTEXTO DE RESPUESTA (RESPONSE)
{RESPONSE_CONTEXT}
""".strip()
texto = f"""
MODO DE ANÁLISIS
{mode}
{contexto_especifico}
EVIDENCIA NUMÉRICA (BASE FACTUAL)
La siguiente evidencia numérica es verdadera.
Regla: NO la repitas completa; úsala solo como soporte (citas puntuales) para tu análisis.
{evidencia}
PROMPT DE ANÁLISIS DEL USUARIO
Desarrolla el análisis solicitado a partir de la evidencia numérica anterior:
{question}
INSTRUCCIONES FINALES:
- Menciona explícitamente los nombres/códigos de los productos que aparezcan en la evidencia.
- Indica qué productos destacan y cuáles quedan por detrás, según la evidencia.
- Usa solo cifras presentes en la evidencia (sin recalcular, extrapolar ni reimprimir tablas).
- Respeta estrictamente el formato de secciones con títulos subrayados.
"""
return texto.strip()
# ===================== app.py (BLOQUE 4/4) =====================
# ------------------------------------------------------------
# ENDPOINTS
# ------------------------------------------------------------
@app.get("/status")
def status():
return {
"status": "ok",
"message": "Backend Consumertec LLM (OpenAI) activo.",
"model": MODEL_NAME,
"max_new_tokens": MAX_NEW_TOKENS,
"has_api_key": bool(OPENAI_API_KEY)
}
@app.post("/generate_answer")
def generate_answer(payload: GenerateRequest):
try:
if not OPENAI_API_KEY:
raise HTTPException(
status_code=500,
detail="OPENAI_API_KEY no configurada en Secrets."
)
user_content = build_user_content(
payload.mode,
payload.question,
payload.numeric_summary
)
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": SYSTEM_MESSAGE},
{"role": "user", "content": user_content},
],
max_completion_tokens=MAX_NEW_TOKENS,
temperature=0.65,
top_p=0.9,
)
answer = (
completion.choices[0].message.content.strip()
if completion.choices
and completion.choices[0].message
and completion.choices[0].message.content
else ""
)
if not answer:
answer = (
"SÍNTESIS EJECUTIVA\n"
"==================\n"
"El análisis no pudo generarse correctamente con los datos actuales.\n\n"
"INTERPRETACIÓN TÉCNICA\n"
"======================\n"
"No se obtuvo contenido suficiente del modelo para redactar una interpretación técnica defendible."
)
return {
"answer": answer,
"mode": payload.mode
}
except HTTPException:
raise
except Exception as e:
traceback.print_exc()
raise HTTPException(
status_code=500,
detail=f"Error interno del backend: {e}"
)
# ------------------------------------------------------------
# ARRANQUE LOCAL (solo pruebas, HF lo ignora)
# ------------------------------------------------------------
if __name__ == "__main__":
uvicorn.run(
app,
host="0.0.0.0",
port=7860
)