add healthcheck to remaining apps

This commit is contained in:
2025-11-25 07:05:14 +00:00
parent eccd53673c
commit 6d9686e373
87 changed files with 850 additions and 632 deletions

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-normativa
namespace: apps
labels:
app: mayacontigo-normativa
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-normativa
template:
metadata:
labels:
app: mayacontigo-normativa
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-normativa
image: gitea.ia-innovacion.work/innovacion/mayacontigo-normativa:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-normativa-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-normativa-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-normativa.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-normativa-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-normativa-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-normativa-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-normativa
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-normativa-service
labels:
app: mayacontigo-normativa
spec:
selector:
app: mayacontigo-normativa
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -1,15 +1,14 @@
import uuid
import os
import uuid
from contextlib import asynccontextmanager
from pathlib import Path
from dotenv import load_dotenv # ← Agregar este import
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse, RedirectResponse
from pydantic import BaseModel
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from langfuse import Langfuse
from dotenv import load_dotenv # ← Agregar este import
from pydantic import BaseModel
from api import services
from api.agent import Agent
@@ -21,8 +20,8 @@ load_dotenv()
# Configurar Langfuse desde variables de entorno
langfuse = Langfuse(
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
host=os.getenv("LANGFUSE_HOST")
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
host=os.getenv("LANGFUSE_HOST"),
)
@@ -34,12 +33,10 @@ PDF_PUBLIC_URLS = {
"Disposiciones de carácter general aplicables a las sociedades controladoras de grupos financieros y subcontroladoras que regulan las materias que corresponden de manera conjunta a las Comisio.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20sociedades%20controladoras%20de%20grupos%20financieros%20y%20subcontroladoras%20que%20regulan%20las%20materias%20que%20corresponden%20de%20manera%20conjunta%20a%20las%20Comisiones%20Nacionales%20Supervisoras.pdf",
"Disposiciones de carácter general aplicables a los fondos de inversión y a las personas que les prestan servicios.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20los%20fondos%20de%20inversi%C3%B3n%20y%20a%20las%20personas%20que%20les%20prestan%20servicios.pdf",
"Ley para la Transparencia y Ordenamiento de los Servicios Financieros.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20para%20la%20Transparencia%20y%20Ordenamiento%20de%20los%20Servicios%20Financieros.pdf",
# Circulares CNBV adicionales
"circular_servicios_de_inversion.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20entidades%20financieras%20y%20dem%C3%A1s%20personas%20que%20proporcionen%20servicios%20de.pdf",
"circular_unica_de_auditores_externos.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20que%20establecen%20los%20requisitos%20que%20deber%C3%A1n%20cumplir%20los%20auditores%20y%20otros%20profesionales%20que.pdf",
"ley_de_instituciones_de_Credito.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20de%20Instituciones%20de%20Cr%C3%A9dito.pdf",
# Circulares de Banxico
"circular_13_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2007/cobro-intereses-por-adelantad.html",
"circular_13_2011.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2011/%7BBA4CBC28-A468-16C9-6F17-9EA9D7B03318%7D.pdf",
@@ -55,15 +52,12 @@ PDF_PUBLIC_URLS = {
"circular_36_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-36-2010/%7B26C55DE6-CC3A-3368-34FC-1A6C50B11130%7D.pdf",
"circular_3_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-3-2012/%7B4E0281A4-7AD8-1462-BC79-7F2925F3171D%7D.pdf",
"circular_4_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-4-2012/%7B97C62974-1C94-19AE-AB5A-D0D949A36247%7D.pdf",
# CONDUSEF
"circular_unica_de_condusef.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
"ley_para_regular_las_sociedades_de_informacion_crediticia.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
# Leyes federales
"ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf",
"reglamento_de_la_ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/regley/Reg_LFPDPPP.pdf",
# SharePoint Banorte
"Modificaciones Recursos Procedencia Ilícita jul 25 PLD.pdf": "https://gfbanorte.sharepoint.com/:w:/r/sites/Formatosyplantillas/Documentos%20compartidos/Otros/Modificaciones%20Recursos%20Procedencia%20Il%C3%ADcita%20jul%2025%20PLD.docx?d=w6a941e9e2c26403ea41c12de35536516&csf=1&web=1&e=EHtc9b",
}
@@ -79,17 +73,17 @@ app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
expose_headers=["*"],
)
agent = Agent()
PDF_FOLDER = Path(__file__).parent / "agent" / "pdf"
PDF_FOLDER.mkdir(parents=True, exist_ok=True)
PDF_FOLDER.mkdir(parents=True, exist_ok=True)
@app.post("/api/v1/conversation")
@@ -110,21 +104,21 @@ async def send(message: Message):
trace = langfuse.trace(
name="rag_chat",
session_id=str(message.conversation_id),
input={"prompt": message.prompt}
input={"prompt": message.prompt},
)
def b64_sse(func):
async def wrapper(*args, **kwargs):
response_parts = []
async for chunk in func(*args, **kwargs):
if chunk.type == "text" and chunk.content:
response_parts.append(str(chunk.content))
content = chunk.model_dump_json()
data = f"data: {content}\n\n"
yield data
# Solo registrar input y output
full_response = "".join(response_parts)
trace.update(output={"response": full_response})
@@ -139,17 +133,21 @@ async def send(message: Message):
@app.get("/api/pdf/{filename}")
async def get_pdf(filename: str):
print(f"🔍 Solicitud PDF para: {filename}")
if not filename.lower().endswith('.pdf'):
if not filename.lower().endswith(".pdf"):
print(f"❌ Archivo no es PDF: {filename}")
raise HTTPException(status_code=400, detail="El archivo debe ser un PDF")
if '..' in filename or ('/' in filename and not filename.startswith('http')) or '\\' in filename:
if (
".." in filename
or ("/" in filename and not filename.startswith("http"))
or "\\" in filename
):
print(f"❌ Nombre de archivo inválido: {filename}")
raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
public_url = PDF_PUBLIC_URLS.get(filename)
if public_url:
print(f"✅ Redirigiendo a URL pública: {public_url}")
return RedirectResponse(
@@ -159,39 +157,41 @@ async def get_pdf(filename: str):
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*"
}
"Access-Control-Allow-Headers": "*",
},
)
pdf_path = PDF_FOLDER / filename
if not pdf_path.exists():
print(f"❌ PDF no encontrado: {pdf_path}")
raise HTTPException(status_code=404, detail=f"PDF no encontrado. Archivo: {filename}")
raise HTTPException(
status_code=404, detail=f"PDF no encontrado. Archivo: {filename}"
)
if not pdf_path.is_file():
print(f"❌ No es un archivo: {pdf_path}")
raise HTTPException(status_code=404, detail="El recurso no es un archivo")
file_size = pdf_path.stat().st_size
print(f"📄 Sirviendo archivo local: {filename} ({file_size} bytes)")
if file_size == 0:
print(f"❌ Archivo vacío: {pdf_path}")
raise HTTPException(status_code=500, detail="El archivo PDF está vacío")
return FileResponse(
path=str(pdf_path),
media_type="application/pdf",
filename=filename,
headers={
"Content-Disposition": f"inline; filename={filename}",
"Content-Disposition": f"inline; filename={filename}",
"Content-Type": "application/pdf",
"Cache-Control": "public, max-age=3600",
"X-Frame-Options": "ALLOWALL",
"X-Frame-Options": "ALLOWALL",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*"
}
"Access-Control-Allow-Origin": "*",
},
)
@@ -199,29 +199,33 @@ async def get_pdf(filename: str):
async def list_pdfs():
try:
pdf_files = []
for filename, url in PDF_PUBLIC_URLS.items():
pdf_files.append({
"filename": filename,
"size": "N/A (Público)",
"url": f"/api/pdf/{filename}",
"public_url": url,
"type": "public"
})
pdf_files.append(
{
"filename": filename,
"size": "N/A (Público)",
"url": f"/api/pdf/{filename}",
"public_url": url,
"type": "public",
}
)
local_files = []
for pattern in ["*.pdf", "*.PDF"]:
for file_path in PDF_FOLDER.glob(pattern):
if file_path.is_file() and file_path.name not in PDF_PUBLIC_URLS:
local_files.append({
"filename": file_path.name,
"size": file_path.stat().st_size,
"url": f"/api/pdf/{file_path.name}",
"type": "local"
})
local_files.append(
{
"filename": file_path.name,
"size": file_path.stat().st_size,
"url": f"/api/pdf/{file_path.name}",
"type": "local",
}
)
pdf_files.extend(local_files)
debug_info = {
"current_working_directory": str(Path.cwd()),
"pdf_folder_path": str(PDF_FOLDER.absolute()),
@@ -230,52 +234,49 @@ async def list_pdfs():
"local_files_count": len(local_files),
"public_files": list(PDF_PUBLIC_URLS.keys()),
}
return {
"pdfs": pdf_files,
"debug": debug_info,
"total_pdfs": len(pdf_files)
}
return {"pdfs": pdf_files, "debug": debug_info, "total_pdfs": len(pdf_files)}
except Exception as e:
import traceback
return {
"error": str(e),
"traceback": traceback.format_exc(),
"debug": {
"current_working_directory": str(Path.cwd()),
"script_file_path": __file__ if '__file__' in globals() else "unknown"
}
"script_file_path": __file__ if "__file__" in globals() else "unknown",
},
}
@app.get("/api/pdf/{filename}/info")
async def get_pdf_info(filename: str):
if not filename.lower().endswith('.pdf'):
if not filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="El archivo debe ser un PDF")
if '..' in filename or '/' in filename or '\\' in filename:
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
public_url = PDF_PUBLIC_URLS.get(filename)
if public_url:
return {
"filename": filename,
"size": "N/A",
"size_mb": "N/A",
"size_mb": "N/A",
"modified": "N/A",
"url": f"/api/pdf/{filename}",
"public_url": public_url,
"type": "public"
"type": "public",
}
pdf_path = PDF_FOLDER / filename
if not pdf_path.exists():
raise HTTPException(status_code=404, detail="PDF no encontrado")
if not pdf_path.is_file():
raise HTTPException(status_code=404, detail="El recurso no es un archivo")
try:
file_stat = pdf_path.stat()
return {
@@ -284,10 +285,12 @@ async def get_pdf_info(filename: str):
"size_mb": round(file_stat.st_size / (1024 * 1024), 2),
"modified": file_stat.st_mtime,
"url": f"/api/pdf/{filename}",
"type": "local"
"type": "local",
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error al obtener información del PDF: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error al obtener información del PDF: {str(e)}"
)
@app.get("/api/health")
@@ -296,5 +299,10 @@ async def health_check():
"status": "healthy",
"pdf_folder": str(PDF_FOLDER),
"pdf_folder_exists": PDF_FOLDER.exists(),
"public_urls_configured": len(PDF_PUBLIC_URLS)
}
"public_urls_configured": len(PDF_PUBLIC_URLS),
}
@app.get("/")
async def health():
return {"status": "ok"}