Spaces:
Running
Running
| import os | |
| from datetime import datetime | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse | |
| from pydantic import BaseModel | |
| from typing import Optional, List | |
| from openai import OpenAI | |
| # ── App Setup ────────────────────────────────────────────────────────────── | |
| app = FastAPI(title="HERMES AGENT", version="1.0.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ── Config ───────────────────────────────────────────────────────────────── | |
| NVIDIA_API_KEY = os.getenv("OPENAI_API_KEY", "") | |
| SUPABASE_URL = os.getenv("SUPABASE_URL", "") | |
| SUPABASE_KEY = os.getenv("SUPABASE_KEY", "") | |
| HERMES_NAME = os.getenv("HERMES_NAME", "HERMES") | |
| NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "meta/llama-3.1-70b-instruct") | |
| HERMES_SYSTEM_PROMPT = os.getenv( | |
| "HERMES_SYSTEM_PROMPT", | |
| "Eres HERMES, un agente de inteligencia artificial avanzado, leal y preciso. " | |
| "Tienes memoria persistente. Respondes en el mismo idioma que el usuario." | |
| ) | |
| # ── NVIDIA NIM Client (OpenAI-compatible) ────────────────────────────────── | |
| client: Optional[OpenAI] = None | |
| if NVIDIA_API_KEY: | |
| client = OpenAI( | |
| base_url="https://integrate.api.nvidia.com/v1", | |
| api_key=NVIDIA_API_KEY | |
| ) | |
| print(f"[HERMES] NVIDIA NIM client initialized. Model: {NVIDIA_MODEL}") | |
| else: | |
| print("[HERMES] WARNING: OPENAI_API_KEY not set!") | |
| # ── Supabase memory ──────────────────────────────────────────────────────── | |
| supabase = None | |
| if SUPABASE_URL and SUPABASE_KEY: | |
| try: | |
| from supabase import create_client | |
| supabase = create_client(SUPABASE_URL, SUPABASE_KEY) | |
| print("[HERMES] Supabase connected.") | |
| except Exception as e: | |
| print(f"[HERMES] Supabase error: {e}") | |
| # In-memory fallback when Supabase not configured | |
| _mem_fallback: dict = {} | |
| def get_memory(session_id: str, limit: int = 20) -> List[dict]: | |
| if supabase: | |
| try: | |
| r = (supabase.table("hermes_memory") | |
| .select("role,content") | |
| .eq("session_id", session_id) | |
| .order("created_at", desc=False) | |
| .limit(limit) | |
| .execute()) | |
| return r.data or [] | |
| except Exception as e: | |
| print(f"[Memory] read error: {e}") | |
| return _mem_fallback.get(session_id, [])[-limit:] | |
| def save_memory(session_id: str, role: str, content: str, user_id: str = "anon"): | |
| if supabase: | |
| try: | |
| supabase.table("hermes_memory").insert({ | |
| "session_id": session_id, "user_id": user_id, | |
| "role": role, "content": content, | |
| "created_at": datetime.utcnow().isoformat() | |
| }).execute() | |
| return | |
| except Exception as e: | |
| print(f"[Memory] save error: {e}") | |
| # fallback | |
| if session_id not in _mem_fallback: | |
| _mem_fallback[session_id] = [] | |
| _mem_fallback[session_id].append({"role": role, "content": content}) | |
| # ── Models ───────────────────────────────────────────────────────────────── | |
| class ChatRequest(BaseModel): | |
| message: str | |
| session_id: str = "default" | |
| user_id: str = "anonymous" | |
| class ChatResponse(BaseModel): | |
| reply: str | |
| session_id: str | |
| timestamp: str | |
| model: str | |
| # ── Routes ───────────────────────────────────────────────────────────────── | |
| async def root(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang='es'> | |
| <head> | |
| <meta charset='UTF-8'> | |
| <meta name='viewport' content='width=device-width, initial-scale=1'> | |
| <title>HERMES AGENT</title> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| body{font-family:'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;height:100vh;display:flex;flex-direction:column} | |
| header{background:#161b22;padding:14px 20px;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:10px} | |
| header h1{font-size:1.2rem;color:#58a6ff} | |
| .badge{font-size:.7rem;background:#1f6feb;color:#fff;padding:2px 8px;border-radius:10px} | |
| .badge-green{background:#238636} | |
| #chat-box{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px} | |
| .msg{max-width:78%;padding:10px 14px;border-radius:12px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap} | |
| .user{align-self:flex-end;background:#1f6feb} | |
| .agent{align-self:flex-start;background:#21262d;border:1px solid #30363d} | |
| .agent-name{color:#58a6ff;font-size:.75rem;font-weight:700;display:block;margin-bottom:3px} | |
| .error-msg{align-self:flex-start;background:#3d1c1c;border:1px solid #f85149;color:#f85149} | |
| #input-area{padding:12px 16px;background:#161b22;border-top:1px solid #30363d;display:flex;gap:8px} | |
| #input-area input{flex:1;padding:9px 14px;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#e6edf3;font-size:.95rem;outline:none} | |
| #input-area input:focus{border-color:#58a6ff} | |
| #input-area button{padding:9px 18px;background:#1f6feb;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:.95rem} | |
| #input-area button:disabled{background:#30363d;cursor:not-allowed} | |
| #status{font-size:.72rem;color:#8b949e;padding:3px 16px;background:#161b22} | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>⚡ HERMES AGENT</h1> | |
| <span class='badge'>v1.0</span> | |
| <span class='badge badge-green'>NVIDIA NIM</span> | |
| </header> | |
| <div id='chat-box'></div> | |
| <div id='status'>Listo. Escribe tu mensaje.</div> | |
| <div id='input-area'> | |
| <input type='text' id='msg-input' placeholder='Escribe tu mensaje...' autocomplete='off'/> | |
| <button id='send-btn' onclick='sendMessage()'>Enviar</button> | |
| </div> | |
| <script> | |
| const sessionId='ses_'+Math.random().toString(36).substr(2,9); | |
| const chatBox=document.getElementById('chat-box'); | |
| const status=document.getElementById('status'); | |
| const btn=document.getElementById('send-btn'); | |
| function appendMsg(role,text,isError=false){ | |
| const d=document.createElement('div'); | |
| if(isError){d.className='msg error-msg';d.textContent='Error: '+text;} | |
| else if(role==='user'){d.className='msg user';d.textContent=text;} | |
| else{d.className='msg agent';d.innerHTML='<span class="agent-name">HERMES</span>'+text.replace(/</g,'<');} | |
| chatBox.appendChild(d); | |
| chatBox.scrollTop=chatBox.scrollHeight; | |
| } | |
| async function sendMessage(){ | |
| const input=document.getElementById('msg-input'); | |
| const msg=input.value.trim(); | |
| if(!msg)return; | |
| input.value=''; | |
| btn.disabled=true; | |
| appendMsg('user',msg); | |
| status.textContent='HERMES esta pensando...'; | |
| try{ | |
| const res=await fetch('/chat',{ | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({message:msg,session_id:sessionId}) | |
| }); | |
| const data=await res.json(); | |
| if(!res.ok){appendMsg('agent',data.detail||'Error desconocido',true);} | |
| else{appendMsg('agent',data.reply);status.textContent='Modelo: '+data.model+' | '+data.timestamp;} | |
| }catch(e){appendMsg('agent','Error de conexion: '+e.message,true);status.textContent='Error.';} | |
| finally{btn.disabled=false;input.focus();} | |
| } | |
| document.getElementById('msg-input').addEventListener('keydown',e=>{if(e.key==='Enter'&&!btn.disabled)sendMessage();}); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def health(): | |
| return { | |
| "status": "ok", | |
| "agent": HERMES_NAME, | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "nvidia_key_set": bool(NVIDIA_API_KEY), | |
| "model": NVIDIA_MODEL, | |
| "supabase_connected": supabase is not None | |
| } | |
| async def chat(req: ChatRequest): | |
| if not client: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="NVIDIA API Key no configurada. Ve a Settings > Variables and secrets > agrega OPENAI_API_KEY con tu nvapi-... key." | |
| ) | |
| history = get_memory(req.session_id) | |
| messages = [{"role": "system", "content": HERMES_SYSTEM_PROMPT}] | |
| for entry in history: | |
| messages.append({"role": entry["role"], "content": entry["content"]}) | |
| messages.append({"role": "user", "content": req.message}) | |
| try: | |
| response = client.chat.completions.create( | |
| model=NVIDIA_MODEL, | |
| messages=messages, | |
| max_tokens=1024, | |
| temperature=0.7, | |
| stream=False | |
| ) | |
| reply = response.choices[0].message.content | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| save_memory(req.session_id, "user", req.message, req.user_id) | |
| save_memory(req.session_id, "assistant", reply, req.user_id) | |
| return ChatResponse( | |
| reply=reply, | |
| session_id=req.session_id, | |
| timestamp=datetime.utcnow().strftime("%H:%M:%S UTC"), | |
| model=NVIDIA_MODEL | |
| ) | |
| async def get_history(session_id: str): | |
| history = get_memory(session_id, limit=100) | |
| return {"session_id": session_id, "messages": history, "count": len(history)} | |
| async def clear_session(session_id: str): | |
| if supabase: | |
| try: | |
| supabase.table("hermes_memory").delete().eq("session_id", session_id).execute() | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| elif session_id in _mem_fallback: | |
| del _mem_fallback[session_id] | |
| return {"status": "cleared", "session_id": session_id} |