jimytech commited on
Commit
ce734ca
·
verified ·
1 Parent(s): 0a729a6

Update rag_api.py

Browse files
Files changed (1) hide show
  1. rag_api.py +88 -124
rag_api.py CHANGED
@@ -19,169 +19,133 @@ os.environ['SENTENCE_TRANSFORMERS_HOME'] = TEMP_CACHE_DIR
19
  os.makedirs(TEMP_CACHE_DIR, exist_ok=True)
20
 
21
  # --------------------------------------------------------
22
- # 1. CONFIGURACIÓN
23
  # --------------------------------------------------------
24
  URL_FAISS = "https://drive.google.com/uc?export=download&id=1hiVycS4DQHO1MBdC-L_z1TXA6sJO_Y-r"
25
  URL_PKL = "https://drive.google.com/uc?export=download&id=1vbG8unx88Kb5jn7puGv1gqSM4S6rIUQC"
26
  DOWNLOAD_DIR = "/tmp/db_faiss"
27
  DB_FAISS_PATH = DOWNLOAD_DIR
28
 
29
- # --------------------------------------------------------
30
- # 2. CLASIFICADOR DE INTENCIÓN ← NUEVO
31
- # --------------------------------------------------------
 
 
 
 
 
 
 
 
32
  INTENT_PROMPT = PromptTemplate(
33
- template="""Eres un clasificador de intenciones para un asistente del portal de la Universidad Poltécnica de Aragua.
34
- Analiza el mensaje del usuario y clasifícalo en UNA de estas categorías:
35
- - SALUDO: saludos, despedidas, conversación casual ("hola", "gracias", "adiós", "¿cómo estás?")
36
- - UNIVERSIDAD: preguntas sobre carreras o programas, investigación, cursos, admisiones, notas, proyectos, postgrado,
37
- PNF, PNFA, diplomados, servivios, Y TAMBIÉN cualquier pregunta relacionada con La Universidad relacionado con: sus autoridades, reglamentos,
38
- servivios estudiantiles, precios de cursos, programas, etc.
39
- - OTRO: preguntas claramente NO relacionadas con la Universidad tales como: matemáticas, historia, tecnología general, etc.
40
- IMPORTANTE: Ante la duda, clasifica como Universidad Politécnica de Aragua o UPT Aragua. Solo usa OTRO cuando estés
41
- completamente seguro de que no tiene relación con la Universidad.
42
- Responde SOLO con la categoría, sin explicación.
43
  Mensaje: {query}
44
  Categoría:""",
45
  input_variables=["query"]
46
  )
47
 
48
  SALUDO_PROMPT = PromptTemplate(
49
- template="""Eres UPTA bot, un Asistente Virtual de la UPT Aragua. Estas aquí para ayudar con información sobre admisiones, programas académicos,
50
- servicios, becas y mucho más. Si el usuario se despide o agradece, invítalo a preguntar sobre la universidad.
51
  Mensaje: {query}
52
  Respuesta:""",
53
  input_variables=["query"]
54
  )
55
 
56
  RAG_PROMPT = PromptTemplate(
57
- template="""Eres UPTA bot, un Asistente Virtual experto de la UPT Aragua. Estas aquí para ayudar con información sobre
58
- admisiones, programas académicos, servicios, becas y mucho más. Tu tarea es responder basándote en el contexto proporcionado. Si el contexto
59
- no tiene suficiente información, pide al usuario que te proporcione una pregunta más específica sobre la UPT Aragua para dar una respuesta fiable. Sé amigable, claro y conciso.
60
- Contexto de la base de datos: {context}
61
- Pregunta del usuario: {question}
62
  Respuesta:""",
63
  input_variables=["context", "question"]
64
  )
65
 
66
  # --------------------------------------------------------
67
- # 3. FUNCIONES DE DESCARGA Y CARGA
68
  # --------------------------------------------------------
69
  class QueryRequest(BaseModel):
70
  query: str
 
71
 
 
 
 
72
  def download_file(url, local_path):
73
- file_name = os.path.basename(local_path)
74
- print(f"Descargando: {file_name}...")
75
  headers = {'User-Agent': 'Mozilla/5.0'}
76
- try:
77
- response = requests.get(url, stream=True, headers=headers, timeout=30)
78
- if response.status_code == 403:
79
- raise PermissionError(f"Error 403: {file_name} no es público.")
80
- response.raise_for_status()
81
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
82
- with open(local_path, 'wb') as f:
83
- shutil.copyfileobj(response.raw, f)
84
- print(f"✓ {file_name} descargado.")
85
- except requests.exceptions.RequestException as e:
86
- raise RuntimeError(f"Fallo al descargar {file_name}: {e}")
87
 
88
  def load_and_configure_rag():
89
- try:
90
- download_file(URL_FAISS, os.path.join(DOWNLOAD_DIR, 'index.faiss'))
91
- download_file(URL_PKL, os.path.join(DOWNLOAD_DIR, 'index.pkl'))
92
-
93
- print("Cargando embeddings...")
94
- embeddings = HuggingFaceEmbeddings(
95
- model_name="sentence-transformers/all-MiniLM-L6-v2",
96
- model_kwargs={'device': 'cpu'},
97
- cache_folder=TEMP_CACHE_DIR
98
- )
99
-
100
- print("Cargando FAISS...")
101
- vectorstore = FAISS.load_local(
102
- DB_FAISS_PATH, embeddings, allow_dangerous_deserialization=True
103
- )
104
-
105
- llm = ChatGroq(temperature=0.150, model_name="openai/gpt-oss-120b")
106
-
107
- # Cadena clasificadora de intención
108
- intent_chain = INTENT_PROMPT | llm
109
-
110
- # Cadena para saludos
111
- saludo_chain = SALUDO_PROMPT | llm
112
-
113
- # Cadena RAG principal
114
- retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
115
- rag_chain = (
116
- {"context": retriever, "question": RunnablePassthrough()}
117
- | RAG_PROMPT
118
- | llm
119
- )
120
-
121
- return intent_chain, saludo_chain, rag_chain, retriever
122
-
123
- except Exception as e:
124
- print(f"Error CRÍTICO al inicializar: {type(e).__name__}: {e}")
125
- raise RuntimeError(f"Falla al cargar RAG: {e}")
126
 
127
  # --------------------------------------------------------
128
- # 4. FASTAPI
129
  # --------------------------------------------------------
130
- app = FastAPI(title="UPT Aragua bot RAG API")
131
-
132
- intent_chain = saludo_chain = qa_chain = retriever = None
133
-
134
- try:
135
- intent_chain, saludo_chain, qa_chain, retriever = load_and_configure_rag()
136
- except RuntimeError:
137
- pass
138
 
139
- @app.get("/")
140
- def home():
141
- if qa_chain is None:
142
- return {"error": "RAG no inicializado. Revisa los logs."}
143
- return {"message": "API UPT Aragua bot operativa. Usa /query."}
144
 
145
  @app.post("/query")
146
  async def process_query(request: QueryRequest):
147
- if qa_chain is None:
148
- return {"error": "El sistema RAG no se pudo cargar."}
149
-
150
- try:
151
- # ── 1. Clasificar intención ──────────────────────────────
152
- intent_result = intent_chain.invoke({"query": request.query})
153
- intent = intent_result.content.strip().upper()
154
- print(f"[Intent] '{request.query}' → {intent}")
155
-
156
- # ── 2. Ruta según intención ──────────────────────────────
157
- if "SALUDO" in intent:
158
- respuesta = saludo_chain.invoke({"query": request.query})
159
- return {
160
- "query": request.query,
161
- "response": respuesta.content,
162
- "intent": "SALUDO",
163
- "sources": []
164
- }
165
-
166
- elif "OTRO" in intent:
167
- return {
168
- "query": request.query,
169
- "response": "Soy UPTA bot, estoy especializado en la UPT Aragua. ¿Tienes alguna pregunta sobre programas, carreras, inscripciones, fechas...? 🥗",
170
- "intent": "OTRO",
171
- "sources": []
172
- }
173
-
174
- else:
175
- # UNIVERSIDAD o cualquier categoría no reconocida → RAG
176
- respuesta = qa_chain.invoke(request.query)
177
- docs = retriever.invoke(request.query)
178
- sources = [doc.metadata.get("source", "N/A") for doc in docs]
179
- return {
180
- "query": request.query,
181
- "response": respuesta.content,
182
- "intent": "UNIVERSIDAD",
183
- "sources": sources
184
- }
185
 
186
  except Exception as e:
187
  return {"error": f"Error al procesar la consulta: {e}"}
 
19
  os.makedirs(TEMP_CACHE_DIR, exist_ok=True)
20
 
21
  # --------------------------------------------------------
22
+ # 1. CONFIGURACIÓN Y PROMPTS
23
  # --------------------------------------------------------
24
  URL_FAISS = "https://drive.google.com/uc?export=download&id=1hiVycS4DQHO1MBdC-L_z1TXA6sJO_Y-r"
25
  URL_PKL = "https://drive.google.com/uc?export=download&id=1vbG8unx88Kb5jn7puGv1gqSM4S6rIUQC"
26
  DOWNLOAD_DIR = "/tmp/db_faiss"
27
  DB_FAISS_PATH = DOWNLOAD_DIR
28
 
29
+ # --- NUEVO: PROMPT PARA RE-ESCRIBIR LA PREGUNTA ---
30
+ CONDENSE_PROMPT = PromptTemplate(
31
+ template="""Dada la siguiente conversación y una pregunta de seguimiento, reescribe la pregunta de seguimiento para que sea una pregunta independiente que contenga todo el contexto, especialmente si se refiere a la UPT Aragua.
32
+
33
+ Historial:
34
+ {chat_history}
35
+ Pregunta de seguimiento: {question}
36
+ Pregunta independiente reescrita:""",
37
+ input_variables=["chat_history", "question"]
38
+ )
39
+
40
  INTENT_PROMPT = PromptTemplate(
41
+ template="""Eres un clasificador de intenciones para la UPT Aragua. Clasifica en: SALUDO, UNIVERSIDAD u OTRO.
42
+ Responde SOLO con la categoría.
 
 
 
 
 
 
 
 
43
  Mensaje: {query}
44
  Categoría:""",
45
  input_variables=["query"]
46
  )
47
 
48
  SALUDO_PROMPT = PromptTemplate(
49
+ template="""Eres UPTA bot, saluda amigablemente y menciona que puedes ayudar con info de la UPT Aragua.
 
50
  Mensaje: {query}
51
  Respuesta:""",
52
  input_variables=["query"]
53
  )
54
 
55
  RAG_PROMPT = PromptTemplate(
56
+ template="""Eres UPTA bot, experto de la UPT Aragua. Responde usando el contexto. Si no lo sabes, pide ser más específico.
57
+ Contexto: {context}
58
+ Pregunta: {question}
 
 
59
  Respuesta:""",
60
  input_variables=["context", "question"]
61
  )
62
 
63
  # --------------------------------------------------------
64
+ # 2. MODELOS DE DATOS
65
  # --------------------------------------------------------
66
  class QueryRequest(BaseModel):
67
  query: str
68
+ history: list = [] # Aquí recibiremos el historial desde Gradio
69
 
70
+ # --------------------------------------------------------
71
+ # 3. FUNCIONES DE CARGA
72
+ # --------------------------------------------------------
73
  def download_file(url, local_path):
 
 
74
  headers = {'User-Agent': 'Mozilla/5.0'}
75
+ response = requests.get(url, stream=True, headers=headers, timeout=30)
76
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
77
+ with open(local_path, 'wb') as f:
78
+ shutil.copyfileobj(response.raw, f)
 
 
 
 
 
 
 
79
 
80
  def load_and_configure_rag():
81
+ download_file(URL_FAISS, os.path.join(DOWNLOAD_DIR, 'index.faiss'))
82
+ download_file(URL_PKL, os.path.join(DOWNLOAD_DIR, 'index.pkl'))
83
+
84
+ embeddings = HuggingFaceEmbeddings(
85
+ model_name="sentence-transformers/all-MiniLM-L6-v2",
86
+ model_kwargs={'device': 'cpu'},
87
+ cache_folder=TEMP_CACHE_DIR
88
+ )
89
+ vectorstore = FAISS.load_local(DB_FAISS_PATH, embeddings, allow_dangerous_deserialization=True)
90
+
91
+ # Asegúrate de tener la variable de entorno GROQ_API_KEY configurada en Hugging Face
92
+ llm = ChatGroq(temperature=0.15, model_name="openai/gpt-oss-120b")
93
+
94
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
95
+
96
+ # Creamos todas las cadenas
97
+ condense_chain = CONDENSE_PROMPT | llm
98
+ intent_chain = INTENT_PROMPT | llm
99
+ saludo_chain = SALUDO_PROMPT | llm
100
+ rag_chain = (
101
+ {"context": retriever, "question": RunnablePassthrough()}
102
+ | RAG_PROMPT
103
+ | llm
104
+ )
105
+
106
+ return condense_chain, intent_chain, saludo_chain, rag_chain, retriever
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  # --------------------------------------------------------
109
+ # 4. API FASTAPI
110
  # --------------------------------------------------------
111
+ app = FastAPI()
112
+ condense_chain = intent_chain = saludo_chain = rag_chain = retriever = None
 
 
 
 
 
 
113
 
114
+ @app.on_event("startup")
115
+ async def startup_event():
116
+ global condense_chain, intent_chain, saludo_chain, rag_chain, retriever
117
+ condense_chain, intent_chain, saludo_chain, rag_chain, retriever = load_and_configure_rag()
 
118
 
119
  @app.post("/query")
120
  async def process_query(request: QueryRequest):
121
+ # 1. Convertir historial a texto
122
+ chat_str = ""
123
+ for user_msg, bot_msg in request.history:
124
+ chat_str += f"Usuario: {user_msg}\nBot: {bot_msg}\n"
125
+
126
+ # 2. Re-escribir consulta si hay historial
127
+ query_to_process = request.query
128
+ if request.history:
129
+ res = condense_chain.invoke({"chat_history": chat_str, "question": request.query})
130
+ query_to_process = res.content.strip()
131
+
132
+ # 3. Clasificar intención
133
+ intent_res = intent_chain.invoke({"query": query_to_process})
134
+ intent = intent_res.content.upper()
135
+
136
+ if "SALUDO" in intent:
137
+ resp = saludo_chain.invoke({"query": request.query})
138
+ return {"response": resp.content, "intent": "SALUDO"}
139
+
140
+ elif "OTRO" in intent:
141
+ return {"response": "Solo puedo ayudarte con temas de la UPT Aragua.", "intent": "OTRO"}
142
+
143
+ else:
144
+ # RAG con la consulta re-escrita
145
+ resp = rag_chain.invoke(query_to_process)
146
+ docs = retriever.invoke(query_to_process)
147
+ sources = list(set([doc.metadata.get("source", "N/A") for doc in docs]))
148
+ return {"response": resp.content, "intent": "UNIVERSIDAD", "sources": sources}
 
 
 
 
 
 
 
 
 
 
149
 
150
  except Exception as e:
151
  return {"error": f"Error al procesar la consulta: {e}"}