Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -31,26 +31,22 @@ ENRUTADOR_MODELOS = {
|
|
| 31 |
}
|
| 32 |
|
| 33 |
# ==========================================
|
| 34 |
-
# 2. LÓGICA DEL ORÁCULO
|
| 35 |
# ==========================================
|
| 36 |
def realizar_exegesis_web(consulta, canon_seleccionado):
|
| 37 |
modelo_asignado = ENRUTADOR_MODELOS[canon_seleccionado]
|
|
|
|
| 38 |
|
| 39 |
# ---------------------------------------------------------
|
| 40 |
-
#
|
| 41 |
# ---------------------------------------------------------
|
|
|
|
| 42 |
prompt_extraccion = f"""
|
| 43 |
Analiza la siguiente consulta del usuario. Extrae el autor o la obra mencionados y deduce las palabras clave temáticas.
|
| 44 |
Si no se menciona ningún autor u obra concreta, deja ese campo estrictamente vacío ("").
|
| 45 |
Responde ÚNICAMENTE con un objeto JSON válido, sin texto adicional.
|
| 46 |
-
|
| 47 |
Consulta: "{consulta}"
|
| 48 |
-
|
| 49 |
-
Formato esperado:
|
| 50 |
-
{{
|
| 51 |
-
"autor_obra": "Nombre del autor u obra (o vacío)",
|
| 52 |
-
"palabras_clave": "concepto 1, concepto 2, tema principal"
|
| 53 |
-
}}
|
| 54 |
"""
|
| 55 |
|
| 56 |
try:
|
|
@@ -62,7 +58,6 @@ def realizar_exegesis_web(consulta, canon_seleccionado):
|
|
| 62 |
],
|
| 63 |
temperature=0.1
|
| 64 |
)
|
| 65 |
-
|
| 66 |
texto_json = respuesta_analista.choices[0].message.content
|
| 67 |
match_json = re.search(r'\{.*\}', texto_json, re.DOTALL)
|
| 68 |
|
|
@@ -71,72 +66,96 @@ def realizar_exegesis_web(consulta, canon_seleccionado):
|
|
| 71 |
filtro_autor = datos.get("autor_obra", "").strip()
|
| 72 |
claves_busqueda = datos.get("palabras_clave", consulta).strip()
|
| 73 |
else:
|
| 74 |
-
filtro_autor = ""
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
|
| 77 |
except Exception as e:
|
| 78 |
-
filtro_autor = ""
|
| 79 |
-
|
| 80 |
|
| 81 |
-
#
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
# ---------------------------------------------------------
|
| 85 |
-
#
|
| 86 |
# ---------------------------------------------------------
|
| 87 |
-
|
| 88 |
texto_a_vectorizar = f"{claves_busqueda}. {consulta}"
|
| 89 |
vector_query = modelo_vectorial.encode(texto_a_vectorizar, convert_to_tensor=False).tolist()
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
# ---------------------------------------------------------
|
| 92 |
-
# FASE 3: EL ALMACÉN (Búsqueda Híbrida en Supabase)
|
| 93 |
-
# ---------------------------------------------------------
|
| 94 |
try:
|
| 95 |
respuesta_db = supabase.rpc(
|
| 96 |
"buscar_pasajes_clasicos",
|
| 97 |
{
|
| 98 |
"query_embedding": vector_query,
|
| 99 |
"match_threshold": 0.15,
|
| 100 |
-
"match_count":
|
| 101 |
"tabla_destino": canon_seleccionado,
|
| 102 |
"filtro_autor_obra": filtro_db
|
| 103 |
}
|
| 104 |
).execute()
|
| 105 |
|
| 106 |
pasajes = respuesta_db.data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
obra = frag.get("obra", "Desconocida")
|
| 114 |
-
texto = frag.get("texto_original", "")
|
| 115 |
-
contexto_documentos += f"\n[Documento {i+1} - Obra: {obra}]\n{texto}\n"
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
3. TRANSPARENCIA: Escribe tu proceso lógico dentro de etiquetas <razonamiento> y </razonamiento>.
|
| 127 |
-
|
| 128 |
-
FORMATO DE SALIDA ESPERADO:
|
| 129 |
-
<razonamiento>
|
| 130 |
-
(Tu proceso lógico aquí)
|
| 131 |
-
</razonamiento>
|
| 132 |
-
### 📌 Documento seleccionado: [Nombre de la obra]
|
| 133 |
-
> [Texto original del documento elegido]
|
| 134 |
-
---
|
| 135 |
-
[Tu exégesis final]
|
| 136 |
-
"""
|
| 137 |
-
|
| 138 |
-
prompt_usuario = f"Consulta del Usuario: {consulta}\n\nDocumentos recuperados de la base de datos:{contexto_documentos}"
|
| 139 |
|
|
|
|
|
|
|
|
|
|
| 140 |
response = cliente_ia.chat.completions.create(
|
| 141 |
model=modelo_asignado,
|
| 142 |
messages=[
|
|
@@ -148,49 +167,46 @@ def realizar_exegesis_web(consulta, canon_seleccionado):
|
|
| 148 |
|
| 149 |
respuesta_completa = response.choices[0].message.content
|
| 150 |
|
| 151 |
-
#
|
| 152 |
-
info_extraccion = f"**🔍 Diagnóstico del Agente Analista:**\n- **Autor/Obra Detectado:** `{filtro_autor if filtro_autor else 'Ninguno (Búsqueda global)'}`\n- **Claves Vectorizadas:** `{claves_busqueda}`\n\n---\n"
|
| 153 |
-
|
| 154 |
match = re.search(r'<razonamiento>(.*?)</razonamiento>', respuesta_completa, re.DOTALL | re.IGNORECASE)
|
| 155 |
if match:
|
| 156 |
-
texto_razonamiento =
|
| 157 |
texto_exegesis = respuesta_completa.replace(match.group(0), '').strip()
|
| 158 |
else:
|
| 159 |
-
texto_razonamiento =
|
| 160 |
texto_exegesis = respuesta_completa
|
| 161 |
|
| 162 |
return texto_exegesis, texto_razonamiento
|
| 163 |
|
| 164 |
except Exception as e:
|
| 165 |
-
return f"⚠️ Error
|
| 166 |
|
| 167 |
# ==========================================
|
| 168 |
-
# 3. INTERFAZ GRÁFICA
|
| 169 |
# ==========================================
|
| 170 |
with gr.Blocks() as demo:
|
| 171 |
-
gr.Markdown("# 🏛️ Socrates - Philological Oracle
|
| 172 |
-
gr.Markdown("Agentic RAG System:
|
| 173 |
|
| 174 |
with gr.Row():
|
| 175 |
with gr.Column(scale=1):
|
| 176 |
canon_dropdown = gr.Dropdown(
|
| 177 |
choices=["textos_clasicos_latin_v3", "textos_clasicos_griego_v3", "textos_clasicos_chino_v3"],
|
| 178 |
value="textos_clasicos_latin_v3",
|
| 179 |
-
label="📚 Canon
|
| 180 |
)
|
| 181 |
-
# Hemos eliminado la caja de Filtro Manual; la IA lo hace sola ahora
|
| 182 |
consulta_input = gr.Textbox(
|
| 183 |
-
label="✍️ Query / Consulta
|
| 184 |
-
placeholder="Ej: La
|
| 185 |
lines=4
|
| 186 |
)
|
| 187 |
boton_consultar = gr.Button("Invoke / Invocar / 召喚", variant="primary")
|
| 188 |
|
| 189 |
with gr.Column(scale=2):
|
| 190 |
-
with gr.Accordion("🧠 Reasoning Trace
|
| 191 |
salida_razonamiento = gr.Markdown(label="")
|
| 192 |
|
| 193 |
-
salida_markdown = gr.Markdown(label="Exegesis
|
| 194 |
|
| 195 |
boton_consultar.click(
|
| 196 |
fn=realizar_exegesis_web,
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
# ==========================================
|
| 34 |
+
# 2. LÓGICA DEL ORÁCULO (ENJAMBRE TRANSPARENTE)
|
| 35 |
# ==========================================
|
| 36 |
def realizar_exegesis_web(consulta, canon_seleccionado):
|
| 37 |
modelo_asignado = ENRUTADOR_MODELOS[canon_seleccionado]
|
| 38 |
+
bitacora_agentes = [] # Aquí guardaremos el rastro de razonamiento transparente
|
| 39 |
|
| 40 |
# ---------------------------------------------------------
|
| 41 |
+
# AGENTE 1: ANALISTA (Extracción de Entidades)
|
| 42 |
# ---------------------------------------------------------
|
| 43 |
+
bitacora_agentes.append("🕵️ **Agente 1 (Analista):** Iniciando disección de la consulta...")
|
| 44 |
prompt_extraccion = f"""
|
| 45 |
Analiza la siguiente consulta del usuario. Extrae el autor o la obra mencionados y deduce las palabras clave temáticas.
|
| 46 |
Si no se menciona ningún autor u obra concreta, deja ese campo estrictamente vacío ("").
|
| 47 |
Responde ÚNICAMENTE con un objeto JSON válido, sin texto adicional.
|
|
|
|
| 48 |
Consulta: "{consulta}"
|
| 49 |
+
Formato esperado: {{"autor_obra": "Nombre", "palabras_clave": "concepto 1, concepto 2"}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
"""
|
| 51 |
|
| 52 |
try:
|
|
|
|
| 58 |
],
|
| 59 |
temperature=0.1
|
| 60 |
)
|
|
|
|
| 61 |
texto_json = respuesta_analista.choices[0].message.content
|
| 62 |
match_json = re.search(r'\{.*\}', texto_json, re.DOTALL)
|
| 63 |
|
|
|
|
| 66 |
filtro_autor = datos.get("autor_obra", "").strip()
|
| 67 |
claves_busqueda = datos.get("palabras_clave", consulta).strip()
|
| 68 |
else:
|
| 69 |
+
filtro_autor, claves_busqueda = "", consulta
|
| 70 |
+
|
| 71 |
+
bitacora_agentes.append(f" ↳ *Autor detectado:* `{filtro_autor if filtro_autor else 'Ninguno'}`")
|
| 72 |
+
bitacora_agentes.append(f" ↳ *Claves vectoriales:* `{claves_busqueda}`")
|
| 73 |
|
| 74 |
except Exception as e:
|
| 75 |
+
filtro_autor, claves_busqueda = "", consulta
|
| 76 |
+
bitacora_agentes.append(f" ⚠️ *Error del Agente 1:* {e}. Usando texto completo.")
|
| 77 |
|
| 78 |
+
# ---------------------------------------------------------
|
| 79 |
+
# AGENTE 2: VERIFICADOR DE ARCHIVOS (Búsqueda Determinista SQL)
|
| 80 |
+
# ---------------------------------------------------------
|
| 81 |
+
bitacora_agentes.append("\n🗄️ **Agente 2 (Archivero):** Verificando inventario en Supabase...")
|
| 82 |
+
conteo_autor = 0
|
| 83 |
+
if filtro_autor:
|
| 84 |
+
try:
|
| 85 |
+
# Hacemos un conteo exacto de pasajes que coinciden parcialmente con el nombre usando 'ilike'
|
| 86 |
+
res_count = supabase.table(canon_seleccionado)\
|
| 87 |
+
.select("id", count="exact")\
|
| 88 |
+
.ilike("obra", f"%{filtro_autor}%")\
|
| 89 |
+
.execute()
|
| 90 |
+
|
| 91 |
+
conteo_autor = res_count.count if res_count.count else 0
|
| 92 |
+
bitacora_agentes.append(f" ↳ ✅ **Éxito:** Se han localizado **{conteo_autor}** pasajes catalogados bajo '{filtro_autor}' en la tabla `{canon_seleccionado}`.")
|
| 93 |
+
|
| 94 |
+
if conteo_autor == 0:
|
| 95 |
+
bitacora_agentes.append(" ↳ ⚠️ *Aviso:* El autor no existe en la base de datos o está escrito de forma diferente (ej. 'Salustio' vs 'Cayo Salustio').")
|
| 96 |
+
except Exception as e:
|
| 97 |
+
bitacora_agentes.append(f" ↳ ⚠️ Error al contar pasajes: {e}")
|
| 98 |
+
else:
|
| 99 |
+
bitacora_agentes.append(" ↳ Búsqueda global autorizada (no se restringió a un autor específico).")
|
| 100 |
|
| 101 |
# ---------------------------------------------------------
|
| 102 |
+
# AGENTE 3: EL MATEMÁTICO (Vectorización y Similitud)
|
| 103 |
# ---------------------------------------------------------
|
| 104 |
+
bitacora_agentes.append("\n📐 **Agente 3 (Matemático):** Trazando coordenadas semánticas...")
|
| 105 |
texto_a_vectorizar = f"{claves_busqueda}. {consulta}"
|
| 106 |
vector_query = modelo_vectorial.encode(texto_a_vectorizar, convert_to_tensor=False).tolist()
|
| 107 |
+
|
| 108 |
+
filtro_db = filtro_autor if filtro_autor != "" else None
|
| 109 |
+
pasajes = []
|
| 110 |
|
|
|
|
|
|
|
|
|
|
| 111 |
try:
|
| 112 |
respuesta_db = supabase.rpc(
|
| 113 |
"buscar_pasajes_clasicos",
|
| 114 |
{
|
| 115 |
"query_embedding": vector_query,
|
| 116 |
"match_threshold": 0.15,
|
| 117 |
+
"match_count": 5,
|
| 118 |
"tabla_destino": canon_seleccionado,
|
| 119 |
"filtro_autor_obra": filtro_db
|
| 120 |
}
|
| 121 |
).execute()
|
| 122 |
|
| 123 |
pasajes = respuesta_db.data
|
| 124 |
+
if pasajes:
|
| 125 |
+
bitacora_agentes.append(f" ↳ ✅ **Éxito:** Se han rescatado **{len(pasajes)}** pasajes que superan el umbral de similitud semántica (0.15).")
|
| 126 |
+
else:
|
| 127 |
+
bitacora_agentes.append(f" ↳ ❌ **Fallo semántico:** Aunque hay {conteo_autor} pasajes del autor, ninguno de ellos coincide conceptualmente con las coordenadas de tus palabras clave. La distancia matemática es demasiado grande.")
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
bitacora_agentes.append(f" ↳ ⚠️ Error en la búsqueda vectorial: {e}")
|
| 131 |
|
| 132 |
+
# ---------------------------------------------------------
|
| 133 |
+
# AGENTE 4: EL FILÓLOGO (Explicador Transparente y Exégeta)
|
| 134 |
+
# ---------------------------------------------------------
|
| 135 |
+
bitacora_agentes.append("\n✍️ **Agente 4 (Filólogo):** Redactando respuesta final basada en la evidencia...")
|
| 136 |
+
|
| 137 |
+
contexto_documentos = ""
|
| 138 |
+
for i, frag in enumerate(pasajes or []):
|
| 139 |
+
obra = frag.get("obra", "Desconocida")
|
| 140 |
+
texto = frag.get("texto_original", "")
|
| 141 |
+
contexto_documentos += f"\n[Documento {i+1} - Obra: {obra}]\n{texto}\n"
|
| 142 |
|
| 143 |
+
# Convertimos la bitácora en texto para pasársela al LLM y que entienda todo lo que ha pasado
|
| 144 |
+
historial_transparente = "\n".join(bitacora_agentes)
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
prompt_sistema = """
|
| 147 |
+
Eres el Oráculo Filológico, el agente final de un enjambre de IA.
|
| 148 |
+
|
| 149 |
+
INSTRUCCIONES VITALES:
|
| 150 |
+
1. IDIOMA: Responde EXCLUSIVAMENTE en el idioma de la "Consulta del Usuario" (Español o Chino Tradicional).
|
| 151 |
+
2. TRANSPARENCIA: Lee el "Historial de los Agentes". Si no se encontraron documentos, DEBES explicarle al usuario amablemente qué pasó (por ejemplo: "El Agente Archivero encontró X textos del autor, pero el Agente Matemático determinó que ninguno habla de tus palabras clave...").
|
| 152 |
+
3. EXÉGESIS: Si hay documentos recuperados, analízalos rigurosamente en relación con la consulta.
|
| 153 |
+
4. Escribe tu proceso lógico final dentro de etiquetas <razonamiento> y </razonamiento>.
|
| 154 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
+
prompt_usuario = f"Consulta del Usuario: {consulta}\n\n=== Historial de los Agentes (Para tu conocimiento) ===\n{historial_transparente}\n\n=== Documentos Recuperados ===\n{contexto_documentos if pasajes else 'NINGUNO'}"
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
response = cliente_ia.chat.completions.create(
|
| 160 |
model=modelo_asignado,
|
| 161 |
messages=[
|
|
|
|
| 167 |
|
| 168 |
respuesta_completa = response.choices[0].message.content
|
| 169 |
|
| 170 |
+
# Juntamos la bitácora técnica con el razonamiento del modelo para la interfaz
|
|
|
|
|
|
|
| 171 |
match = re.search(r'<razonamiento>(.*?)</razonamiento>', respuesta_completa, re.DOTALL | re.IGNORECASE)
|
| 172 |
if match:
|
| 173 |
+
texto_razonamiento = historial_transparente + "\n\n**💭 Reflexión final del Filólogo:**\n" + match.group(1).strip()
|
| 174 |
texto_exegesis = respuesta_completa.replace(match.group(0), '').strip()
|
| 175 |
else:
|
| 176 |
+
texto_razonamiento = historial_transparente + "\n\n**💭 Reflexión final del Filólogo:**\nNo se generaron etiquetas de razonamiento."
|
| 177 |
texto_exegesis = respuesta_completa
|
| 178 |
|
| 179 |
return texto_exegesis, texto_razonamiento
|
| 180 |
|
| 181 |
except Exception as e:
|
| 182 |
+
return f"⚠️ Error en la fase de exégesis: {str(e)}", historial_transparente
|
| 183 |
|
| 184 |
# ==========================================
|
| 185 |
+
# 3. INTERFAZ GRÁFICA
|
| 186 |
# ==========================================
|
| 187 |
with gr.Blocks() as demo:
|
| 188 |
+
gr.Markdown("# 🏛️ Socrates - Philological Oracle")
|
| 189 |
+
gr.Markdown("Agentic RAG System: *Glass-Box Swarm Architecture* (Transparencia Total)")
|
| 190 |
|
| 191 |
with gr.Row():
|
| 192 |
with gr.Column(scale=1):
|
| 193 |
canon_dropdown = gr.Dropdown(
|
| 194 |
choices=["textos_clasicos_latin_v3", "textos_clasicos_griego_v3", "textos_clasicos_chino_v3"],
|
| 195 |
value="textos_clasicos_latin_v3",
|
| 196 |
+
label="📚 Canon"
|
| 197 |
)
|
|
|
|
| 198 |
consulta_input = gr.Textbox(
|
| 199 |
+
label="✍️ Query / Consulta",
|
| 200 |
+
placeholder="Ej: La decadencia moral en Salustio...",
|
| 201 |
lines=4
|
| 202 |
)
|
| 203 |
boton_consultar = gr.Button("Invoke / Invocar / 召喚", variant="primary")
|
| 204 |
|
| 205 |
with gr.Column(scale=2):
|
| 206 |
+
with gr.Accordion("🧠 Reasoning Trace (Bitácora del Enjambre) / 系統推理", open=True):
|
| 207 |
salida_razonamiento = gr.Markdown(label="")
|
| 208 |
|
| 209 |
+
salida_markdown = gr.Markdown(label="Exegesis")
|
| 210 |
|
| 211 |
boton_consultar.click(
|
| 212 |
fn=realizar_exegesis_web,
|