import json import os from fastapi import FastAPI import uvicorn from engine.risk_engine import VertexRiskEngine from engine.semantic import VertexSemanticAgent from engine.web3_engine import VertexWeb3Engine app = FastAPI() # Archivo de persistencia de la watchlist WATCHLIST_FILE = "app/watchlist.json" @app.get("/batch_audit") def run_batch_audit(): """Audita toda la watchlist y devuelve alertas para n8n.""" if not os.path.exists(WATCHLIST_FILE): return {"error": "Watchlist file not found", "alerts": []} try: with open(WATCHLIST_FILE, "r") as f: watchlist = json.load(f) except Exception as e: return {"error": f"Failed to read watchlist: {e}", "alerts": []} results = [] for ticker in watchlist: try: # Reutilizamos la lógica del motor Némesis engine = VertexRiskEngine(ticker.upper()) num_res = engine.run_audit() sem_agent = VertexSemanticAgent(ticker.upper()) risk_text = sem_agent.get_sec_risk_factors() sem_res = sem_agent.judge_risks(risk_text) # Blindaje de tipos para evitar errores de comparación z_score = float(num_res.get('altman_z', 0.0)) m_dsri = float(num_res.get('m_score_dsri', 0.0)) s_score = float(sem_res.get('semantic_score', 0.0)) # Lógica de Semáforo de Riesgo if z_score < 1.8 or m_dsri > 1.4 or s_score > 3: status = "RED" elif z_score < 3.0: status = "YELLOW" else: status = "GREEN" results.append({ "ticker": ticker, "status": status, "z_score": z_score, "summary": sem_res.get("summary", ""), "alert": True if status == "RED" else False }) except Exception as ticker_error: results.append({"ticker": ticker, "status": "ERROR", "msg": str(ticker_error)}) critical_alerts = [r for r in results if r.get("status") in ["RED", "YELLOW"]] return { "total_analyzed": len(watchlist), "critical_count": len(critical_alerts), "alerts": critical_alerts, "full_results": results } @app.get("/audit/{ticker}") def audit_company(ticker: str): """Auditoría individual para el tab de Stock Audit.""" try: engine = VertexRiskEngine(ticker.upper()) num_res = engine.run_audit() sem_agent = VertexSemanticAgent(ticker.upper()) risk_text = sem_agent.get_sec_risk_factors() sem_res = sem_agent.judge_risks(risk_text) # Blindaje de tipos z_score = float(num_res.get('altman_z', 0.0)) m_dsri = float(num_res.get('m_score_dsri', 0.0)) s_score = float(sem_res.get('semantic_score', 0.0)) if z_score < 1.8 or m_dsri > 1.4 or s_score > 3: status = "RED" msg = "CRITICAL RISK: Red flags detected." elif z_score < 3.0: status = "YELLOW" msg = "CAUTION: Monitor closely." else: status = "GREEN" msg = "SAFE: Solid fundamentals." return { "ticker": ticker.upper(), "status": status, "numeric_analysis": {"altman_z": z_score, "m_score_dsri": m_dsri}, "semantic_analysis": sem_res, "msg": msg } except Exception as e: return {"status": "ERROR", "msg": str(e)} @app.get("/audit_contract/{address}") def audit_smart_contract(address: str): """Auditoría de Web3 usando el nuevo motor V2.""" try: web3_engine = VertexWeb3Engine(address) audit_res = web3_engine.get_contract_source() if not audit_res["success"]: return {"status": "ERROR", "msg": audit_res["error"]} source = audit_res["source_code"] vulnerabilities = web3_engine.scan_basic_vulnerabilities(source) return { "address": address, "status": "DANGER" if len(vulnerabilities) > 0 else "SAFE", "vulnerabilities": vulnerabilities, "source_preview": source[:500] + "..." # Ahora source es string y el slice no falla } except Exception as e: return {"status": "ERROR", "msg": str(e)} @app.get("/get_settings") async def get_settings(): # Esta línea es la que saca las llaves de los Secrets de Hugging Face token = os.environ.get("BOT_TOKEN", "") chat = os.environ.get("CHAT_ID", "") if token and chat: return {"bot_token": token, "chat_id": chat, "status": "loaded_from_secrets"} return {"bot_token": "", "chat_id": "", "status": "secrets_not_found"} if __name__ == "__main__": # Hugging Face SIEMPRE usa el puerto 7860 internamente port = int(os.environ.get("PORT", 7860)) # Importante: host="0.0.0.0" para que sea accesible desde fuera del contenedor uvicorn.run(app, host="0.0.0.0", port=port)