eceafc's picture
Upload app.py
a63b3c7 verified
Raw
History Blame Contribute Delete
14 kB
from dotenv import load_dotenv
from openai import OpenAI
import json
import os
import requests
from pypdf import PdfReader
import gradio as gr
from openai import APIConnectionError, APITimeoutError, RateLimitError, APIError
import time
from pathlib import Path
import numpy as np
import faiss
from datetime import datetime
from zoneinfo import ZoneInfo
load_dotenv(override=True)
LOG_PATH = Path("me/questions_log.jsonl")
MADRID_TZ = ZoneInfo("Europe/Madrid")
def push(text):
try:
requests.post(
"https://api.pushover.net/1/messages.json",
data={
"token": os.getenv("PUSHOVER_TOKEN"),
"user": os.getenv("PUSHOVER_USER"),
"message": text,
},
timeout=10, # ✅ evita cuelgues
)
except Exception as e:
print("Pushover error:", repr(e), flush=True)
def log_user_question(question: str, user_label: str = "unknown", notes: str = ""):
"""
Guarda en JSONL (append) un registro por pregunta.
- question: texto literal de la pregunta del usuario
- user_label: nombre/email/alias si el LLM lo conoce; si no, "unknown"
- notes: pistas extra (ej: "no dio nombre", "dice que es recruiter", etc.)
"""
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
record = {
"ts": datetime.now(MADRID_TZ).isoformat(timespec="seconds"),
"user": user_label or "unknown",
"question": question,
"notes": notes or "",
}
with LOG_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return {"logged": "ok"}
def _read_pdf(path: str) -> str:
reader = PdfReader(path)
out = []
for page in reader.pages:
t = page.extract_text()
if t:
out.append(t)
return "\n".join(out)
def _read_text(path: str) -> str:
return Path(path).read_text(encoding="utf-8", errors="ignore")
def _chunk_text(text: str, chunk_size: int = 1400, overlap: int = 220) -> list[str]:
text = " ".join(text.split())
chunks = []
i = 0
while i < len(text):
chunks.append(text[i:i + chunk_size])
i += max(1, chunk_size - overlap)
return chunks
class SimpleRAG:
def __init__(self, client, docs_dir="me", embed_model="text-embedding-3-small"):
self.client = client
self.docs_dir = docs_dir
self.embed_model = embed_model
self.index = None
self.chunks: list[str] = []
def _embed(self, texts: list[str]) -> np.ndarray:
resp = self.client.embeddings.create(model=self.embed_model, input=texts)
vecs = np.array([d.embedding for d in resp.data], dtype="float32")
faiss.normalize_L2(vecs) # cosine via inner product
return vecs
def build(self):
all_chunks = []
base = Path(self.docs_dir)
if not base.exists():
print(f"[RAG] docs_dir not found: {self.docs_dir}", flush=True)
self.index = None
self.chunks = []
return
for p in base.rglob("*"):
if p.is_dir():
continue
suffix = p.suffix.lower()
if suffix == ".pdf":
raw = _read_pdf(str(p))
elif suffix in [".txt", ".md"]:
raw = _read_text(str(p))
else:
continue
if not raw.strip():
continue
for c in _chunk_text(raw):
all_chunks.append(f"[{p.name}] {c}")
self.chunks = all_chunks
if not self.chunks:
print("[RAG] No chunks created (empty index).", flush=True)
self.index = None
return
vecs = self._embed(self.chunks)
dim = vecs.shape[1]
self.index = faiss.IndexFlatIP(dim)
self.index.add(vecs)
print(f"[RAG] Indexed {len(self.chunks)} chunks from {self.docs_dir}", flush=True)
def retrieve(self, query: str, k: int = 4) -> list[str]:
if self.index is None or not self.chunks:
return []
qv = self._embed([query])
scores, idx = self.index.search(qv, k)
out = []
for i, s in zip(idx[0], scores[0]):
if i == -1:
continue
# filtro suave por relevancia (ajusta si quieres)
if s < 0.20:
continue
out.append(self.chunks[int(i)])
return out
def record_user_details(email, name="Nombre no indicado", notes="no proporcionadas"):
push(f"Registrando {name} con email {email} y notas {notes}")
return {"recorded": "ok"}
def record_unknown_question(question):
push(f"Registrando {question}")
return {"recorded": "ok"}
record_user_details_json = {
"name": "record_user_details",
"description": "Utiliza esta herramienta para registrar que un usuario está interesado en estar en contacto y proporcionó una dirección de correo electrónico.",
"parameters": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "La dirección de email del usuario"
},
"name": {
"type": "string",
"description": "El nombre del usuario, si se indica"
}
,
"notes": {
"type": "string",
"description": "¿Alguna información adicional sobre la conversación que valga la pena registrar para dar contexto?"
}
},
"required": ["email"],
"additionalProperties": False
}
}
record_unknown_question_json = {
"name": "record_unknown_question",
"description": "Utiliza siempre esta herramienta para registrar cualquier pregunta que no haya podido responder porque no se sabía la respuesta.",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "La pregunta no sabe responderse"
},
},
"required": ["question"],
"additionalProperties": False
}
}
log_user_question_json = {
"name": "log_user_question",
"description": "Registra SIEMPRE la pregunta del usuario en un fichero para análisis posterior. Usa user_label si conoces nombre/email; si no, 'unknown'.",
"parameters": {
"type": "object",
"properties": {
"question": {"type": "string", "description": "Pregunta del usuario (texto literal)."},
"user_label": {"type": "string", "description": "Nombre/email/alias si se conoce; si no, 'unknown'."},
"notes": {"type": "string", "description": "Pistas adicionales si no se conoce identidad (ej: 'no dio nombre')."}
},
"required": ["question"],
"additionalProperties": False
}
}
tools = [{"type": "function", "function": record_user_details_json},
{"type": "function", "function": record_unknown_question_json},
{"type": "function", "function": log_user_question_json}]
class Me:
def __init__(self):
api_key = os.getenv("OPENAI_API_KEY")
print("OPENAI_API_KEY exists:", bool(api_key))
print("OPENAI_API_KEY length:", len(api_key) if api_key else 0)
import httpx
def netcheck():
try:
r = httpx.get("https://api.openai.com/v1/models", timeout=15.0, headers={
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"
})
print("NETCHECK status:", r.status_code, flush=True)
if r.status_code != 200:
print("NETCHECK body:", r.text[:200], flush=True)
except Exception as e:
print("NETCHECK exception:", repr(e), flush=True)
netcheck()
self.openai = OpenAI(api_key=api_key, timeout=30.0, max_retries=5)
# self.openai = OpenAI()
self.name = "Alberto Fraile Centenera"
self.rag = SimpleRAG(self.openai, docs_dir="me")
self.rag.build()
reader = PdfReader("me/Profile.pdf")
self.linkedin = ""
for page in reader.pages:
text = page.extract_text()
if text:
self.linkedin += text
reader = PdfReader("me/Profile-2.pdf")
for page in reader.pages:
text = page.extract_text()
if text:
self.linkedin += text
with open("me/summary.txt", "r", encoding="utf-8") as f:
self.summary = f.read()
def handle_tool_call(self, tool_calls):
results = []
for tool_call in tool_calls:
tool_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
print(f"Tool called: {tool_name}", flush=True)
tool = globals().get(tool_name)
result = tool(**arguments) if tool else {}
results.append({"role": "tool","content": json.dumps(result),"tool_call_id": tool_call.id})
return results
def system_prompt(self):
system_prompt = f"""Actúas como {self.name}. Respondes preguntas en el sitio web de {self.name}, en particular preguntas relacionadas con la trayectoria profesional, los antecedentes, las habilidades y la experiencia de {self.name}.
Tu responsabilidad es representar a {self.name} en las interacciones del sitio web con la mayor fidelidad posible.
Muestra un tono profesional y atractivo, como si hablaras con un cliente potencial o un futuro empleador que haya visitado el sitio web.
Responde en el mismo idioma que sea la pregunta.
Usa el contexto recuperado (RAG) como fuente principal.
REGLA OBLIGATORIA (logging):
- En CADA mensaje del usuario, ANTES de responder, debes llamar a la herramienta 'log_user_question' con:
- question: el mensaje literal del usuario
- user_label: el nombre/email si el usuario lo dio; si no, "unknown"
- notes: una breve pista si no hay identidad (ej: "no dio nombre")
Si no sabes la respuesta a alguna pregunta, usa la herramienta 'record_unknown_question' para registrar la pregunta.
Si el usuario participa en una conversación, intenta que se ponga en contacto por correo electrónico; pídele su correo electrónico y regístralo con la herramienta 'record_user_details'.
En este contexto, por favor chatea con el usuario, manteniéndote siempre en el personaje de {self.name}."""
return system_prompt
def chat(self, message, history):
rag_chunks = self.rag.retrieve(message, k=4)
rag_block = "\n\n".join(rag_chunks) if rag_chunks else "No relevant context found."
messages = [{"role": "system", "content": self.system_prompt() + "\n\n## Contexto recuperado (RAG):\n" + rag_block}] + history + [{"role": "user", "content": message}]
done = False
attempts = 0
max_attempts = 3 # reintentos del bucle completo ante errores de red
while not done:
try:
response = self.openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
except (APIConnectionError, APITimeoutError) as e:
attempts += 1
print("OpenAI network error:", repr(e), "attempt", attempts, flush=True)
if attempts >= max_attempts:
return "Ahora mismo no puedo conectar con el servicio de IA (problema de red). Prueba de nuevo en unos segundos."
time.sleep(1.5) # pequeño backoff
continue
except RateLimitError as e:
print("OpenAI rate limit:", repr(e), flush=True)
return "Hay demasiadas solicitudes ahora mismo. Prueba de nuevo en unos segundos."
except APIError as e:
# errores 5xx / upstream
print("OpenAI API error:", repr(e), flush=True)
return "El servicio de IA está teniendo problemas momentáneos. Prueba de nuevo en unos segundos."
except Exception as e:
print("Unexpected error:", repr(e), flush=True)
return "Ha ocurrido un error inesperado. Inténtalo otra vez."
finish = response.choices[0].finish_reason
if finish == "tool_calls":
assistant_msg = response.choices[0].message
tool_calls = assistant_msg.tool_calls
results = []
for tool_call in tool_calls:
tool_name = tool_call.function.name
try:
arguments = json.loads(tool_call.function.arguments or "{}")
except json.JSONDecodeError:
arguments = {}
print(f"Tool called: {tool_name}", flush=True)
tool = globals().get(tool_name)
result = tool(**arguments) if tool else {}
results.append({
"role": "tool",
"content": json.dumps(result),
"tool_call_id": tool_call.id
})
messages.append(assistant_msg)
messages.extend(results)
else:
done = True
return response.choices[0].message.content
if __name__ == "__main__":
me = Me()
gr.ChatInterface(me.chat, type="messages").launch(ssr_mode=False)