Laundry / app.py
alexacido's picture
Update app.py
469607a verified
# ============================================================
# Laundry Space - Programa para enviar csv (UNIVERSAL)
#
# - UI: GET / -> página con selector de destino + botones
# - Status: GET /status -> healthcheck + config
# - Export: GET /export_csv -> devuelve CSV desde disco (o sample)
# - Push: POST /send_csv -> envía CSV al destino seleccionado (/ingest_csv)
#
# Variables:
# CSV_FILE_PATH
# DEFAULT_MODE
# LLM_INGEST_URL_OPENAI
# LLM_INGEST_URL_GROQ
# LLM_INGEST_URL (fallback)
# INGEST_TOKEN (opcional, header X-INGEST-TOKEN)
# ============================================================
import os
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response, FileResponse, HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
import httpx
from pydantic import BaseModel
app = FastAPI(title="Programa para enviar csv", version="3.0.3")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
# ------------------------------------------------------------
# CONFIG
# ------------------------------------------------------------
CSV_FILE_PATH = (os.getenv("CSV_FILE_PATH", "/data/OUTPUT_MAP_COMPARISONS.csv") or "").strip()
DEFAULT_MODE = (os.getenv("DEFAULT_MODE", "comparaciones") or "comparaciones").strip()
LLM_INGEST_URL_OPENAI = (os.getenv("LLM_INGEST_URL_OPENAI", "") or "").strip()
LLM_INGEST_URL_GROQ = (os.getenv("LLM_INGEST_URL_GROQ", "") or "").strip()
LLM_INGEST_URL = (os.getenv("LLM_INGEST_URL", "") or "").strip()
INGEST_TOKEN = (os.getenv("INGEST_TOKEN", "") or "").strip()
def _sample_csv() -> str:
return (
"PRODUCT_TEST,PRODUCT_BENCH,RESPONSE,MODALITY,SCENARIOS,WASH,PROCESS,MONITOR,INDEX,THRESHOLD\n"
"P1,P0,01.SUPERIOR,STANDARD,S1,1,NORMAL,a.1.DSPCG,1.2.WI_STw,T1\n"
"P2,P0,03.PARITY,BLUISH,S2,1,NORMAL,b.1.DSCCG,2.2.WI_GEw,T1\n"
)
def _resolve_dest(dest_key: str, custom_url: str) -> str:
dest_key = (dest_key or "").strip().lower()
custom_url = (custom_url or "").strip()
if dest_key == "openai":
return LLM_INGEST_URL_OPENAI or LLM_INGEST_URL
if dest_key == "groq":
return LLM_INGEST_URL_GROQ or LLM_INGEST_URL
if dest_key == "custom":
return custom_url or LLM_INGEST_URL
return LLM_INGEST_URL
def _ui_from_ingest(ingest_url: str) -> str:
"""
Convierte:
https://xxxx.hf.space/ingest_csv -> https://xxxx.hf.space/
"""
u = (ingest_url or "").strip()
if not u:
return ""
u = u.split("?", 1)[0].strip()
while u.endswith("/"):
u = u[:-1]
if u.lower().endswith("/ingest_csv"):
u = u[: -len("/ingest_csv")]
if u and not u.endswith("/"):
u = u + "/"
if not (u.startswith("http://") or u.startswith("https://")):
return ""
return u
# ------------------------------------------------------------
# UI
# ------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
def home():
sugg_openai = LLM_INGEST_URL_OPENAI or "https://<tu-openai-space>.hf.space/ingest_csv"
sugg_groq = LLM_INGEST_URL_GROQ or "https://<tu-groq-space>.hf.space/ingest_csv"
ui_openai = _ui_from_ingest(LLM_INGEST_URL_OPENAI) or "https://alexacido-llm-consumertec.hf.space/"
ui_groq = _ui_from_ingest(LLM_INGEST_URL_GROQ) or "https://alexacido-consumetec-llm.hf.space/"
html = """
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Programa para enviar csv</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:#0b1220;color:#e5e7eb;margin:0;padding:24px;}
.card{max-width:980px;margin:0 auto;background:#111827;border:1px solid #243244;border-radius:18px;padding:18px 18px;}
h1{margin:0 0 10px 0;font-size:1.35rem;}
.muted{color:#9ca3af;font-size:.92rem;line-height:1.45;}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;align-items:center;}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px;}
@media (max-width: 820px){ .grid2{grid-template-columns:1fr;} }
button{cursor:pointer;border:0;border-radius:999px;padding:10px 16px;font-weight:800;background:linear-gradient(135deg,#22d3ee,#4ade80);color:#0b1220;}
button.secondary{background:#0b1220;color:#e5e7eb;border:1px solid #243244;font-weight:700;}
select,input{width:100%;padding:10px 12px;border-radius:12px;border:1px solid #243244;background:#0b1220;color:#e5e7eb;}
label{display:block;color:#9ca3af;font-size:.85rem;margin-bottom:6px;}
pre{white-space:pre-wrap;background:#0b1220;border:1px solid #243244;border-radius:12px;padding:12px;margin-top:12px;max-height:48vh;overflow:auto;}
code{color:#93c5fd;}
.pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px solid #243244;border-radius:999px;background:#0b1220;color:#9ca3af;font-size:.8rem;}
</style>
</head>
<body>
<div class="card">
<h1>Programa para enviar csv</h1>
<div class="muted">
Este servicio lee el CSV desde <code>__CSV_FILE_PATH__</code> y lo envía a un Space destino
usando <code>POST /ingest_csv</code>. (Ahora abre el Space destino inmediatamente al hacer click).
</div>
<div class="row">
<div class="pill">Modo: <code>__DEFAULT_MODE__</code></div>
<div class="pill">Token en Laundry: <code>__HAS_TOKEN__</code></div>
</div>
<div class="grid2">
<div>
<label>Destino</label>
<select id="destKey">
<option value="openai">OpenAI (llm-consumertec)</option>
<option value="groq">Groq (consumetec-llm)</option>
<option value="custom">Custom (pegar URL)</option>
</select>
</div>
<div>
<label>Custom URL (si eliges "Custom")</label>
<input id="customUrl" placeholder="https://&lt;space&gt;.hf.space/ingest_csv" />
</div>
</div>
<div class="grid2">
<div>
<label>Sugerencia OpenAI</label>
<input value="__SUGG_OPENAI__" readonly />
</div>
<div>
<label>Sugerencia Groq</label>
<input value="__SUGG_GROQ__" readonly />
</div>
</div>
<div class="row">
<button id="sendBtn">Enviar CSV (Destino seleccionado) + Abrir Space</button>
<button class="secondary" id="sendOpenAI">Enviar a OpenAI + Abrir</button>
<button class="secondary" id="sendGroq">Enviar a Groq + Abrir</button>
<button class="secondary" id="statusBtn">Ver Status</button>
</div>
<pre id="out">// Resultado aparecerá aquí…</pre>
</div>
<script>
const out = document.getElementById("out");
const UI_OPENAI = "__UI_OPENAI__";
const UI_GROQ = "__UI_GROQ__";
function uiFromIngest(url){
if(!url) return "";
let u = String(url).trim();
if(!/^https?:\\/\\//i.test(u)) return "";
u = u.split("?",1)[0].trim();
while(u.endsWith("/")) u = u.slice(0,-1);
if(u.toLowerCase().endsWith("/ingest_csv")) u = u.slice(0, -("/ingest_csv".length));
if(u && !u.endsWith("/")) u += "/";
return u;
}
async function callSend(destKey){
const customUrl = (document.getElementById("customUrl").value || "").trim();
let targetUI = "";
if(destKey === "openai") targetUI = UI_OPENAI;
else if(destKey === "groq") targetUI = UI_GROQ;
else if(destKey === "custom") targetUI = uiFromIngest(customUrl);
// ✅ ABRIR DIRECTO EL SPACE (evita pestaña blanca)
if(targetUI){
try{
window.open(targetUI, "_blank");
}catch(e){
// si el navegador bloquea, no rompemos el flujo
}
}
out.textContent = "Enviando CSV…";
try{
const r = await fetch("/send_csv", {
method:"POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({dest_key: destKey, custom_url: customUrl})
});
const t = await r.text();
out.textContent = t;
}catch(e){
out.textContent = "ERROR llamando /send_csv: " + e;
}
}
document.getElementById("sendBtn").onclick = async () => {
const destKey = document.getElementById("destKey").value;
await callSend(destKey);
};
document.getElementById("sendOpenAI").onclick = async () => {
await callSend("openai");
};
document.getElementById("sendGroq").onclick = async () => {
await callSend("groq");
};
document.getElementById("statusBtn").onclick = async () => {
out.textContent = "Consultando /status…";
try{
const r = await fetch("/status");
const t = await r.text();
out.textContent = t;
}catch(e){
out.textContent = "ERROR llamando /status: " + e;
}
};
</script>
</body>
</html>
"""
html = html.replace("__CSV_FILE_PATH__", CSV_FILE_PATH)
html = html.replace("__DEFAULT_MODE__", DEFAULT_MODE)
html = html.replace("__HAS_TOKEN__", "SI" if bool(INGEST_TOKEN) else "NO")
html = html.replace("__SUGG_OPENAI__", sugg_openai)
html = html.replace("__SUGG_GROQ__", sugg_groq)
html = html.replace("__UI_OPENAI__", ui_openai)
html = html.replace("__UI_GROQ__", ui_groq)
return HTMLResponse(content=html)
# ------------------------------------------------------------
# STATUS
# ------------------------------------------------------------
@app.get("/status")
def status():
return {
"status": "ok",
"csv_file_path": CSV_FILE_PATH,
"exists": os.path.exists(CSV_FILE_PATH),
"default_mode": DEFAULT_MODE,
"ingest_token_in_laundry": bool(INGEST_TOKEN),
"llm_ingest_url_default": bool(LLM_INGEST_URL),
"llm_ingest_url_openai": bool(LLM_INGEST_URL_OPENAI),
"llm_ingest_url_groq": bool(LLM_INGEST_URL_GROQ),
}
# ------------------------------------------------------------
# EXPORT CSV (pull)
# ------------------------------------------------------------
@app.get("/export_csv")
def export_csv():
if os.path.exists(CSV_FILE_PATH):
return FileResponse(
CSV_FILE_PATH,
media_type="text/csv; charset=utf-8",
filename=os.path.basename(CSV_FILE_PATH),
)
return Response(content=_sample_csv(), media_type="text/csv; charset=utf-8")
# ------------------------------------------------------------
# SEND CSV (push)
# ------------------------------------------------------------
class SendRequest(BaseModel):
dest_key: str = "openai" # openai | groq | custom
custom_url: str = "" # se usa si dest_key=custom
@app.post("/send_csv")
async def send_csv(payload: SendRequest):
dest_url = _resolve_dest(payload.dest_key, payload.custom_url)
if not dest_url:
raise HTTPException(
status_code=400,
detail="Destino no configurado. Configura LLM_INGEST_URL_* o usa Custom URL."
)
if not os.path.exists(CSV_FILE_PATH):
raise HTTPException(status_code=404, detail=f"No existe el archivo CSV en: {CSV_FILE_PATH}")
try:
with open(CSV_FILE_PATH, "r", encoding="utf-8") as f:
csv_text = f.read()
except Exception as e:
raise HTTPException(status_code=500, detail=f"No se pudo leer CSV: {e}")
out_payload = {
"csv_text": csv_text,
"filename": os.path.basename(CSV_FILE_PATH),
"mode": DEFAULT_MODE
}
headers = {"Content-Type": "application/json"}
if INGEST_TOKEN:
headers["X-INGEST-TOKEN"] = INGEST_TOKEN
timeout = httpx.Timeout(60.0, connect=20.0)
try:
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
r = await client.post(dest_url, json=out_payload, headers=headers)
except Exception as e:
raise HTTPException(status_code=502, detail=f"Error enviando al destino: {e}")
if r.status_code != 200:
return {
"status": "error",
"message": "El destino respondió con error.",
"destination": dest_url,
"destination_status_code": r.status_code,
"destination_body": (r.text or "")[:1500],
}
return {
"status": "ok",
"message": "CSV enviado correctamente al destino.",
"destination": dest_url,
"destination_status_code": r.status_code,
"destination_body": (r.text or "")[:1500],
"bytes_sent": len(csv_text.encode("utf-8")),
}
if __name__ == "__main__":
port = int(os.environ.get("PORT", "7860"))
uvicorn.run(app, host="0.0.0.0", port=port)