File size: 7,681 Bytes
94cb89a
9242cc0
d8caf2b
 
94cb89a
 
f148c2a
34ca345
a324130
94cb89a
 
2c7b13b
d8caf2b
2c7b13b
 
d8caf2b
2c7b13b
 
d8caf2b
c6f5f47
94cb89a
 
 
1bd50ce
d8caf2b
 
 
94cb89a
d8d049d
d8caf2b
d8d049d
d8caf2b
 
 
 
 
0ff702f
 
 
 
 
 
 
 
 
d8caf2b
 
 
 
 
 
 
 
 
 
 
 
94cb89a
d8caf2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94cb89a
 
 
9242cc0
2c7b13b
d8caf2b
 
2c7b13b
 
 
d8caf2b
 
2c7b13b
 
 
d8caf2b
2c7b13b
d8caf2b
9242cc0
94cb89a
 
9242cc0
d8caf2b
 
 
2c7b13b
 
d8caf2b
 
2c7b13b
d8caf2b
 
9242cc0
d8caf2b
9242cc0
d8caf2b
 
 
 
 
 
 
 
 
 
 
 
ec18649
 
d8caf2b
94cb89a
d8caf2b
 
 
94cb89a
d8caf2b
 
94cb89a
 
d8caf2b
94cb89a
 
d8caf2b
 
2c7b13b
9242cc0
d8caf2b
9242cc0
2c7b13b
94cb89a
 
 
9242cc0
d8caf2b
 
94cb89a
 
 
 
d8caf2b
 
94cb89a
d8caf2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ff702f
d8caf2b
 
 
 
 
 
 
 
 
 
94cb89a
d8caf2b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import os
import requests
import shutil
from langchain_community.vectorstores import FAISS
from fastapi import FastAPI
from pydantic import BaseModel
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import PromptTemplate
from langchain_groq import ChatGroq

# --------------------------------------------------------
# CACHÉ EN /tmp
# --------------------------------------------------------
TEMP_CACHE_DIR = '/tmp/huggingface_cache'
os.environ['TRANSFORMERS_CACHE'] = TEMP_CACHE_DIR
os.environ['HF_HOME'] = TEMP_CACHE_DIR
os.environ['SENTENCE_TRANSFORMERS_HOME'] = TEMP_CACHE_DIR
os.makedirs(TEMP_CACHE_DIR, exist_ok=True)

# --------------------------------------------------------
# 1. CONFIGURACIÓN
# --------------------------------------------------------
URL_FAISS = "https://drive.google.com/uc?export=download&id=1XqImFIKiuRDhSDK6Rm6dAZbHm03NdzQa"
URL_PKL   = "https://drive.google.com/uc?export=download&id=156BWHHGi-JuD9EM2Nek1mNcyitivQWAH"
DOWNLOAD_DIR  = "/tmp/db_faiss"
DB_FAISS_PATH = DOWNLOAD_DIR

# --------------------------------------------------------
# 2. CLASIFICADOR DE INTENCIÓN  ← NUEVO
# --------------------------------------------------------
INTENT_PROMPT = PromptTemplate(
    template="""Eres un clasificador de intenciones para un asistente de nutrición llamado NutriActive.

Analiza el mensaje del usuario y clasifícalo en UNA de estas categorías:
- SALUDO: saludos, despedidas, conversación casual ("hola", "gracias", "adiós", "¿cómo estás?")
- NUTRICION: preguntas sobre nutrición, dieta, salud, alimentos, calorías, macros, IMC, 
  planes alimenticios, recetas, suplementos, hábitos saludables, Y TAMBIÉN cualquier 
  pregunta relacionada con NutriActive como empresa: sus cursos, servicios, productos, 
  planes, precios, programas, etc.
- OTRO: preguntas claramente NO relacionadas con nutrición, salud ni NutriActive 
  (matemáticas, historia, tecnología general, etc.)

IMPORTANTE: Ante la duda, clasifica como NUTRICION. Solo usa OTRO cuando estés 
completamente seguro de que no tiene relación con nutrición ni con NutriActive.

Responde SOLO con la categoría, sin explicación.

Mensaje: {query}
Categoría:""",
    input_variables=["query"]
)

SALUDO_PROMPT = PromptTemplate(
    template="""Eres NutriActive, un asistente amigable especializado en nutrición y salud.
Responde de forma natural y cálida al siguiente mensaje casual del usuario.
Si el usuario se despide o agradece, invítalo a preguntar sobre nutrición.

Mensaje: {query}
Respuesta:""",
    input_variables=["query"]
)

RAG_PROMPT = PromptTemplate(
    template="""Eres NutriActive, un asistente experto en nutrición y salud.
Tu tarea es responder basándote en el contexto proporcionado.
Si el contexto no tiene suficiente información, usa tu conocimiento general sobre nutrición para dar una respuesta útil.
Sé amigable, claro y conciso.

Contexto de la base de datos: {context}
Pregunta del usuario: {question}

Respuesta:""",
    input_variables=["context", "question"]
)

# --------------------------------------------------------
# 3. FUNCIONES DE DESCARGA Y CARGA
# --------------------------------------------------------
class QueryRequest(BaseModel):
    query: str

def download_file(url, local_path):
    file_name = os.path.basename(local_path)
    print(f"Descargando: {file_name}...")
    headers = {'User-Agent': 'Mozilla/5.0'}
    try:
        response = requests.get(url, stream=True, headers=headers, timeout=30)
        if response.status_code == 403:
            raise PermissionError(f"Error 403: {file_name} no es público.")
        response.raise_for_status()
        os.makedirs(os.path.dirname(local_path), exist_ok=True)
        with open(local_path, 'wb') as f:
            shutil.copyfileobj(response.raw, f)
        print(f"✓ {file_name} descargado.")
    except requests.exceptions.RequestException as e:
        raise RuntimeError(f"Fallo al descargar {file_name}: {e}")

def load_and_configure_rag():
    try:
        download_file(URL_FAISS, os.path.join(DOWNLOAD_DIR, 'index.faiss'))
        download_file(URL_PKL,   os.path.join(DOWNLOAD_DIR, 'index.pkl'))

        print("Cargando embeddings...")
        embeddings = HuggingFaceEmbeddings(
            model_name="sentence-transformers/all-MiniLM-L6-v2",
            model_kwargs={'device': 'cpu'},
            cache_folder=TEMP_CACHE_DIR
        )

        print("Cargando FAISS...")
        vectorstore = FAISS.load_local(
            DB_FAISS_PATH, embeddings, allow_dangerous_deserialization=True
        )

        llm = ChatGroq(temperature=0.3, model_name="llama-3.3-70b-versatile")

        # Cadena clasificadora de intención
        intent_chain = INTENT_PROMPT | llm

        # Cadena para saludos
        saludo_chain = SALUDO_PROMPT | llm

        # Cadena RAG principal
        retriever  = vectorstore.as_retriever(search_kwargs={"k": 4})
        rag_chain  = (
            {"context": retriever, "question": RunnablePassthrough()}
            | RAG_PROMPT
            | llm
        )

        return intent_chain, saludo_chain, rag_chain, retriever

    except Exception as e:
        print(f"Error CRÍTICO al inicializar: {type(e).__name__}: {e}")
        raise RuntimeError(f"Falla al cargar RAG: {e}")

# --------------------------------------------------------
# 4. FASTAPI
# --------------------------------------------------------
app = FastAPI(title="NutriActive RAG API")

intent_chain = saludo_chain = qa_chain = retriever = None

try:
    intent_chain, saludo_chain, qa_chain, retriever = load_and_configure_rag()
except RuntimeError:
    pass

@app.get("/")
def home():
    if qa_chain is None:
        return {"error": "RAG no inicializado. Revisa los logs."}
    return {"message": "API NutriActive operativa. Usa /query."}

@app.post("/query")
async def process_query(request: QueryRequest):
    if qa_chain is None:
        return {"error": "El sistema RAG no se pudo cargar."}

    try:
        # ── 1. Clasificar intención ──────────────────────────────
        intent_result = intent_chain.invoke({"query": request.query})
        intent = intent_result.content.strip().upper()
        print(f"[Intent] '{request.query}' → {intent}")

        # ── 2. Ruta según intención ──────────────────────────────
        if "SALUDO" in intent:
            respuesta = saludo_chain.invoke({"query": request.query})
            return {
                "query": request.query,
                "response": respuesta.content,
                "intent": "SALUDO",
                "sources": []
            }

        elif "OTRO" in intent:
            return {
                "query": request.query,
                "response": "Soy NutriActive, especializado en nutrición y salud. ¿Tienes alguna pregunta sobre alimentación, dietas o bienestar? 🥗",
                "intent": "OTRO",
                "sources": []
            }

        else:
            # NUTRICION o cualquier categoría no reconocida → RAG
            respuesta  = qa_chain.invoke(request.query)
            docs       = retriever.invoke(request.query)
            sources    = [doc.metadata.get("source", "N/A") for doc in docs]
            return {
                "query":    request.query,
                "response": respuesta.content,
                "intent":   "NUTRICION",
                "sources":  sources
            }

    except Exception as e:
        return {"error": f"Error al procesar la consulta: {e}"}