Agente de Planificación y Ejecución (Plan-and-Execute Agent) especializado en realizar investigación historiográfica y etimológica especializada
El propósito central de este agente de Agente de Planificación y Ejecución (Plan-and-Execute Agent) es realizar investigación historiográfica y etimológica especializada (específicamente en toponimia pirenaica, como sugiere la pregunta de ejemplo), siguiendo un plan estructurado, utilizando múltiples herramientas de consulta de bases de datos y refinando continuamente una hipótesis central hasta alcanzar un alto nivel de certeza.
1. Arquitectura General y Modelos LLM
Este script establece un flujo de trabajo cíclico donde el agente planifica, ejecuta el paso y luego replanifica, todo mientras registra meticulosamente el proceso.
Modelos y Roles
El sistema utiliza dos modelos de lenguaje grandes (LLMs), cada uno con un rol específico para maximizar la eficiencia y precisión:
- GPT-4o (para Planificación y Replanificación): Utilizado para las tareas de razonamiento de alto nivel (
plan_stepyreplan_step). Se le pide generar un output estructurado usando Pydantic (PlanoAct), asegurando que la salida sea lógica y predecible. - Deepseek (para Ejecución y Uso de Herramientas): Utilizado para el nodo de ejecución (
execute_step). Este modelo actúa como el Agente ReAct (Reasoning and Acting) que decide qué herramientas especializadas usar para cumplir el paso del plan.
Herramientas Especializadas
El agente está armado con una vasta colección de herramientas específicas, lo que lo convierte en un experto en su dominio:
- Herramientas Externas:
wikipedia_tool,ddg_news_tool(para búsquedas generales). - Herramienta Humana:
request_human_feedback_tool(permite solicitar la intervención o validación de un usuario en un paso crítico). - Herramientas RAG Específicas: Una larga lista de agentes de recuperación de información (RAG) especializados en bases de datos etimológicas, históricas y geográficas (ej:
georgia_agent,hierro_agent,anatolia_agent,vasciberaqui_agent, etc.).
2. El Corazón del Agente: El Estado y la Hipótesis
La estructura PlanExecute (Estado) es el motor que impulsa el flujo. No solo almacena el input y el plan, sino que incluye la lógica de persistencia de hipótesis, que es la parte más innovadora del código:
| Campo | Propósito | Explicación |
main_hypothesis | Rastreo | Lista de las raíces etimológicas o teorías principales identificadas hasta ahora. |
hypothesis_certainty | Condición | Mide la confianza del agente en la hipótesis actual («alta», «media», «baja»). Este valor controla la lógica del flujo. |
past_steps | Memoria | Historial de las tareas ejecutadas y sus resultados. |
log_filename | Auditoría | Nombre del archivo Markdown donde se registra la traza de ejecución. |
Lógica de Restricción Crítica (Dynamic Prompt)
La función generate_agent_system_message es crucial. Genera el prompt del sistema de forma dinámica para el Agente y el Planner:
- Define el rol experto del agente (Investigador Lingüístico/Historiográfico).
- Impone un formato de salida estricto en la respuesta del agente, exigiendo las líneas
Grado de Certeza: [alta/media/baja]yHipótesis Principal: [Lista de elementos]. - Aplica una Restricción Lógica: Si el estado indica que la
hypothesis_certaintyes"alta", el prompt del sistema se actualiza para prohibir que el agente formule nuevas hipótesis. Su única tarea restante es verificar, refutar rivales o concluir, lo que fuerza al agente a cerrar el ciclo de investigación.
3. Nodos del Workflow y Extracción de Datos
El flujo de trabajo es una cadena de nodos que se ejecuta en bucle hasta que se encuentra una respuesta final:
A. execute_step (La Ejecución y Extracción)
Este nodo es el más complejo:
- Toma el primer
taskdelplan. - Invoca al Agente ReAct (Deepseek) con las herramientas para ejecutar la tarea.
- Aplica una lógica de extracción por Regex a la respuesta del agente. Busca las líneas de
Grado de Certeza:yHipótesis Principal:para actualizar el estado. - Lógica de Sobrescritura: Solo actualiza la hipótesis y la certeza si la nueva certeza detectada es igual o mayor que la certeza actual, asegurando que el agente no retroceda en sus hallazgos.
B. replan_step (Decisión y Replanificación)
Este nodo es la puerta de salida o la continuación del bucle:
- Invoca al GPT-4o para que revise el progreso.
- Recibe una estructura
Act, que puede ser unResponse(si el trabajo ha terminado) o un nuevoPlan(si se necesita más investigación). - La lógica aquí también es sensible a la certeza: si la certeza es «alta», el prompt se ajusta para presionar al modelo a generar una
Response(respuesta final) y terminar el flujo.
C. log_to_file
Este nodo (ejecutado después de cada paso principal) es un sistema de auditoría y trazabilidad. Escribe la salida de cada nodo en un archivo Markdown, creando una traza completa de la toma de decisiones del agente.
D. should_end (Condición)
Una función simple que determina el siguiente paso:
- END: Si se ha generado una respuesta final (
state.get("response")es verdadero) o si el plan está vacío. - «agent»: Si aún hay pasos pendientes en el plan.
4. Ejecución Asíncrona
El bloque final utiliza asyncio.run(run_planner_agent(...)) para iniciar el motor LangGraph. Esto es necesario porque app.astream() es una función asíncrona que genera eventos en tiempo real (streaming). El código itera sobre estos eventos, registrando el progreso y finalmente imprimiendo la final_response capturada.
En resumen, este script no es solo un agente, sino un pipeline de investigación autosuficiente que utiliza la inteligencia de dos modelos LLM para planificar, ejecutar y refinar una investigación compleja y especializada, con un mecanismo incorporado para saber cuándo detenerse (al alcanzar la «certeza alta»).
import asyncio
import logging
import operator
import os
import re
from datetime import datetime
from typing import TypedDict , List , Union , Annotated , Tuple
from langchain.schema import SystemMessage , HumanMessage
from langchain_core.prompts import ChatPromptTemplate , MessagesPlaceholder
from langgraph.constants import END , START
from langgraph.graph import StateGraph
from langgraph.prebuilt import create_react_agent
from pydantic import BaseModel , Field
# --- ASUNCIONES DE IMPORTACIONES REALES ---
try:
from agent_service_toolkit.retrievers.grafos.draw_graph import draw_graph_with_graphviz
from agent_service_toolkit.retrievers.vall_aran_hypotesis_flow.vall_global_utils import generar_edge_labels , \
merge_graph_and_legend
from agent_service_toolkit.tools.registry.external_rools.external_tool_functions import wikipedia_tool , \
ddg_news_tool
from agent_service_toolkit.tools.registry.rag_tools.rag_registry import agent_tools
from src.agent_service_toolkit.core.load_ret_llm_models import get_model
from agent_service_toolkit.tools.registry.function_tools.request_human_feedback.request_human_feedback_tool import \
request_human_feedback_tool
except ImportError as e:
logging.warning ( f"🚨 Advertencia de Importación: Algunas herramientas o utilidades no están disponibles. {e}" )
# Definiciones dummy para que el código compile si faltan tus módulos
def dummy_tool(query: str) -> str:
return f"Dummy result for {query}"
wikipedia_tool = dummy_tool
ddg_news_tool = dummy_tool
request_human_feedback_tool = dummy_tool
agent_tools = {}
# --- Tool and Model Setup ---
logging.basicConfig ( level = logging.INFO , format = '%(asctime)s - %(levelname)s - %(message)s' )
# 1. Preparar herramientas
deepseek_model_loaded = get_model ( "Deepseek" )
gpt4o_model_loaded = get_model ( "gpt-4o" )
if deepseek_model_loaded and gpt4o_model_loaded:
try:
planner_tools_list = [ wikipedia_tool , ddg_news_tool , request_human_feedback_tool ]
# Agregamos todas las herramientas RAG disponibles
rag_keys = [
"georgia_agent" , "etimology_agent" , "hierro_agent" , "bronce_agent" , "armenia_agent" , "anatolia_agent" ,
"calcolitic_agent" , "mesolitic_agent" , "neolitic_agent" , "vall_ager_agent" , "vasciberaqui_agent" ,
"vall_aran_agent" , "iberos_agent" , "celtas_agent" , "indoeuropa_agent" , "Tradition_agent" ]
for key in rag_keys:
planner_tools_list.extend ( agent_tools.get ( key , [ ] ) )
planner_tools = planner_tools_list
logging.info ( f"[Setup] {len ( planner_tools )} Herramientas cargadas (RAG + Externas)." )
except Exception as e:
logging.error ( f"[Setup] Fallo al cargar herramientas RAG/Personalizadas: {e}. Usando solo las básicas." )
planner_tools = [ wikipedia_tool , ddg_news_tool , request_human_feedback_tool ]
else:
logging.error ( "⛔ ERROR: Modelos LLM no cargados. El agente fallará en la ejecución." )
planner_tools = [ ]
# --- Pydantic Data Structures ---
class Plan ( BaseModel ):
"""Plan inicial con pasos a seguir"""
steps: List[ str ] = Field ( description = "Pasos ordenados para resolver la tarea" )
class Response ( BaseModel ):
"""Respuesta final al usuario"""
response: str
class Act ( BaseModel ):
"""Acción a realizar: responder o planificar."""
action: Union[ Response , Plan ]
class PlanExecute ( TypedDict ):
"""
Estado compartido en el workflow. Incluye persistencia de la hipótesis etimológica.
"""
input: str
plan: List[ str ]
past_steps: Annotated[ List[ Tuple ] , operator.add ]
response: str
log_filename: str
last_node_output: str
# 🔑 CAMBIO CLAVE: main_hypothesis ahora es una lista para múltiples entradas
hypothesis_certainty: str # "alta", "media", "baja"
main_hypothesis: List[ str ] # Lista de las hipótesis principales
# --- LLM and Agent Definitions ---
deepseek_model = deepseek_model_loaded
gpt4o_model = gpt4o_model_loaded
if not deepseek_model or not gpt4o_model:
def dummy_runnable(state):
return {"messages": [ HumanMessage ( content = "Dummy response" ) ]}
planner_agent = dummy_runnable
planner_runnable = dummy_runnable
replanner_runnable = dummy_runnable
else:
# --- PROMPT TEMPLATE GENERAL (Para inyección de contexto) ---
def generate_agent_system_message(state: PlanExecute) -> str:
"""Genera el mensaje del sistema dinámico para los prompts de Agent y Planner."""
base_message = (
"Eres un **Investigador Lingüístico e Historiográfico experto en etimología y toponimia pirenaica**. "
"Tu tarea es ejecutar un paso de un plan mayor relacionado con el estudio diacrónico de nombres de lugar, "
"raíces lingüísticas y patrones de asentamiento en el ámbito pirenaico. "
"Utiliza tus herramientas para obtener información detallada. **Enfoca tu respuesta en las evidencias filológicas, "
"arqueológicas, paleográficas o geográficas** que sustenten o refuten el paso de la tarea, asegurando la **precisión semántica y cronológica**."
# 📢 INSTRUCCIÓN CRÍTICA DE FORMATO MEJORADA Y DESTACADA
"\n\n🚨 **INSTRUCCIÓN DE PERSISTENCIA:** Si tu respuesta incluye la identificación de etimologías principales, "
"DEBES terminar tu respuesta con estas dos líneas de forma obligatoria. Si hay múltiples, USA UN GUION POR CADA UNA (Formato de lista Markdown):"
"\n**Grado de Certeza:** [alta/media/baja]"
"\n**Hipótesis Principal:** "
"\n- [La primera raíz etimológica y su significado. Ej: Prerromana BERG- ('aprisco/corral')]"
"\n- [La segunda raíz etimológica y su significado. Ej: Preindoeuropea IBER- ('agua/río')]"
)
# 🔑 Lógica de Restricción
if state.get ( "hypothesis_certainty" ) == "alta":
# Formateamos la lista de hipótesis para la restricción
hypotheses_str = "\n".join ( f"- {h}" for h in state[ 'main_hypothesis' ] )
base_message += (
"\n\n**RESTRICCIÓN CRÍTICA:** Ya se encontró una o varias hipótesis de ALTA CERTEZA: "
f"\n{hypotheses_str}\nA partir de ahora, NO DEBES FORMULAR NUEVAS HIPÓTESIS. "
"Tu única función es: 1) Verificar la hipótesis contra fuentes históricas, 2) Refutar hipótesis rivales, o 3) Concluir la tarea. "
)
return base_message
# 1. Tool-Using Agent (for EXECUTION)
planner_agent = create_react_agent (
model = deepseek_model ,
tools = planner_tools ,
prompt = ChatPromptTemplate.from_messages (
[
SystemMessage (
content = "Este es un placeholder, el SystemMessage real se inyecta en execute_step." ) ,
MessagesPlaceholder ( variable_name = "messages" )
] ) ,
name = "planner_agent"
)
# --- Funciones de los nodos ---
async def plan_step(state: PlanExecute):
"""Genera un plan inicial y lo prepara para el log."""
logging.info ( " [Node: planner] Iniciando Planificación inicial..." )
system_message_content = generate_agent_system_message ( state )
current_planner_runnable = (
ChatPromptTemplate.from_messages (
[
SystemMessage (
content = system_message_content + "\n\nTu único propósito es generar un plan de investigación estructurado." ) ,
MessagesPlaceholder ( variable_name = "messages" )
] )
| gpt4o_model.with_structured_output ( Plan )
)
plan_obj = await current_planner_runnable.ainvoke ( {"messages": [ HumanMessage ( content = state[ "input" ] ) ]} )
if isinstance ( plan_obj , Plan ):
plan_steps = plan_obj.steps
output_str = "## 📝 Plan Inicial Generado\n\n" + "\n".join (
f"{i + 1}. {step}" for i , step in enumerate ( plan_steps ) )
return {"plan": plan_steps , "last_node_output": output_str}
else:
output_str = f"## ❌ Error de Planificación\n\nNo se pudo generar un Plan estructurado. Tipo recibido: {type ( plan_obj )}"
return {"plan": [ ] , "last_node_output": output_str}
async def execute_step(state: PlanExecute):
"""Ejecuta el primer paso del plan, extrae la certeza y prepara la salida para el log."""
logging.info ( " [Node: agent] Iniciando Ejecución de paso..." )
plan = state[ "plan" ]
if not plan: return {"response": "No hay pasos en el plan."}
task = plan[ 0 ]
system_message_content = generate_agent_system_message ( state )
if not planner_agent:
response_content = "El modelo de ejecución no está cargado. Paso simulado."
remaining_plan = plan[ 1: ]
else:
plan_str = "\n".join ( f"{i + 1}. {s}" for i , s in enumerate ( plan ) )
task_formatted = f"For the following plan:\n{plan_str}\n\nExecute step 1: {task}"
messages_to_agent = [
SystemMessage ( content = system_message_content ) ,
HumanMessage ( content = task_formatted )
]
agent_response = await planner_agent.ainvoke ( {"messages": messages_to_agent} )
remaining_plan = plan[ 1: ]
response_content = agent_response[ "messages" ][ -1 ].content if agent_response[
"messages" ] else "Execution complete."
# 🔑 Lógica de EXTRACCIÓN de Hipótesis y Certeza (Adaptada a Listas)
new_hypothesis = state.get ( "main_hypothesis" , [ "Aún no establecida" ] ) # Ahora una lista
new_certainty = state.get ( "hypothesis_certainty" , "baja" )
# 1. Búsqueda de Certeza
match_certainty = re.search ( r"Grado de Certeza:\s*(alta|media|baja)" , response_content , re.IGNORECASE )
# 2. Búsqueda de Hipótesis Principal: Buscamos la línea "Hipótesis Principal:"
match_hypothesis_start = re.search ( r"Hipótesis Principal:\s*\n" , response_content , re.IGNORECASE )
if match_certainty and match_hypothesis_start:
potential_certainty = match_certainty.group ( 1 ).lower ( )
# 2b. Extraer todas las líneas que comienzan con un guion después del inicio de la hipótesis
start_index = match_hypothesis_start.end ( )
# Buscamos todas las líneas que inician con '-' desde el punto de inicio
potential_hypotheses = re.findall ( r"^- (.*)" , response_content[ start_index: ] , re.MULTILINE )
if potential_hypotheses:
# Filtramos hipótesis vacías y verificamos la longitud mínima (relevancia)
valid_hypotheses = [ h.strip ( ) for h in potential_hypotheses if len ( h.strip ( ) ) > 15 ]
if valid_hypotheses:
# Mapa para comparar niveles de certeza
certainty_order = {"baja": 1 , "media": 2 , "alta": 3}
current_certainty_level = certainty_order.get ( new_certainty , 1 )
potential_certainty_level = certainty_order.get ( potential_certainty , 1 )
# Solo actualizamos si la nueva certeza es igual o mayor
if potential_certainty_level >= current_certainty_level:
new_certainty = potential_certainty
new_hypothesis = valid_hypotheses # Guardamos la lista completa de hipótesis válidas
logging.info (
f"✅ [Extractor] {len ( new_hypothesis )} Hipótesis guardadas con certeza {new_certainty.upper ( )}." )
else:
logging.warning (
f"⚠️ [Extractor] Nueva hipótesis de menor certeza ({potential_certainty}) ignorada." )
else:
logging.warning ( "⚠️ [Extractor] Hipótesis detectadas pero son demasiado cortas/irrelevantes." )
# Si la extracción falla o es irrelevante, el estado se mantiene como estaba.
# Formato de salida para el log (usando guiones ya que new_hypothesis es una lista)
hypothesis_output = "\n".join ( f"- {h}" for h in new_hypothesis )
output_str = (
f"## ➡️ Paso Ejecutado\n"
f"**Tarea:** `{task}`\n"
f"**Certeza Guardada:** `{new_certainty.upper ( )}`\n"
f"**Hipótesis Guardada:**\n{hypothesis_output}\n\n" # Formato de salida modificado
f"**Resultado Completo:**\n```markdown\n{response_content}\n```\n"
)
return {
"plan": remaining_plan ,
"past_steps": [ (task , response_content) ] ,
"last_node_output": output_str ,
"main_hypothesis": new_hypothesis , # Retorna la lista
"hypothesis_certainty": new_certainty
}
async def replan_step(state: PlanExecute):
"""Revisa el plan, decide si continuar/responder y prepara la salida para el log."""
logging.info ( " [Node: replan] Iniciando Decisión/Replanificación..." )
system_message_content = generate_agent_system_message ( state )
# 🔑 Lógica de RESTRICCIÓN para el replanner
restriction_message = (
"Revisa el plan original, la tarea inicial y los pasos ya ejecutados en el contexto de la evolución de nombres de lugar. "
"- **Si la respuesta final está completa y se basa en una sólida evidencia filológica y cronológica**, devuelve una respuesta final (Response). "
"- **Si el plan es incompleto o la información obtenida es insuficiente**, genera SOLO los pasos pendientes o un nuevo Plan que aborde la brecha de información. "
"- Tu respuesta DEBE ser una de las dos estructuras Pydantic (Response o Plan)."
)
if state.get ( "hypothesis_certainty" ) == "alta":
restriction_message = (
f"RESTRICCIÓN CRÍTICA: La hipótesis principal ya tiene CERTEZA ALTA. "
f"Si el plan restante solo verifica hechos menores o la información es suficiente, DEBES devolver la estructura RESPONSE. "
f"NO generes un nuevo Plan si la certeza es alta."
) + restriction_message
current_replanner_runnable = (
ChatPromptTemplate.from_messages (
[
SystemMessage ( content = system_message_content ) ,
SystemMessage ( content = restriction_message ) ,
MessagesPlaceholder ( variable_name = "messages" )
] )
| gpt4o_model.with_structured_output ( Act )
)
# Adaptación para la lista de hipótesis en el contexto
current_hypotheses = "\n".join ( f"- {h}" for h in state.get ( "main_hypothesis" , [ "N/A" ] ) )
past_steps_summary = "\n".join ( f" - {step}: {result[ :50 ]}..." for step , result in state[ 'past_steps' ] )
context_messages = [
SystemMessage ( content = f"Tarea Inicial: {state[ 'input' ]}" ) ,
SystemMessage (
content = f"Hipótesis actual (Certeza {state[ 'hypothesis_certainty' ].upper ( )}):\n{current_hypotheses}" ) ,
SystemMessage ( content = f"Plan Restante: {state[ 'plan' ]}" ) ,
SystemMessage ( content = f"Pasos Completados:\n{past_steps_summary}" )
]
output = await current_replanner_runnable.ainvoke ( {"messages": context_messages} )
output_str = ""
if isinstance ( output.action , Response ):
output_str = f"## 🛑 Decisión Final: Responder\n\n**Respuesta Final:**\n{output.action.response}"
return {"response": output.action.response , "last_node_output": output_str}
elif isinstance ( output.action , Plan ):
new_plan_steps = output.action.steps
output_str = "## 🔄 Decisión: Replanificar\n\n**Nuevo Plan:**\n" + "\n".join (
f"{i + 1}. {step}" for i , step in enumerate ( new_plan_steps ) )
return {"plan": new_plan_steps , "last_node_output": output_str}
else:
output_str = "## ❌ Error de Replanificación\n\nNo se produjo una estructura Act válida."
return {"response": "Error: Planificación inválida o nula." , "last_node_output": output_str}
# --- NODO DE LOGGING A ARCHIVO / CONDICIONAL ---
def log_to_file(state: PlanExecute):
"""Escribe la salida del nodo anterior en un archivo Markdown."""
reports_dir = "informes_trace"
os.makedirs ( reports_dir , exist_ok = True )
log_filename = state.get ( "log_filename" )
if not log_filename:
timestamp = datetime.now ( ).strftime ( "%Y%m%d_%H%M%S" )
log_filename = os.path.join ( reports_dir , f"trace_{timestamp}.md" )
with open ( log_filename , "w" , encoding = "utf-8" ) as f:
f.write ( f"# 📊 Traza de Ejecución del Workflow\n" )
f.write ( f"## 📥 Tarea Inicial\n{state[ 'input' ]}\n\n---\n" )
logging.info ( f"💾 [LogToFile] Archivo de traza creado: {log_filename}" )
if state.get ( "last_node_output" ):
output_to_write = state[ "last_node_output" ]
with open ( log_filename , "a" , encoding = "utf-8" ) as f:
f.write ( f"\n\n# 🕒 {datetime.now ( ).strftime ( '%H:%M:%S' )} - Salida del Nodo\n\n" )
f.write ( output_to_write )
f.write ( "\n\n---\n" )
logging.info ( f"💾 [LogToFile] Salida del nodo registrada en {log_filename}." )
return {"log_filename": log_filename , "last_node_output": ""}
def should_end(state: PlanExecute):
"""Condición de finalización, llamada DESPUÉS del log_replan."""
if state.get ( "response" ):
logging.info ( "[Condition] Terminando: Respuesta final generada." )
return END
if not state.get ( "plan" ):
logging.info ( "[Condition] Terminando: Plan vacío y sin respuesta final." )
return END
logging.info ( "[Condition] Continuando: Se necesita el siguiente paso ('agent')." )
return "agent"
# --- Construcción del workflow ---
workflow = StateGraph ( PlanExecute )
workflow.add_node ( "planner" , plan_step )
workflow.add_node ( "agent" , execute_step )
workflow.add_node ( "replan" , replan_step )
workflow.add_node ( "log_plan" , log_to_file )
workflow.add_node ( "log_exec" , log_to_file )
workflow.add_node ( "log_replan" , log_to_file )
# 1. Flujo Inicial: START -> planner -> log_plan -> agent
workflow.add_edge ( START , "planner" )
workflow.add_edge ( "planner" , "log_plan" )
workflow.add_edge ( "log_plan" , "agent" )
# 2. Bucle de Ejecución: agent -> log_exec -> replan
workflow.add_edge ( "agent" , "log_exec" )
workflow.add_edge ( "log_exec" , "replan" )
# 3. Decisión y Bucle: replan -> log_replan -> condicional (agent/END)
workflow.add_edge ( "replan" , "log_replan" )
workflow.add_conditional_edges (
"log_replan" ,
should_end ,
{
"agent": "agent" ,
END: END
}
)
# --- Compilación ---
app = workflow.compile ( )
logging.info ( "[Workflow] Workflow compilado con éxito." )
# --- Ejecución asíncrona ---
async def run_planner_agent(question):
"""Define y ejecuta el flujo asíncrono del LangGraph."""
inputs = {
"input": question ,
"past_steps": [ ] ,
"log_filename": "" ,
"last_node_output": "" ,
# 🔑 Valores iniciales para los nuevos campos
"hypothesis_certainty": "baja" ,
"main_hypothesis": [ "Aún no establecida" ] # Ahora una lista
}
config = {"recursion_limit": 50}
logging.info ( f"[Execution] Iniciando para la tarea: {inputs[ 'input' ]}" )
logging.info ( "-" * 50 )
logging.info ( "--- LangGraph Trace ---" )
final_response = None
# LÓGICA DE EJECUCIÓN
async for event in app.astream ( inputs , config = config ):
for k , v in event.items ( ):
if k == "__end__":
logging.info ( "-" * 50 )
logging.info ( "[Execution] Evento FINALIZADO." )
if v.get ( 'response' ):
final_response = v[ 'response' ]
break
if k in [ "planner" , "agent" , "replan" ]:
logging.info ( f"[Stream] 🔄 {k.upper ( )} - Nodo ejecutado." )
elif k in [ "log_plan" , "log_exec" , "log_replan" ]:
logging.info ( f"[Stream] 💾 LOGGING ({k}): Registrando salida en archivo Markdown." )
logging.info ( "[Execution] Ejecución finalizada." )
if final_response:
print ( "\n" + "=" * 80 )
print ( " ✨ RESPUESTA FINAL DEL AGENTE (Capturado del estado final) ✨" )
print ( "=" * 80 )
print ( final_response )
print ( "=" * 80 + "\n" )
return final_response
if __name__ == "__main__":
# Pregunta de investigación
RESEARCH_QUESTION = "Analiza la etimología del topónimo 'Ager' desde sus raíces, y cita las fuentes documentales más antiguas. Tiene origen vascoaquitano o indoeuropeo?"
# Ejecución asíncrona
asyncio.run ( run_planner_agent ( question = RESEARCH_QUESTION ) )
# --- Visualization ---
try:
edge_labels = generar_edge_labels ( app.get_graph ( ) )
draw_graph_with_graphviz ( app , "planner_completo" , edge_labels = edge_labels )
merge_graph_and_legend (
main_graph_path = "grafos/planner_completo.png" ,
legend_path = "grafos/planner_completo_legend.png" ,
output_path = "grafos/planner_pipeline_completo_with_legend.png" ,
padding = 20 ,
)
logging.info ( "\n[Visualization] Visualización del grafo generada en 'grafos/' folder." )
except Exception as e:
logging.error (
f"\n[Visualization] No se pudo generar la visualización del grafo. {e}"
)







Pla de Beret y el enigma ibérico: etimología, Ebro y las dos Iberias – sanchezpares.com
[…] Basado en el modelo investigación historiográfica y etimológica especializada: […]
Ager: etimología, fuentes antiguas y el cruce de lenguas ancestrales – sanchezpares.com
[…] Basado en el modelo investigación historiográfica y etimológica especializada: […]