En las partes anteriores creamos un chatbot con herramientas (Parte 1) y memoria a corto plazo dentro de un hilo (Parte 2). Ahora damos dos pasos más: **memoria a largo plazo** (entre hilos) para recordar información entre conversaciones, y el patrón **human-in-the-loop** para pausar el grafo y pedir aprobación humana.
⚠️ Este capítulo continúa el código de las partes anteriores (Parte 1 · Parte 2).
📚 **Esta entrada es parte de la serie _Guía completa de LangGraph_**, dividida en cuatro capítulos que se leen en orden:
> * Parte 1: Chatbot básico y herramientas
* Parte 2: Memoria a corto plazo
* 👉 **Parte 3: Memoria a largo plazo y human-in-the-loop**
* Parte 4: Personalización del estado y checkpoints
Memoria a largo plazo, memoria entre hilos
La memoria es una función cognitiva que permite a las personas almacenar, recuperar y utilizar información para comprender, a partir de su pasado, su presente y su futuro.
Existen varios tipos de memoria a largo plazo que pueden utilizarse en aplicaciones de IA.
Introducción a LangGraph Memory Store
LangGraph proporciona el LangGraph Memory Store, que es una forma de guardar y recuperar memoria a largo plazo entre diferentes hilos. De esta manera, en una conversación, un usuario puede indicar que le gusta algo, y en otra conversación, el chatbot puede recuperar esa información para generar una respuesta más personalizada.
Se trata de una clase para almacenes persistentes de clave-valor (key-value).
Cuando se almacenan objetos en la memoria se necesitan tres cosas:
- Un
namespacepara el objeto, se hace mediante unatupla - Una
keyúnica - El
valordel objeto
Vamos a ver un ejemplo
InputPythonimport uuidfrom langgraph.store.memory import InMemoryStorein_memory_store = InMemoryStore()# Namespace for the memory to saveuser_id = "1"namespace_for_memory = (user_id, "memories")# Save a memory to namespace as key and valuekey = str(uuid.uuid4())# The value needs to be a dictionaryvalue = {"food_preference" : "I like pizza"}# Save the memoryin_memory_store.put(namespace_for_memory, key, value)Copied
El objeto in_memory_store que hemos creado tiene varios métodos y uno de ellos es search, que nos permite buscar por namespace
InputPython# Searchmemories = in_memory_store.search(namespace_for_memory)type(memories), len(memories)Copied
(list, 1)
Es una lista de un único valor, lo cual es lógico, porque solo hemos almacenado un valor, así que vamos a verlo
InputPythonvalue = memories[0]value.dict()Copied
{'namespace': ['1', 'memories'],'key': '70006131-948a-4d7a-bdce-78351c44fc4d','value': {'food_preference': 'I like pizza'},'created_at': '2025-05-11T07:24:31.462465+00:00','updated_at': '2025-05-11T07:24:31.462468+00:00','score': None}
Podemos ver su key y su value
InputPython# The key, valuememories[0].key, memories[0].valueCopied
('70006131-948a-4d7a-bdce-78351c44fc4d', {'food_preference': 'I like pizza'})
También podemos usar el método get para obtener un objeto de la memoria a partir de su namespace y su key
InputPython# Get the memory by namespace and keymemory = in_memory_store.get(namespace_for_memory, key)memory.dict()Copied
{'namespace': ['1', 'memories'],'key': '70006131-948a-4d7a-bdce-78351c44fc4d','value': {'food_preference': 'I like pizza'},'created_at': '2025-05-11T07:24:31.462465+00:00','updated_at': '2025-05-11T07:24:31.462468+00:00'}
Al igual que para la memoria a corto plazo hemos usado los checkpoints, para la memoria a largo plazo vamos a usar LangGraph Store
Chatbot con memoria a largo plazo
Creamos un chatbot básico, con memoria a largo plazo y memoria a corto plazo.
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom huggingface_hub import loginfrom IPython.display import Image, displayimport osimport dotenvdotenv.load_dotenv()HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")class State(TypedDict):messages: Annotated[list, add_messages]os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLM modellogin(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the modelMODEL = "Qwen/Qwen2.5-72B-Instruct"model = HuggingFaceEndpoint(repo_id=MODEL,task="text-generation",max_new_tokens=512,do_sample=False,repetition_penalty=1.03,)# Create the chat modelllm = ChatHuggingFace(llm=model)# Chatbot instructionMODEL_SYSTEM_MESSAGE = """You are a helpful assistant that can answer questions and help with tasks.You have access to a long-term memory that you can use to answer questions and help with tasks.Here is the memory (it may be empty): {memory}"""# Create new memory from the chat history and any existing memoryCREATE_MEMORY_INSTRUCTION = """You are a helpful assistant that gets information from the user to personalize your responses.# INFORMATION FROM THE USER:{memory}# INSTRUCTIONS:1. Carefully review the chat history2. Identify new information from the user, such as:- Personal details (name, location)- Preferences (likes, dislikes)- Interests and hobbies- Past experiences- Goals or future plans3. Combine any new information with the existing memory4. Format the memory as a clear, bulleted list5. If new information conflicts with existing memory, keep the most recent versionRemember: Only include factual information directly stated by the user. Do not make assumptions or inferences.Based on the chat history below, please update the user information:"""# Nodesdef call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Load memory from the store and use it to personalize the chatbot's response."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memory", user_id)key = "user_memory"existing_memory = store.get(namespace, key)# Extract the actual memory content if it exists and add a prefixif existing_memory:# Value is a dictionary with a memory keyexisting_memory_content = existing_memory.value.get('memory')else:existing_memory_content = "No existing memory found."if isinstance(existing_memory_content, str):print(f" [Call model debug] Existing memory: {existing_memory_content}")else:print(f" [Call model debug] Existing memory: {existing_memory_content.content}")# Format the memory in the system promptsystem_msg = MODEL_SYSTEM_MESSAGE.format(memory=existing_memory_content)# Respond using memory as well as the chat historyresponse = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])return {"messages": response}def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Reflect on the chat history and save a memory to the store."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve existing memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Extract the memoryif existing_memory:existing_memory_content = existing_memory.value.get('memory')else:existing_memory_content = "No existing memory found."if isinstance(existing_memory_content, str):print(f" [Write memory debug] Existing memory: {existing_memory_content}")else:print(f" [Write memory debug] Existing memory: {existing_memory_content.content}")# Format the memory in the system promptsystem_msg = CREATE_MEMORY_INSTRUCTION.format(memory=existing_memory_content)new_memory = llm.invoke([SystemMessage(content=system_msg)]+state['messages'])if isinstance(new_memory, str):print(f" [Write memory debug] New memory: {new_memory}")else:print(f" [Write memory debug] New memory: {new_memory.content}")# Overwrite the existing memory in the storekey = "user_memory"# Write value as a dictionary with a memory keystore.put(namespace, key, {"memory": new_memory.content})# Create graph buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_builder.add_edge(START, "call_model")graph_builder.add_edge("call_model", "write_memory")graph_builder.add_edge("write_memory", END)# Store for long-term (across-thread) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Vamos a probarlo
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================Hi, my name is Maximo[Call model debug] Existing memory: No existing memory found.================================== Ai Message ==================================Hello Maximo! It's nice to meet you. How can I assist you today?[Write memory debug] Existing memory: No existing memory found.[Write memory debug] New memory:Here's the updated information I have about you:- Name: Maximo
InputPython# User inputinput_messages = [HumanMessage(content="I like to bike around San Francisco")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I like to bike around San Francisco[Call model debug] Existing memory:Here's the updated information I have about you:- Name: Maximo================================== Ai Message ==================================That sounds like a great way to explore the city! San Francisco has some fantastic biking routes. Are there any specific areas or routes you enjoy biking the most, or are you looking for some new recommendations?[Write memory debug] Existing memory:Here's the updated information I have about you:- Name: Maximo[Write memory debug] New memory:Here's the updated information about you:- Name: Maximo- Location: San Francisco- Interest: Biking around San Francisco
Si recuperamos la memoria a largo plazo
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.dict()Copied
{'namespace': ['memory', '1'],'key': 'user_memory','value': {'memory': " Here's the updated information about you: - Name: Maximo - Location: San Francisco - Interest: Biking around San Francisco"},'created_at': '2025-05-11T09:41:26.739207+00:00','updated_at': '2025-05-11T09:41:26.739211+00:00'}
Obtenemos su valor
InputPythonprint(existing_memory.value.get('memory'))Copied
Here's the updated information about you:- Name: Maximo- Location: San Francisco- Interest: Biking around San Francisco
Ahora podemos empezar un nuevo hilo de conversación, pero con la misma memoria a largo plazo. Veremos que el chatbot recuerda la información del usuario.
InputPython# We supply a user ID for across-thread memory as well as a new thread IDconfig = {"configurable": {"thread_id": "2", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi! Where would you recommend that I go biking?")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================Hi! Where would you recommend that I go biking?[Call model debug] Existing memory:Here's the updated information about you:- Name: Maximo- Location: San Francisco- Interest: Biking around San Francisco================================== Ai Message ==================================Hi there! Given my interest in biking around San Francisco, I'd recommend a few great routes:1. **Golden Gate Park**: This is a fantastic place to bike, with wide paths that are separated from vehicle traffic. You can start at the eastern end near Stow Lake and bike all the way to the western end at Ocean Beach. There are plenty of scenic spots to stop and enjoy along the way.2. **The Embarcadero**: This route follows the waterfront from Fisherman’s Wharf to the Bay Bridge. It’s relatively flat and offers beautiful views of the San Francisco Bay and the city skyline. You can also stop by the Ferry Building for some delicious food and drinks.3. **Presidio**: The Presidio is a large park with numerous trails that offer diverse landscapes, from forests to coastal bluffs. The Crissy Field area is especially popular for its views of the Golden Gate Bridge.4. **Golden Gate Bridge**: Riding across the Golden Gate Bridge is a must-do experience. You can start from the San Francisco side, bike across the bridge, and then continue into Marin County for a longer ride with stunning views.5. **Lombard Street**: While not a long ride, biking down the famous crooked section of Lombard Street can be a fun and memorable experience. Just be prepared for the steep hill on the way back up!Each of these routes offers a unique experience, so you can choose based on your interests and the type of scenery you enjoy. Happy biking![Write memory debug] Existing memory:Here's the updated information about you:- Name: Maximo- Location: San Francisco- Interest: Biking around San Francisco[Write memory debug] New memory: 😊Let me know if you have any other questions or if you need more recommendations!
He abierto un nuevo hilo de conversación, le he preguntado dónde podría ir a montar en bici, ha recordado que le había dicho que me gusta ir a montar en bici por San Francisco y me ha respondido con lugares de San Francisco a los que podría ir
Chatbot con perfil de usuario
Nota: Este apartado lo vamos a hacer usando Sonnet 3.7, ya que la integración de HuggingFace no tiene la funcionalidad de
with_structured_outputque proporciona una salida estructurada con una estructura definida.
Podemos crear tipados para que el LLM genere una salida con una estructura definida por nosotros.
Vamos a crear un tipado para el perfil de usuario.
InputPythonfrom typing import TypedDict, Listclass UserProfile(TypedDict):"""User profile schema with typed fields"""user_name: str # The user's preferred nameinterests: List[str] # A list of the user's interestsCopied
Ahora volvemos a crear el grafo, pero ahora con el tipado UserProfile
Vamos a usar with_structured_output para que el LLM genere una salida con una estructura definida por nosotros, esa estructura la vamos a definir con la clase Subjects que es una clase de tipo BaseModel de Pydantic.
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_anthropic import ChatAnthropicfrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom IPython.display import Image, displayfrom pydantic import BaseModel, Fieldimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")class State(TypedDict):messages: Annotated[list, add_messages]os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)llm_with_structured_output = llm.with_structured_output(UserProfile)# Chatbot instructionMODEL_SYSTEM_MESSAGE = """You are a helpful assistant with memory that provides information about the user.If you have memory for this user, use it to personalize your responses.Here is the memory (it may be empty): {memory}"""# Create new memory from the chat history and any existing memoryCREATE_MEMORY_INSTRUCTION = """Create or update a user profile memory based on the user's chat history.This will be saved for long-term memory. If there is an existing memory, simply update it.Here is the existing memory (it may be empty): {memory}"""# Nodesdef call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Load memory from the store and use it to personalize the chatbot's response."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Format the memories for the system promptif existing_memory and existing_memory.value:memory_dict = existing_memory.valueformatted_memory = (f"Name: {memory_dict.get('user_name', 'Unknown')} "f"Interests: {', '.join(memory_dict.get('interests', []))}")else:formatted_memory = None# if isinstance(existing_memory_content, str):print(f" [Call model debug] Existing memory: {formatted_memory}")# else:# print(f" [Call model debug] Existing memory: {existing_memory_content.content}")# Format the memory in the system promptsystem_msg = MODEL_SYSTEM_MESSAGE.format(memory=formatted_memory)# Respond using memory as well as the chat historyresponse = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])return {"messages": response}def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Reflect on the chat history and save a memory to the store."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve existing memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Format the memories for the system promptif existing_memory and existing_memory.value:memory_dict = existing_memory.valueformatted_memory = (f"Name: {memory_dict.get('user_name', 'Unknown')} "f"Interests: {', '.join(memory_dict.get('interests', []))}")else:formatted_memory = Noneprint(f" [Write memory debug] Existing memory: {formatted_memory}")# Format the existing memory in the instructionsystem_msg = CREATE_MEMORY_INSTRUCTION.format(memory=formatted_memory)# Invoke the model to produce structured output that matches the schemanew_memory = llm_with_structured_output.invoke([SystemMessage(content=system_msg)]+state['messages'])print(f" [Write memory debug] New memory: {new_memory}")# Overwrite the existing use profile memorykey = "user_memory"store.put(namespace, key, new_memory)# Create graph buildergraph_builder = StateGraph(MessagesState)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_builder.add_edge(START, "call_model")graph_builder.add_edge("call_model", "write_memory")graph_builder.add_edge("write_memory", END)# Store for long-term (across-thread) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Ejecutamos el grafo
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo and I like to bike around Madrid and eat salads.")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================Hi, my name is Maximo and I like to bike around Madrid and eat salads.[Call model debug] Existing memory: None================================== Ai Message ==================================Hello Maximo! It's nice to meet you. I see you enjoy biking around Madrid and eating salads - those are great healthy habits! Madrid has some beautiful areas to explore by bike, and the city has been improving its cycling infrastructure in recent years.Is there anything specific about Madrid's cycling routes or perhaps some good places to find delicious salads in the city that you'd like to know more about? I'd be happy to help with any questions you might have.[Write memory debug] Existing memory: None[Write memory debug] New memory: {'user_name': 'Maximo', 'interests': ['biking', 'Madrid', 'salads']}
Como vemos, el LLM ha generado una salida con la estructura definida por nosotros.
Vamos a ver cómo se ha guardado la memoria a largo plazo.
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.valueCopied
{'user_name': 'Maximo', 'interests': ['biking', 'Madrid', 'salads']}
Más
Actualizar esquemas estructurados con Trustcall
En el ejemplo anterior, hemos creado perfiles de usuario con datos estructurados
En realidad, lo que se hace por debajo, es regenerar el perfil de usuario en cada interacción. Lo cual genera un gasto innecesario de tokens y puede hacer que se pierda información importante del perfil del usuario.
Así que para solucionarlo vamos a usar la librería TrustCall, que es una librería open source para actualizar esquemas JSON. Cuando tiene que actualizar un esquema JSON, lo hace de manera incremental, es decir, no borra el esquema anterior, sino que va añadiendo los nuevos campos.
Vamos a crear un ejemplo de conversación para ver cómo funciona.
InputPythonfrom langchain_core.messages import HumanMessage, AIMessage# Conversationconversation = [HumanMessage(content="Hi, I'm Maximo."),AIMessage(content="Nice to meet you, Maximo."),HumanMessage(content="I really like playing soccer.")]Copied
Creamos un esquema estructurado y un modelo de LLM
InputPythonfrom pydantic import BaseModel, Fieldfrom typing import List# Schemaclass UserProfile(BaseModel):"""User profile schema with typed fields"""user_name: str = Field(description="The user's preferred name")interests: List[str] = Field(description="A list of the user's interests")from langchain_anthropic import ChatAnthropicimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)Copied
Utilizamos la función create_extractor de trustcall para crear un extractor de datos estructurados
InputPythonfrom trustcall import create_extractor# Create the extractortrustcall_extractor = create_extractor(llm,tools=[UserProfile],tool_choice="UserProfile")Copied
Como se puede ver, al método trustcall_extractor se le da un llm, que va a usarse como motor de búsqueda
Extrajimos los datos estructurados
InputPythonfrom langchain_core.messages import SystemMessage# Instructionsystem_msg = "Extract the user profile from the following conversation"# Invoke the extractorresult = trustcall_extractor.invoke({"messages": [SystemMessage(content=system_msg)]+conversation})resultCopied
{'messages': [AIMessage(content=[{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'input': {'user_name': 'Maximo', 'interests': ['soccer']}, 'name': 'UserProfile', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_01TEB3FeDKLAeHJtbKo5noyW', 'model': 'claude-3-7-sonnet-20250219', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 497, 'output_tokens': 56}, 'model_name': 'claude-3-7-sonnet-20250219'}, id='run-8a15289b-fd39-4a2d-878a-fa6feaa805c5-0', tool_calls=[{'name': 'UserProfile', 'args': {'user_name': 'Maximo', 'interests': ['soccer']}, 'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'type': 'tool_call'}], usage_metadata={'input_tokens': 497, 'output_tokens': 56, 'total_tokens': 553, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}})],'responses': [UserProfile(user_name='Maximo', interests=['soccer'])],'response_metadata': [{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY'}],'attempts': 1}
Vamos a ver los mensajes que se han generado para extraer los datos estructurados
InputPythonfor m in result["messages"]:m.pretty_print()Copied
================================== Ai Message ==================================[{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'input': {'user_name': 'Maximo', 'interests': ['soccer']}, 'name': 'UserProfile', 'type': 'tool_use'}]Tool Calls:UserProfile (toolu_01WfgbD1fG3rJYAXGrjqjfVY)Call ID: toolu_01WfgbD1fG3rJYAXGrjqjfVYArgs:user_name: Maximointerests: ['soccer']
El esquema de UserProfile se ha actualizado con el nuevo dato.
InputPythonschema = result["responses"]schemaCopied
[UserProfile(user_name='Maximo', interests=['soccer'])]
Como vemos, el esquema es una lista, vamos a ver el tipo de dato de su único elemento
InputPythontype(schema[0])Copied
__main__.UserProfile
Podemos convertirlo a un diccionario con model_dump
InputPythonschema[0].model_dump()Copied
{'user_name': 'Maximo', 'interests': ['soccer']}
Gracias a haberle dado un LLM a trustcall_extractor, podemos pedirle qué queremos que extraiga
Vamos a simular que continúa la conversación para ver cómo se actualiza el esquema
InputPython# Update the conversationupdated_conversation = [HumanMessage(content="Hi, I'm Maximo."),AIMessage(content="Nice to meet you, Maximo."),HumanMessage(content="I really like playing soccer."),AIMessage(content="It is great to play soccer! Where do you go after playing soccer?"),HumanMessage(content="I really like to go to a bakery after playing soccer."),]Copied
Le pedimos al modelo que actualice el esquema (un JSON) mediante la librería trustcall
InputPython# Update the instructionsystem_msg = f"""Update the memory (JSON doc) to incorporate new information from the following conversation"""# Invoke the extractor with the updated instruction and existing profile with the corresponding tool name (UserProfile)result = trustcall_extractor.invoke({"messages": [SystemMessage(content=system_msg)]+updated_conversation},{"existing": {"UserProfile": schema[0].model_dump()}})resultCopied
{'messages': [AIMessage(content=[{'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb', 'input': {'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}, 'name': 'UserProfile', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_01RYUJvCdzL4b8kBYKo4BtQf', 'model': 'claude-3-7-sonnet-20250219', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 538, 'output_tokens': 60}, 'model_name': 'claude-3-7-sonnet-20250219'}, id='run-06994472-5ba0-46cc-a512-5fcacce283fc-0', tool_calls=[{'name': 'UserProfile', 'args': {'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}, 'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb', 'type': 'tool_call'}], usage_metadata={'input_tokens': 538, 'output_tokens': 60, 'total_tokens': 598, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}})],'responses': [UserProfile(user_name='Maximo', interests=['soccer', 'bakeries'])],'response_metadata': [{'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb'}],'attempts': 1}
Vamos a ver los mensajes que se han generado para actualizar el esquema
InputPythonfor m in result["messages"]:m.pretty_print()Copied
================================== Ai Message ==================================[{'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb', 'input': {'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}, 'name': 'UserProfile', 'type': 'tool_use'}]Tool Calls:UserProfile (toolu_01K1zTh33kXDAw1h18Yh2HBb)Call ID: toolu_01K1zTh33kXDAw1h18Yh2HBbArgs:user_name: Maximointerests: ['soccer', 'bakeries']
Vemos el esquema actualizado
InputPythonupdated_schema = result["responses"][0]updated_schema.model_dump()Copied
{'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}
Chatbot con perfil de usuario actualizado con Trustcall
Volvemos a crear el grafo que actualiza el perfil de usuario, pero ahora con la librería trustcall
InputPythonfrom pydantic import BaseModel, Fieldfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_anthropic import ChatAnthropicfrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom IPython.display import Image, displayfrom pydantic import BaseModel, Fieldimport osimport dotenvfrom trustcall import create_extractordotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Schemaclass UserProfile(BaseModel):""" Profile of a user """user_name: str = Field(description="The user's preferred name")user_location: str = Field(description="The user's location")interests: list = Field(description="A list of the user's interests")# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)# Create the extractortrustcall_extractor = create_extractor(llm,tools=[UserProfile],tool_choice="UserProfile", # Enforces use of the UserProfile tool)# Chatbot instructionMODEL_SYSTEM_MESSAGE = """You are a helpful assistant with memory that provides information about the user.If you have memory for this user, use it to personalize your responses.Here is the memory (it may be empty): {memory}"""# Create new memory from the chat history and any existing memoryTRUSTCALL_INSTRUCTION = """Create or update the memory (JSON doc) to incorporate information from the following conversation:"""# Nodesdef call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Load memory from the store and use it to personalize the chatbot's response.""""""Load memory from the store and use it to personalize the chatbot's response."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Format the memories for the system promptif existing_memory and existing_memory.value:memory_dict = existing_memory.valueformatted_memory = (f"Name: {memory_dict.get('user_name', 'Unknown')} "f"Location: {memory_dict.get('user_location', 'Unknown')} "f"Interests: {', '.join(memory_dict.get('interests', []))}")else:formatted_memory = Noneprint(f" [Call model debug] Existing memory: {formatted_memory}")# Format the memory in the system promptsystem_msg = MODEL_SYSTEM_MESSAGE.format(memory=formatted_memory)# Respond using memory as well as the chat historyresponse = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])return {"messages": response}def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Reflect on the chat history and save a memory to the store."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve existing memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Get the profile as the value from the list, and convert it to a JSON docexisting_profile = {"UserProfile": existing_memory.value} if existing_memory else Noneprint(f" [Write memory debug] Existing profile: {existing_profile}")# Invoke the extractorresult = trustcall_extractor.invoke({"messages": [SystemMessage(content=TRUSTCALL_INSTRUCTION)]+state["messages"], "existing": existing_profile})# Get the updated profile as a JSON objectupdated_profile = result["responses"][0].model_dump()print(f" [Write memory debug] Updated profile: {updated_profile}")# Save the updated profilekey = "user_memory"store.put(namespace, key, updated_profile)# Create graph buildergraph_builder = StateGraph(MessagesState)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_builder.add_edge(START, "call_model")graph_builder.add_edge("call_model", "write_memory")graph_builder.add_edge("write_memory", END)# Store for long-term (across-thread) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Iniciamos la conversación
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================Hi, my name is Maximo[Call model debug] Existing memory: None================================== Ai Message ==================================Hello Maximo! It's nice to meet you. How can I help you today? Whether you have questions, need information, or just want to chat, I'm here to assist you. Is there something specific you'd like to talk about?[Write memory debug] Existing profile: None[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': '<UNKNOWN>', 'interests': []}
Como vemos, no sabe ni la localización ni los intereses del usuario. Vamos a actualizar el perfil del usuario.
InputPython# User inputinput_messages = [HumanMessage(content="I like to play soccer and I live in Madrid")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I like to play soccer and I live in Madrid[Call model debug] Existing memory: Name: MaximoLocation: <UNKNOWN>Interests:================================== Ai Message ==================================Hello Maximo! It's great to learn that you live in Madrid and enjoy playing soccer. Madrid is a fantastic city with a rich soccer culture, being home to world-famous clubs like Real Madrid and Atlético Madrid.Soccer is truly a way of life in Spain, so you're in a perfect location for your interest. Do you support any particular team in Madrid? Or perhaps you enjoy playing soccer recreationally in the city's parks and facilities?Is there anything specific about Madrid or soccer you'd like to discuss further?[Write memory debug] Existing profile: {'UserProfile': {'user_name': 'Maximo', 'user_location': '<UNKNOWN>', 'interests': []}}[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}
Ha actualizado el perfil con la localización y los intereses del usuario
Vamos a ver la memoria actualizada
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.dict()Copied
{'namespace': ['memory', '1'],'key': 'user_memory','value': {'user_name': 'Maximo','user_location': 'Madrid','interests': ['soccer']},'created_at': '2025-05-12T17:35:03.583258+00:00','updated_at': '2025-05-12T17:35:03.583259+00:00'}
Vemos el esquema con el perfil del usuario actualizado
InputPython# The user profile saved as a JSON objectexisting_memory.valueCopied
{'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}
Vamos a añadir un nuevo interés del usuario
InputPython# User inputinput_messages = [HumanMessage(content="I also like to play basketball")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I also like to play basketball[Call model debug] Existing memory: Name: MaximoLocation: MadridInterests: soccer================================== Ai Message ==================================That's great to know, Maximo! It's nice that you enjoy both soccer and basketball. Basketball is also quite popular in Spain, with Liga ACB being one of the strongest basketball leagues in Europe.In Madrid, you have the opportunity to follow Real Madrid's basketball section, which is one of the most successful basketball teams in Europe. The city offers plenty of courts and facilities where you can play basketball too.Do you play basketball casually with friends, or are you part of any local leagues in Madrid? And how do you balance your time between soccer and basketball?[Write memory debug] Existing profile: {'UserProfile': {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}}[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer', 'basketball']}
Volvemos a ver la memoria actualizada
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.valueCopied
{'user_name': 'Maximo','user_location': 'Madrid','interests': ['soccer', 'basketball']}
Ha añadido bien el nuevo interés del usuario.
Con esta memoria a largo plazo guardada, podemos iniciar un nuevo hilo y el chatbot tendrá acceso a nuestro perfil actualizado.
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "2", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="What soccer players do you recommend for me?")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================What soccer players do you recommend for me?[Call model debug] Existing memory: Name: MaximoLocation: MadridInterests: soccer, basketball================================== Ai Message ==================================Based on your interest in soccer, I can recommend some players who might appeal to you. Since you're from Madrid, you might already follow Real Madrid or Atlético Madrid players, but here are some recommendations:From La Liga:- Vinícius Júnior and Jude Bellingham (Real Madrid)- Antoine Griezmann (Atlético Madrid)- Robert Lewandowski (Barcelona)- Lamine Yamal (Barcelona's young talent)International stars:- Kylian Mbappé- Erling Haaland- Mohamed Salah- Kevin De BruyneYou might also enjoy watching players with creative playing styles since you're interested in basketball as well, which is a sport that values creativity and flair - players like Rodrigo De Paul or João Félix.Is there a particular league or playing style you prefer in soccer?[Write memory debug] Existing profile: {'UserProfile': {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer', 'basketball']}}[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer', 'basketball']}
Como sabe que vivo en Madrid, primero me ha sugerido jugadores de fútbol de la Liga española. Y luego me ha sugerido jugadores de otras ligas
Chatbot con colecciones de documentos de usuario actualizadas con Trustcall
Otro enfoque es que en vez de guardar en un solo documento el perfil del usuario, guardar una colección de documentos, de esta manera no estamos atados a un solo esquema cerrado
Vamos a ver cómo hacerlo
InputPythonfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langchain_anthropic import ChatAnthropicfrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langchain_core.messages import merge_message_runsfrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom IPython.display import Image, displayfrom trustcall import create_extractorfrom pydantic import BaseModel, Fieldimport uuidimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Memory schemaclass Memory(BaseModel):"""A memory item representing a piece of information learned about the user."""content: str = Field(description="The main content of the memory. For example: User expressed interest in learning about French.")# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)# Create the extractortrustcall_extractor = create_extractor(llm,tools=[Memory],tool_choice="Memory",# This allows the extractor to insert new memoriesenable_inserts=True,)# Chatbot instructionMODEL_SYSTEM_MESSAGE = """You are a helpful chatbot. You are designed to be a companion to a user.You have a long term memory which keeps track of information you learn about the user over time.Current Memory (may include updated memories from this conversation):{memory}"""# Create new memory from the chat history and any existing memoryTRUSTCALL_INSTRUCTION = """Reflect on following interaction.Use the provided tools to retain any necessary memories about the user.Use parallel tool calling to handle updates and insertions simultaneously:"""# Nodesdef call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Load memory from the store and use it to personalize the chatbot's response."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memories", user_id)memories = store.search(namespace)print(f" [Call model debug] Memories: {memories}")# Format the memories for the system promptinfo = " ".join(f"- {mem.value['content']}" for mem in memories)system_msg = MODEL_SYSTEM_MESSAGE.format(memory=info)# Respond using memory as well as the chat historyresponse = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])return {"messages": response}def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):"""Reflect on the chat history and save a memory to the store."""# Get the user ID from the configuser_id = config["configurable"]["user_id"]# Define the namespace for the memoriesnamespace = ("memories", user_id)# Retrieve the most recent memories for contextexisting_items = store.search(namespace)# Format the existing memories for the Trustcall extractortool_name = "Memory"existing_memories = ([(existing_item.key, tool_name, existing_item.value)for existing_item in existing_items]if existing_itemselse None)print(f" [Write memory debug] Existing memories: {existing_memories}")# Merge the chat history and the instructionupdated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION)] + state["messages"]))# Invoke the extractorresult = trustcall_extractor.invoke({"messages": updated_messages,"existing": existing_memories})# Save the memories from Trustcall to the storefor r, rmeta in zip(result["responses"], result["response_metadata"]):store.put(namespace,rmeta.get("json_doc_id", str(uuid.uuid4())),r.model_dump(mode="json"),)print(f" [Write memory debug] Saved memories: {result['responses']}")# Create graph buildergraph_builder = StateGraph(MessagesState)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_builder.add_edge(START, "call_model")graph_builder.add_edge("call_model", "write_memory")graph_builder.add_edge("write_memory", END)# Store for long-term (across-thread) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Empezamos una nueva conversación
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================Hi, my name is Maximo[Call model debug] Memories: []================================== Ai Message ==================================Hello Maximo! It's nice to meet you. I'm your companion chatbot, here to chat, help answer questions, or just be someone to talk to.I'll remember your name is Maximo for our future conversations. What would you like to talk about today? How are you doing?[Write memory debug] Existing memories: None[Write memory debug] Saved memories: [Memory(content="User's name is Maximo.")]
Añadimos un nuevo interés del usuario
InputPython# User inputinput_messages = [HumanMessage(content="I like to play soccer")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I like to play soccer[Call model debug] Memories: [Item(namespace=['memories', '1'], key='6d06c4f5-3a74-46b2-92b4-1e29ba128c90', value={'content': "User's name is Maximo."}, created_at='2025-05-12T18:32:38.070902+00:00', updated_at='2025-05-12T18:32:38.070903+00:00', score=None)]================================== Ai Message ==================================That's great to know, Maximo! Soccer is such a wonderful sport. Do you play on a team, or more casually with friends? I'd also be curious to know what position you typically play, or if you have a favorite professional team you follow. I'll remember that you enjoy soccer for our future conversations.[Write memory debug] Existing memories: [('6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'Memory', {'content': "User's name is Maximo."})][Write memory debug] Saved memories: [Memory(content='User enjoys playing soccer.')]
Como vemos, se ha añadido el nuevo interés del usuario a la memoria.
Vamos a ver la memoria actualizada
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memories", user_id)memories = long_term_memory.search(namespace)for m in memories:print(m.dict())Copied
{'namespace': ['memories', '1'], 'key': '6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'value': {'content': "User's name is Maximo."}, 'created_at': '2025-05-12T18:32:38.070902+00:00', 'updated_at': '2025-05-12T18:32:38.070903+00:00', 'score': None}{'namespace': ['memories', '1'], 'key': '25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', 'value': {'content': 'User enjoys playing soccer.'}, 'created_at': '2025-05-12T18:32:42.558787+00:00', 'updated_at': '2025-05-12T18:32:42.558789+00:00', 'score': None}
InputPythonfor m in memories:print(m.value)Copied
{'content': "User's name is Maximo."}{'content': 'User enjoys playing soccer.'}
Vemos que se guardan documentos de memoria, no un perfil del usuario.
Vamos a añadir un nuevo interés del usuario
InputPython# User inputinput_messages = [HumanMessage(content="I also like to play basketball")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I also like to play basketball[Call model debug] Memories: [Item(namespace=['memories', '1'], key='6d06c4f5-3a74-46b2-92b4-1e29ba128c90', value={'content': "User's name is Maximo."}, created_at='2025-05-12T18:32:38.070902+00:00', updated_at='2025-05-12T18:32:38.070903+00:00', score=None), Item(namespace=['memories', '1'], key='25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', value={'content': 'User enjoys playing soccer.'}, created_at='2025-05-12T18:32:42.558787+00:00', updated_at='2025-05-12T18:32:42.558789+00:00', score=None)]================================== Ai Message ==================================That's awesome, Maximo! Both soccer and basketball are fantastic sports. I'll remember that you enjoy basketball as well. Do you find yourself playing one more than the other? And similar to soccer, do you play basketball with a team or more casually? Many people enjoy the different skills and dynamics each sport offers - soccer with its continuous flow and footwork, and basketball with its fast pace and shooting precision. Any favorite basketball teams you follow?[Write memory debug] Existing memories: [('6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'Memory', {'content': "User's name is Maximo."}), ('25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', 'Memory', {'content': 'User enjoys playing soccer.'})][Write memory debug] Saved memories: [Memory(content='User enjoys playing basketball.')]
Volvemos a ver la memoria actualizada
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memories", user_id)memories = long_term_memory.search(namespace)for m in memories:print(m.value)Copied
{'content': "User's name is Maximo."}{'content': 'User enjoys playing soccer.'}{'content': 'User enjoys playing basketball.'}
Iniciamos una nueva conversación con un nuevo hilo
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "2", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="What soccer players do you recommend for me?")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================What soccer players do you recommend for me?[Call model debug] Memories: [Item(namespace=['memories', '1'], key='6d06c4f5-3a74-46b2-92b4-1e29ba128c90', value={'content': "User's name is Maximo."}, created_at='2025-05-12T18:32:38.070902+00:00', updated_at='2025-05-12T18:32:38.070903+00:00', score=None), Item(namespace=['memories', '1'], key='25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', value={'content': 'User enjoys playing soccer.'}, created_at='2025-05-12T18:32:42.558787+00:00', updated_at='2025-05-12T18:32:42.558789+00:00', score=None), Item(namespace=['memories', '1'], key='965f2e52-bea0-44d4-8534-4fce2bbc1c4b', value={'content': 'User enjoys playing basketball.'}, created_at='2025-05-12T18:33:38.613626+00:00', updated_at='2025-05-12T18:33:38.613629+00:00', score=None)]================================== Ai Message ==================================Hi Maximo! Since you enjoy soccer, I'd be happy to recommend some players you might find interesting to follow or learn from.Based on your interests in both soccer and basketball, I might suggest players who are known for their athleticism and skill:1. Lionel Messi - Widely considered one of the greatest players of all time2. Cristiano Ronaldo - Known for incredible athleticism and dedication3. Kylian Mbappé - Young talent with amazing speed and technical ability4. Kevin De Bruyne - Master of passing and vision5. Erling Haaland - Goal-scoring phenomenonIs there a particular position or playing style you're most interested in? That would help me refine my recommendations further. I could also suggest players from specific leagues or teams if you have preferences![Write memory debug] Existing memories: [('6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'Memory', {'content': "User's name is Maximo."}), ('25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', 'Memory', {'content': 'User enjoys playing soccer.'}), ('965f2e52-bea0-44d4-8534-4fce2bbc1c4b', 'Memory', {'content': 'User enjoys playing basketball.'})][Write memory debug] Saved memories: [Memory(content='User asked for soccer player recommendations, suggesting an active interest in following professional soccer beyond just playing it.')]
Vemos que se acordaba de que nos gustaba el fútbol y el baloncesto.
Human in the loop
Aunque un agente puede realizar tareas, para según qué tareas, es necesario que haya una supervisión humana. A esto se le llama human in the loop. Así que vamos a ver cómo se puede hacer esto con LangGraph.
La capa de persistencia de LangGraph admite flujos de trabajo con humanos en el bucle, lo que permite que la ejecución se detenga y reanude en función de los comentarios de los usuarios. La interfaz principal de esta funcionalidad es la función interrupt. Llamando a interrupt dentro de un nodo se detendrá la ejecución. La ejecución se puede reanudar, junto con la nueva aportación del humano, pasada en una primitiva Command. interrupt es similar al comando de Python input(), pero con algunas consideraciones extra.
Vamos a añadir al chatbot que tiene memoria a corto plazo y acceso a tools, pero haremos un cambio, que es agregar una simple herramienta human_assistance. Esta herramienta utiliza interrupt para recibir información de un humano.
Primero cargamos los valores de las API KEYs
InputPythonimport osimport dotenvdotenv.load_dotenv()HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")TAVILY_API_KEY = os.getenv("TAVILY_LANGGRAPH_API_KEY")Copied
Creamos el grafo
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesclass State(TypedDict):messages: Annotated[list, add_messages]graph_builder = StateGraph(State)Copied
Definimos la tool de búsqueda
InputPythonfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapperfrom langchain_community.tools.tavily_search import TavilySearchResultswrapper = TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)search_tool = TavilySearchResults(api_wrapper=wrapper, max_results=2)Copied
Ahora creamos la tool de ayuda humana
InputPythonfrom langgraph.types import Command, interruptfrom langchain_core.tools import tool@tooldef human_assistance(query: str) -> str:"""Request assistance from a human expert. Use this tool ONLY ONCE per conversation.After receiving the expert's response, you should provide an elaborated response to the user based on the information receivedbased on the information received, without calling this tool again.Args:query: The query to ask the human expert.Returns:The response from the human expert."""human_response = interrupt({"query": query})return human_response["data"]Copied
LangGraph obtiene información de las herramientas mediante la documentación de la herramienta, es decir, el docstring de la función. Por lo que es muy importante generar un buen docstring para la herramienta.
Creamos una lista de tools
InputPythontools_list = [search_tool, human_assistance]Copied
A continuación, el LLM con las bind_tools y lo añadimos al grafo
InputPythonfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom huggingface_hub import loginos.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLMlogin(token=HUGGINGFACE_TOKEN)MODEL = "Qwen/Qwen2.5-72B-Instruct"model = HuggingFaceEndpoint(repo_id=MODEL,task="text-generation",max_new_tokens=512,do_sample=False,repetition_penalty=1.03,)# Create the chat modelllm = ChatHuggingFace(llm=model)# Modification: tell the LLM which tools it can callllm_with_tools = llm.bind_tools(tools_list)# Define the chatbot functiondef chatbot_function(state: State):message = llm_with_tools.invoke(state["messages"])assert len(message.tool_calls) <= 1return {"messages": [message]}# Add the chatbot nodegraph_builder.add_node("chatbot_node", chatbot_function)Copied
<langgraph.graph.state.StateGraph at 0x10764b380>
Si te fijas, hemos cambiado la forma de definir la función chatbot_function, ya que ahora tiene que manejar la interrupción.
Añadimos la tool_node al grafo
InputPythonfrom langgraph.prebuilt import ToolNode, tools_conditiontool_node = ToolNode(tools=tools_list)graph_builder.add_node("tools", tool_node)graph_builder.add_conditional_edges("chatbot_node", tools_condition)graph_builder.add_edge("tools", "chatbot_node")Copied
<langgraph.graph.state.StateGraph at 0x10764b380>
Añadimos el nodo de START al grafo
InputPythongraph_builder.add_edge(START, "chatbot_node")Copied
<langgraph.graph.state.StateGraph at 0x10764b380>
Creamos un checkpointer MemorySaver.
InputPythonfrom langgraph.checkpoint.memory import MemorySavermemory = MemorySaver()Copied
Compilamos el grafo con el checkpointer
InputPythongraph = graph_builder.compile(checkpointer=memory)Copied
Lo representamos gráficamente
InputPythonfrom IPython.display import Image, displaytry:display(Image(graph.get_graph().draw_mermaid_png()))except Exception as e:print(f"Error al visualizar el grafo: {e}")Copied
<IPython.core.display.Image object>
Ahora solicitemos al chatbot con una pregunta que involucrará la nueva herramienta human_assistance:
InputPythonuser_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"config = {"configurable": {"thread_id": "1"}}events = graph.stream({"messages": [{"role": "user", "content": user_input}]},config,stream_mode="values",)for event in events:if "messages" in event:event["messages"][-1].pretty_print()Copied
================================ Human Message =================================I need some expert guidance for building an AI agent. Could you request assistance for me?================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need some expert guidance for building an AI agent. Could you provide me with some advice?
Cómo se puede ver, el chatbot generó una llamada a la herramienta de asistencia humana.
Tool Calls:
human_assistance (0)
Call ID: 0
Args:
query: I need some expert guidance for building an AI agent. Could you provide advice on key considerations, best practices, and potential pitfalls to avoid?Pero luego la ejecución se ha interrumpido. Vamos a ver el estado del grafo
InputPythonsnapshot = graph.get_state(config)snapshot.nextCopied
('tools',)
Vemos que se detuvo en el nodo de tools. Analizamos cómo se ha definido la herramienta human_assistance.
from langgraph.types import Command, interrupt
from langchain_core.tools import tool
@tool
def human_assistance(query: str) -> str:
"""
Request assistance from a human expert. Use this tool ONLY ONCE per conversation.
After receiving the expert's response, you should provide an elaborated response to the user based on the information received
based on the information received, without calling this tool again.
Args:
query: The query to ask the human expert.
Returns:
The response from the human expert.
"""
human_response = interrupt({"query": query})
return human_response["data"]Llamando a la herramienta interrupt se detendrá la ejecución, similar a la función de Python input().
El progreso se mantiene en función de nuestra elección de checkpointer. Es decir, la elección de dónde se guarda el estado del grafo. Así que si estamos persistiendo (guardando el estado del grafo) con una base de datos como SQLite, Postgres, etc., podemos reanudar la ejecución en cualquier momento siempre y cuando la base de datos esté viva.
Aquí estamos persistiendo (guardando el estado del grafo) con el puntero de verificación en memoria RAM, por lo que podemos reanudar en cualquier momento mientras nuestro kernel de Python se esté ejecutando. En mi caso, mientras no reseete el kernel de mi Jupyter Notebook.
Para reanudar la ejecución, pasamos un objeto Command que contiene los datos esperados por la herramienta. El formato de estos datos se puede personalizar en función de nuestras necesidades. Aquí, solo necesitamos un diccionario con una key data
InputPythonhuman_response = ("We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.""It's much more reliable and extensible than simple autonomous agents.")human_command = Command(resume={"data": human_response})events = graph.stream(human_command, config, stream_mode="values")for event in events:if "messages" in event:event["messages"][-1].pretty_print()Copied
================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need some expert guidance for building an AI agent. Could you provide me with some advice?================================= Tool Message =================================Name: human_assistanceWe, the experts are here to help! We'd recommend you check out LangGraph to build your agent.It's much more reliable and extensible than simple autonomous agents.================================== Ai Message ==================================The experts recommend checking out LangGraph for building your AI agent. It's known for being more reliable and extensible compared to simple autonomous agents.
Como vemos, el chatbot ha esperado a que un humano le proporcione la respuesta y luego ha generado una respuesta basada en la información recibida. Le hemos pedido ayuda sobre un experto sobre cómo crear agentes, el humano le ha dicho que lo mejor es usar LangGraph, y el chatbot ha generado una respuesta basada en esa información.
Pero sigue teniendo la posibilidad de realizar búsquedas en la web. Así que ahora le vamos a pedir las últimas noticias sobre LangGraph.
InputPythonuser_input = "What's the latest news about LangGraph?"events = graph.stream({"messages": [{"role": "user", "content": user_input}]},config,stream_mode="values",)for event in events:if "messages" in event:event["messages"][-1].pretty_print()Copied
================================ Human Message =================================What's the latest news about LangGraph?================================== Ai Message ==================================Tool Calls:tavily_search_results_json (0)Call ID: 0Args:query: latest news LangGraph================================= Tool Message =================================Name: tavily_search_results_json[{"title": "LangChain - Changelog", "url": "https://changelog.langchain.com/", "content": "LangGraph `interrupt`: Simplifying human-in-the-loop agents --------------------------------------------------- Our latest feature in LangGraph, interrupt , makes building human-in-the-loop workflows easier. Agents aren’t perfect, so keeping humans “in the loop”... December 16, 2024 [...] LangGraph 🔁 Modify graph state from tools in LangGraph --------------------------------------------- LangGraph's latest update gives you greater control over your agents by enabling tools to directly update the graph state. This is a game-changer for use... December 18, 2024 [...] LangGraph Platform Custom authentication & access control for LangGraph Platform ------------------------------------------------------------- Today, we're thrilled to announce Custom Authentication and Resource-Level Access Control for Python deployments in LangGraph Cloud and self-hosted... December 20, 2024", "score": 0.78650844}, {"title": "LangGraph 0.3 Release: Prebuilt Agents - LangChain Blog", "url": "https://blog.langchain.dev/langgraph-0-3-release-prebuilt-agents/", "content": "LangGraph 0.3 Release: Prebuilt Agents 2 min read Feb 27, 2025 By Nuno Campos and Vadym Barda Over the past year, we’ve invested heavily in making LangGraph the go-to framework for building AI agents. With companies like Replit, Klarna, LinkedIn and Uber choosing to build on top of LangGraph, we have more conviction than ever that we are on the right path. [...] Up to this point, we’ve had one higher level abstraction and it’s lived in the main langgraph package. It was create_react_agent, a wrapper for creating a simple tool calling agent. Today, we are splitting that out of langgraph as part of a 0.3 release, and moving it into langgraph-prebuilt. We are also introducing a new set of prebuilt agents built on top of LangGraph, in both Python and JavaScript. Over the past three weeks, we’ve already released a few of these: [...] Published Time: 2025-02-27T15:09:15.000Z LangGraph 0.3 Release: Prebuilt Agents Skip to content Case Studies In the Loop LangChain Docs Changelog Sign in Subscribe", "score": 0.72348577}]================================== Ai Message ==================================Tool Calls:tavily_search_results_json (0)Call ID: 0Args:query: latest news about LangGraph================================= Tool Message =================================Name: tavily_search_results_json[{"title": "LangChain - Changelog", "url": "https://changelog.langchain.com/", "content": "LangGraph 🔁 Modify graph state from tools in LangGraph --------------------------------------------- LangGraph's latest update gives you greater control over your agents by enabling tools to directly update the graph state. This is a game-changer for use... December 18, 2024 [...] LangGraph `interrupt`: Simplifying human-in-the-loop agents --------------------------------------------------- Our latest feature in LangGraph, interrupt , makes building human-in-the-loop workflows easier. Agents aren’t perfect, so keeping humans “in the loop”... December 16, 2024 [...] LangGraph Platform Custom authentication & access control for LangGraph Platform ------------------------------------------------------------- Today, we're thrilled to announce Custom Authentication and Resource-Level Access Control for Python deployments in LangGraph Cloud and self-hosted... December 20, 2024", "score": 0.79732054}, {"title": "LangGraph 0.3 Release: Prebuilt Agents - LangChain Blog", "url": "https://blog.langchain.dev/langgraph-0-3-release-prebuilt-agents/", "content": "LangGraph 0.3 Release: Prebuilt Agents 2 min read Feb 27, 2025 By Nuno Campos and Vadym Barda Over the past year, we’ve invested heavily in making LangGraph the go-to framework for building AI agents. With companies like Replit, Klarna, LinkedIn and Uber choosing to build on top of LangGraph, we have more conviction than ever that we are on the right path. [...] Up to this point, we’ve had one higher level abstraction and it’s lived in the main langgraph package. It was create_react_agent, a wrapper for creating a simple tool calling agent. Today, we are splitting that out of langgraph as part of a 0.3 release, and moving it into langgraph-prebuilt. We are also introducing a new set of prebuilt agents built on top of LangGraph, in both Python and JavaScript. Over the past three weeks, we’ve already released a few of these: [...] Published Time: 2025-02-27T15:09:15.000Z LangGraph 0.3 Release: Prebuilt Agents Skip to content Case Studies In the Loop LangChain Docs Changelog Sign in Subscribe", "score": 0.7552947}]================================== Ai Message ==================================The latest news about LangGraph includes several updates and releases. Firstly, the 'interrupt' feature has been added, which simplifies creating human-in-the-loop workflows, essential for maintaining oversight of AI agents. Secondly, an update allows tools to modify the graph state directly, providing more control over the agents. Lastly, custom authentication and resource-level access control have been implemented for Python deployments in LangGraph Cloud and self-hosted environments. In addition, LangGraph released version 0.3, which introduces prebuilt agents in both Python and JavaScript, aimed at making it even easier to develop AI agents.
Ha buscado las últimas noticias sobre LangGraph y ha generado una respuesta basada en la información recibida.
Vamos a escribir todo junto para que sea más comprensible
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom huggingface_hub import loginfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapperfrom langchain_community.tools.tavily_search import TavilySearchResultsfrom langchain_core.messages import ToolMessagefrom langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.types import Command, interruptfrom langchain_core.tools import toolfrom langgraph.checkpoint.memory import MemorySaverfrom IPython.display import Image, displayimport jsonimport osos.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracingimport dotenvdotenv.load_dotenv()HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")TAVILY_API_KEY = os.getenv("TAVILY_LANGGRAPH_API_KEY")# Stateclass State(TypedDict):messages: Annotated[list, add_messages]# Toolswrapper = TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)tool_search = TavilySearchResults(api_wrapper=wrapper, max_results=2)@tooldef human_assistance(query: str) -> str:"""Request assistance from a human expert. Use this tool ONLY ONCE per conversation.After receiving the expert's response, you should provide an elaborated response to the user based on the information receivedbased on the information received, without calling this tool again.Args:query: The query to ask the human expert.Returns:The response from the human expert."""human_response = interrupt({"query": query})return human_response["data"]tools_list = [tool_search, human_assistance]# Create the LLM modellogin(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the modelMODEL = "Qwen/Qwen2.5-72B-Instruct"model = HuggingFaceEndpoint(repo_id=MODEL,task="text-generation",max_new_tokens=512,do_sample=False,repetition_penalty=1.03,)# Create the chat modelllm = ChatHuggingFace(llm=model)# Create the LLM with toolsllm_with_tools = llm.bind_tools(tools_list)# Tool nodetool_node = ToolNode(tools=tools_list)# Functionsdef chatbot_function(state: State):message = llm_with_tools.invoke(state["messages"])assert len(message.tool_calls) <= 1return {"messages": [message]}# Start to build the graphgraph_builder = StateGraph(State)# Add nodes to the graphgraph_builder.add_node("chatbot_node", chatbot_function)graph_builder.add_node("tools", tool_node)# Add edgesgraph_builder.add_edge(START, "chatbot_node")graph_builder.add_conditional_edges( "chatbot_node", tools_condition)graph_builder.add_edge("tools", "chatbot_node")# Compile the graphmemory = MemorySaver()graph = graph_builder.compile(checkpointer=memory)# Display the graphtry:display(Image(graph.get_graph().draw_mermaid_png()))except Exception as e:print(f"Error al visualizar el grafo: {e}")Copied
Error al visualizar el grafo: Failed to reach https://mermaid.ink/ API while trying to render your graph after 1 retries. To resolve this issue:1. Check your internet connection and try again2. Try with higher retry settings: `draw_mermaid_png(..., max_retries=5, retry_delay=2.0)`3. Use the Pyppeteer rendering method which will render your graph locally in a browser: `draw_mermaid_png(..., draw_method=MermaidDrawMethod.PYPPETEER)`
Volvemos a pedirle ayuda al chatbot para crear agentes. Le pedimos que busque ayuda
InputPythonuser_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"config = {"configurable": {"thread_id": "1"}}events = graph.stream({"messages": [{"role": "user", "content": user_input}]},config,stream_mode="values",)for event in events:if "messages" in event:event["messages"][-1].pretty_print()Copied
================================ Human Message =================================I need some expert guidance for building an AI agent. Could you request assistance for me?================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need expert guidance for building an AI agent.
Vemos en qué estado se ha quedado el grafo
InputPythonsnapshot = graph.get_state(config)snapshot.nextCopied
('tools',)
Le damos la asistencia que está pidiendo
InputPythonhuman_response = ("We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.""It's much more reliable and extensible than simple autonomous agents.")human_command = Command(resume={"data": human_response})events = graph.stream(human_command, config, stream_mode="values")for event in events:if "messages" in event:event["messages"][-1].pretty_print()Copied
================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need expert guidance for building an AI agent.================================= Tool Message =================================Name: human_assistanceWe, the experts are here to help! We'd recommend you check out LangGraph to build your agent.It's much more reliable and extensible than simple autonomous agents.================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need some expert guidance for building an AI agent. Could you recommend a platform and any tips for getting started?
Y por último le pedimos que busque en internet las últimas noticias de LangGraph
InputPythonuser_input = "What's the latest news about LangGraph?"events = graph.stream({"messages": [{"role": "user", "content": user_input}]},config,stream_mode="values",)for event in events:if "messages" in event:event["messages"][-1].pretty_print()Copied
================================ Human Message =================================What's the latest news about LangGraph?================================== Ai Message ==================================Tool Calls:tavily_search_results_json (0)Call ID: 0Args:query: latest news about LangGraph================================= Tool Message =================================Name: tavily_search_results_json[{"title": "LangChain Blog", "url": "https://blog.langchain.dev/", "content": "LangSmith Incident on May 1, 2025 Requests to the US LangSmith API from both the web application and SDKs experienced an elevated error rate for 28 minutes on May 1, 2025 Featured How Klarna's AI assistant redefined customer support at scale for 85 million active users Is LangGraph Used In Production? Introducing Interrupt: The AI Agent Conference by LangChain Top 5 LangGraph Agents in Production 2024 [...] See how Harmonic uses LangSmith and LangGraph products to streamline venture investing workflows. Why Definely chose LangGraph for building their multi-agent AI system See how Definely used LangGraph to design a multi-agent system to help lawyers speed up their workflows. Introducing End-to-End OpenTelemetry Support in LangSmith LangSmith now provides end-to-end OpenTelemetry (OTel) support for applications built on LangChain and/or LangGraph.", "score": 0.6811549}, {"title": "LangGraph + UiPath: advancing agentic automation together", "url": "https://www.uipath.com/blog/product-and-updates/langgraph-uipath-advancing-agentic-automation-together", "content": "Raghu Malpani, Chief Technology Officer at UiPath, emphasizes the significance of these announcements for the UiPath developer community: Our collaboration with LangChain on LangSmith and Agent Protocol advances interoperability across agent frameworks. Further, by enabling the deployment of LangGraph agents into UiPath's enterprise-grade infrastructure, we are expanding the capabilities of our platform and opening up more possibilities for our developer community. [...] Today, we’re excited to announce: Native support for LangSmith observability in the UiPath LLM Gateway via OpenTelemetry (OTLP), enabling developers to monitor, debug, and evaluate LLM-powered features in UiPath using LangSmith either in LangChain’s cloud or self-hosted on-premises. This feature is currently in private preview.", "score": 0.6557114}]================================== Ai Message ==================================Tool Calls:tavily_search_results_json (0)Call ID: 0Args:query: latest news about LangGraph================================= Tool Message =================================Name: tavily_search_results_json[{"title": "LangChain Blog", "url": "https://blog.langchain.dev/", "content": "LangSmith Incident on May 1, 2025 Requests to the US LangSmith API from both the web application and SDKs experienced an elevated error rate for 28 minutes on May 1, 2025 Featured How Klarna's AI assistant redefined customer support at scale for 85 million active users Is LangGraph Used In Production? Introducing Interrupt: The AI Agent Conference by LangChain Top 5 LangGraph Agents in Production 2024 [...] See how Harmonic uses LangSmith and LangGraph products to streamline venture investing workflows. Why Definely chose LangGraph for building their multi-agent AI system See how Definely used LangGraph to design a multi-agent system to help lawyers speed up their workflows. Introducing End-to-End OpenTelemetry Support in LangSmith LangSmith now provides end-to-end OpenTelemetry (OTel) support for applications built on LangChain and/or LangGraph.", "score": 0.6811549}, {"title": "LangGraph + UiPath: advancing agentic automation together", "url": "https://www.uipath.com/blog/product-and-updates/langgraph-uipath-advancing-agentic-automation-together", "content": "Raghu Malpani, Chief Technology Officer at UiPath, emphasizes the significance of these announcements for the UiPath developer community: Our collaboration with LangChain on LangSmith and Agent Protocol advances interoperability across agent frameworks. Further, by enabling the deployment of LangGraph agents into UiPath's enterprise-grade infrastructure, we are expanding the capabilities of our platform and opening up more possibilities for our developer community. [...] Today, we’re excited to announce: Native support for LangSmith observability in the UiPath LLM Gateway via OpenTelemetry (OTLP), enabling developers to monitor, debug, and evaluate LLM-powered features in UiPath using LangSmith either in LangChain’s cloud or self-hosted on-premises. This feature is currently in private preview.", "score": 0.6557114}]...================================= Tool Message =================================Name: tavily_search_results_json[{"title": "LangGraph - LangChain", "url": "https://www.langchain.com/langgraph", "content": "“As Ally advances its exploration of Generative AI, our tech labs is excited by LangGraph, the new library from LangChain, which is central to our experiments", "score": 0.98559}, {"title": "Evaluating LangGraph Framework : Series 1 | by Jalaj Agrawal", "url": "https://medium.com/@jalajagr/evaluating-langgraph-as-a-multiagent-framework-a-10-dimensional-framework-series-1-c7203b7f4659", "content": ": LangGraph excels with its intuitive graph-based abstraction that allows new developers to build working multi-agent systems within hours.", "score": 0.98196}]================================== Ai Message ==================================It looks like LangGraph has been generating some significant buzz in the AI community, especially for its capabilities in building multi-agent systems. Here are a few highlights from the latest news:1. **LangGraph in Production**: Companies like Klarna and Definely are already using LangGraph to build and optimize their AI systems. Klarna has leveraged LangGraph to enhance their customer support, and Definely has used it to design a multi-agent system to speed up legal workflows.2. **Integration with UiPath**: LangChain and UiPath have collaborated to advance agentic automation. This partnership includes native support for LangSmith observability in UiPath’s LLM Gateway via OpenTelemetry, which will allow developers to monitor, debug, and evaluate LLM-powered features more effectively.3. **Intuitive Design**: LangGraph is praised for its intuitive graph-based abstraction, which enables developers to build working multi-agent systems quickly, even if they are new to the field.4. **Community and Conferences**: LangChain is also hosting an AI Agent Conference called "Interrupt," which could be a great opportunity to learn more about the latest developments and best practices in building AI agents.If you're considering using LangGraph for your project, these resources and updates might provide valuable insights and support. Would you like more detailed information on any specific aspect of LangGraph?
Más
Aprobación del uso de herramientas
Nota: Este apartado lo vamos a hacer usando Sonnet 3.7, ya que a día de la escritura del post, es el mejor modelo para uso con agentes, y es el único que entiende cuándo tiene que llamar a las tools y cuándo no para este ejemplo
Podemos meter un human in the loop para aprobar el uso de herramientas. Vamos a crear un chatbot con varias herramientas para hacer operaciones matemáticas, para ello a la hora de construir el grafo indicamos dónde queremos meter el breakpoint (graph_builder.compile(interrupt_before=["tools"], checkpointer=memory))
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_core.messages import SystemMessage, HumanMessage, AIMessagefrom langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.checkpoint.memory import MemorySaverfrom langchain_core.tools import toolfrom langchain_anthropic import ChatAnthropicfrom IPython.display import Image, displayimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")memory = MemorySaver()class State(TypedDict):messages: Annotated[list, add_messages]os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Tools@tooldef multiply(a: int, b: int) -> int:"""Multiply a and b.Args:a: first intb: second intReturns:The product of a and b."""return a * b@tooldef add(a: int, b: int) -> int:"""Adds a and b.Args:a: first intb: second intReturns:The sum of a and b."""return a + b@tooldef subtract(a: int, b: int) -> int:"""Subtract b from a.Args:a: first intb: second intReturns:The difference between a and b."""return a - b@tooldef divide(a: int, b: int) -> float:"""Divide a by b.Args:a: first intb: second intReturns:The quotient of a and b."""return a / btools_list = [multiply, add, subtract, divide]# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)llm_with_tools = llm.bind_tools(tools_list)# Nodesdef chat_model_node(state: State):system_message = "You are a helpful assistant that can use tools to answer questions. Once you have the result of a tool, provide a final answer without calling more tools."messages = [SystemMessage(content=system_message)] + state["messages"]return {"messages": [llm_with_tools.invoke(messages)]}# Create graph buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("chatbot_node", chat_model_node)tool_node = ToolNode(tools=tools_list)graph_builder.add_node("tools", tool_node)# Connecto nodesgraph_builder.add_edge(START, "chatbot_node")graph_builder.add_conditional_edges("chatbot_node", tools_condition)graph_builder.add_edge("tools", "chatbot_node")graph_builder.add_edge("chatbot_node", END)# Compile the graphgraph = graph_builder.compile(interrupt_before=["tools"], checkpointer=memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Como vemos en el grafo, hay un interrupt antes de usar las tools. Eso significa que se va a parar antes de usarlas para pedirnos permiso
InputPython# Inputinitial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}config = {"configurable": {"thread_id": "1"}}# Run the graph until the first interruptionfor event in graph.stream(initial_input, config, stream_mode="updates"):if 'chatbot_node' in event:print(event['chatbot_node']['messages'][-1].pretty_print())else:print(event)Copied
================================== Ai Message ==================================[{'text': "I'll multiply 2 and 3 for you.", 'type': 'text'}, {'id': 'toolu_01QDuind1VBHWtvifELN9SPf', 'input': {'a': 2, 'b': 3}, 'name': 'multiply', 'type': 'tool_use'}]Tool Calls:multiply (toolu_01QDuind1VBHWtvifELN9SPf)Call ID: toolu_01QDuind1VBHWtvifELN9SPfArgs:a: 2b: 3None{'__interrupt__': ()}
Como podemos ver, el LLM sabe que tiene que usar la herramienta multiply, pero se interrumpe la ejecución, porque tiene que esperar a que un humano le autorice el uso de la herramienta.
Podemos ver el estado en el que se ha quedado el grafo
InputPythonstate = graph.get_state(config)state.nextCopied
('tools',)
Como vemos, se ha quedado en el nodo de tools.
Podemos crear una función (no en el grafo, sino fuera del grafo, para mejorar la experiencia de usuario y que entienda por qué se para la ejecución) que le pida al usuario que apruebe el uso de la herramienta.
Creamos un nuevo thread_id para que se cree un nuevo estado.
InputPython# Inputinitial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}config = {"configurable": {"thread_id": "2"}}# Run the graph until the first interruptionfor event in graph.stream(initial_input, config, stream_mode="updates"):function_name = Nonefunction_args = Noneif 'chatbot_node' in event:for element in event['chatbot_node']['messages'][-1].content:if element['type'] == 'text':print(element['text'])elif element['type'] == 'tool_use':function_name = element['name']function_args = element['input']print(f"The LLM wants to use the tool {function_name} with the arguments {function_args}")elif '__interrupt__' in event:passelse:print(event)question = f"Do you approve the use of the tool {function_name} with the arguments {function_args}? (y/n)"user_approval = input(question)print(f"{question}: {user_approval}")if user_approval.lower() == 'y':print("User approved the use of the tool")for event in graph.stream(None, config, stream_mode="updates"):if 'chatbot_node' in event:for element in event['chatbot_node']['messages'][-1].content:if isinstance(element, str):print(element, end="")elif 'tools' in event:result = event['tools']['messages'][-1].contenttool_used = event['tools']['messages'][-1].nameprint(f"The result of the tool {tool_used} is {result}")else:print(event)Copied
I'll multiply 2 and 3 for you.The LLM wants to use the tool multiply with the arguments {'a': 2, 'b': 3}Do you approve the use of the tool None with the arguments None? (y/n): yUser approved the use of the toolThe result of the tool multiply is 6The result of multiplying 2 and 3 is 6.
Podemos ver que nos ha preguntado si aprobamos el uso de la tool de multiplicación, la hemos aprobado y el grafo ha terminado la ejecución. Vemos el estado del grafo.
InputPythonstate = graph.get_state(config)state.nextCopied
()
Vemos que el siguiente estado del grafo está vacío, eso indica que ha terminado la ejecución del grafo
Modificación del estado
Nota: Este apartado lo vamos a hacer usando Sonnet 3.7, ya que a día de la escritura del post, es el mejor modelo para uso con agentes, y es el único que entiende cuándo tiene que llamar a las tools y cuándo no para este ejemplo
Vamos a repetir el ejemplo de antes, pero en vez de interrumpir el grafo antes del uso de una tool, lo vamos a interrumpir en el LLM, para ello, a la hora de construir el grafo indicamos que queremos pararlo en el agente (graph_builder.compile(interrupt_before=["chatbot_node"], checkpointer=memory))
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_core.messages import SystemMessage, HumanMessage, AIMessagefrom langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.checkpoint.memory import MemorySaverfrom langchain_core.tools import toolfrom langchain_anthropic import ChatAnthropicfrom IPython.display import Image, displayimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")memory = MemorySaver()class State(TypedDict):messages: Annotated[list, add_messages]os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Tools@tooldef multiply(a: int, b: int) -> int:"""Multiply a and b.Args:a: first intb: second intReturns:The product of a and b."""return a * b@tooldef add(a: int, b: int) -> int:"""Adds a and b.Args:a: first intb: second intReturns:The sum of a and b."""return a + b@tooldef subtract(a: int, b: int) -> int:"""Subtract b from a.Args:a: first intb: second intReturns:The difference between a and b."""return a - b@tooldef divide(a: int, b: int) -> float:"""Divide a by b.Args:a: first intb: second intReturns:The quotient of a and b."""return a / btools_list = [multiply, add, subtract, divide]# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)llm_with_tools = llm.bind_tools(tools_list)# Nodesdef chat_model_node(state: State):system_message = "You are a helpful assistant that can use tools to answer questions. Once you have the result of a tool, provide a final answer without calling more tools."messages = [SystemMessage(content=system_message)] + state["messages"]return {"messages": [llm_with_tools.invoke(messages)]}# Create graph buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("chatbot_node", chat_model_node)tool_node = ToolNode(tools=tools_list)graph_builder.add_node("tools", tool_node)# Connecto nodesgraph_builder.add_edge(START, "chatbot_node")graph_builder.add_conditional_edges("chatbot_node", tools_condition)graph_builder.add_edge("tools", "chatbot_node")graph_builder.add_edge("chatbot_node", END)# Compile the graphgraph = graph_builder.compile(interrupt_before=["chatbot_node"], checkpointer=memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Vemos en la representación del grafo que hay un interrupt antes de la ejecución de chatbot_node, así que antes de que se ejecute el chatbot se interrumpirá la ejecución y tendremos que hacer nosotros que continúe
Ahora le volvemos a pedir una multiplicación
InputPython# Inputinitial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}config = {"configurable": {"thread_id": "1"}}# Run the graph until the first interruptionfor event in graph.stream(initial_input, config, stream_mode="updates"):if 'chatbot_node' in event:print(event['chatbot_node']['messages'][-1].pretty_print())else:print(event)Copied
{'__interrupt__': ()}
Podemos ver que no ha hecho nada. Si vemos el estado
InputPythonstate = graph.get_state(config)state.nextCopied
('chatbot_node',)
Vemos que el siguiente nodo es el de chatbot. Además, si vemos sus valores, vemos el mensaje que le hemos mandado
InputPythonstate.valuesCopied
{'messages': [HumanMessage(content='Multiply 2 and 3', additional_kwargs={}, response_metadata={}, id='08fd6084-ecd2-4156-ab24-00d2d5c26f00')]}
Ahora procedemos a modificar el estado, añadiendo un nuevo mensaje
InputPythongraph.update_state(config,{"messages": [HumanMessage(content="No, actually multiply 3 and 3!")]})Copied
{'configurable': {'thread_id': '1','checkpoint_ns': '','checkpoint_id': '1f027eb6-6c8b-6b6a-8001-bc0f8942566c'}}
Obtenemos el nuevo estado
InputPythonnew_state = graph.get_state(config)new_state.nextCopied
('chatbot_node',)
El siguiente nodo sigue siendo el del chatbot, pero si ahora vemos los mensajes
InputPythonnew_state.valuesCopied
{'messages': [HumanMessage(content='Multiply 2 and 3', additional_kwargs={}, response_metadata={}, id='08fd6084-ecd2-4156-ab24-00d2d5c26f00'),HumanMessage(content='No, actually multiply 3 and 3!', additional_kwargs={}, response_metadata={}, id='e95394c2-e62e-47d2-b9b2-51eba40f3e22')]}
Vemos que se ha añadido el nuevo. Así que hacemos que continúe la ejecución
InputPythonfor event in graph.stream(None, config, stream_mode="values"):event['messages'][-1].pretty_print()Copied
================================ Human Message =================================No, actually multiply 3 and 3!================================== Ai Message ==================================[{'text': "I'll multiply 3 and 3 for you.", 'type': 'text'}, {'id': 'toolu_01UABhLnEdg5ZqxVQTE5pGUx', 'input': {'a': 3, 'b': 3}, 'name': 'multiply', 'type': 'tool_use'}]Tool Calls:multiply (toolu_01UABhLnEdg5ZqxVQTE5pGUx)Call ID: toolu_01UABhLnEdg5ZqxVQTE5pGUxArgs:a: 3b: 3================================= Tool Message =================================Name: multiply9
Se ha hecho la multiplicación de 3 por 3, que es la modificación del estado que hemos hecho, y no 2 por 3, que es lo que le pedimos la primera vez
Esto puede ser útil cuando tenemos un agente y queremos revisar que lo que hace esté bien, por lo que podemos entrar en la ejecución y modificar el estado
Breakpoints dinámicos
Hasta ahora hemos creado breakpoints estáticos mediante la compilación del grafo, pero podemos crear breakpoints dinámicos mediante NodeInterrupt. Esto es útil porque se puede interrumpir la ejecución por reglas lógicas introducidas por programación
Estos NodeInterrupt permiten personalizar cómo se va a notificar al usuario de la interrupción
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom langchain_core.messages import SystemMessage, HumanMessage, AIMessagefrom langgraph.checkpoint.memory import MemorySaverfrom langgraph.errors import NodeInterruptfrom huggingface_hub import loginfrom IPython.display import Image, displayimport osimport dotenvdotenv.load_dotenv()HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")memory_saver = MemorySaver()class State(TypedDict):messages: Annotated[list, add_messages]os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLM modellogin(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the modelMODEL = "Qwen/Qwen2.5-72B-Instruct"model = HuggingFaceEndpoint(repo_id=MODEL,task="text-generation",max_new_tokens=512,do_sample=False,repetition_penalty=1.03,)# Create the chat modelllm = ChatHuggingFace(llm=model)# Nodesdef chatbot_function(state: State):max_len = 15input_message = state["messages"][-1]# Check len messageif len(input_message.content) > max_len:raise NodeInterrupt(f"Received input is longer than {max_len} characters --> {input_message}")# Invoke the LLM with the messagesresponse = llm.invoke(state["messages"])# Return the LLM's response in the correct state formatreturn {"messages": [response]}# Create graph buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("chatbot_node", chatbot_function)# Connecto nodesgraph_builder.add_edge(START, "chatbot_node")graph_builder.add_edge("chatbot_node", END)# Compile the graphgraph = graph_builder.compile(checkpointer=memory_saver)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Como se puede ver, hemos creado una interrupción en caso de que el mensaje sea largo. Vamos a probarlo
InputPythoninitial_input = {"messages": HumanMessage(content="Hello, how are you? My name is Máximo")}config = {"configurable": {"thread_id": "1"}}# Run the graph until the first interruptionfor event in graph.stream(initial_input, config, stream_mode="updates"):if 'chatbot_node' in event:print(event['chatbot_node']['messages'][-1].pretty_print())else:print(event)Copied
{'__interrupt__': (Interrupt(value="Received input is longer than 15 characters --> content='Hello, how are you? My name is Máximo' additional_kwargs={} response_metadata={} id='2bdc6d41-0cfe-4d3c-8748-ca7d46fd5a60'", resumable=False, ns=None),)}
Efectivamente se ha parado la interrupción y nos ha dado el mensaje de error que hemos creado
Si vemos el nodo en el que se ha parado
InputPythonstate = graph.get_state(config)state.nextCopied
('chatbot_node',)
Vemos que está parado en el nodo del chatbot. Podemos volver a hacer que continúe con la ejecución, pero nos va a dar el mismo error
InputPythonfor event in graph.stream(None, config, stream_mode="updates"):if 'chatbot_node' in event:print(event['chatbot_node']['messages'][-1].pretty_print())else:print(event)Copied
{'__interrupt__': (Interrupt(value="Received input is longer than 15 characters --> content='Hello, how are you? My name is Máximo' additional_kwargs={} response_metadata={} id='2bdc6d41-0cfe-4d3c-8748-ca7d46fd5a60'", resumable=False, ns=None),)}
Así que tenemos que modificar el estado
InputPythongraph.update_state(config,{"messages": [HumanMessage(content="How are you?")]})Copied
{'configurable': {'thread_id': '1','checkpoint_ns': '','checkpoint_id': '1f027f13-5827-6a18-8001-4209d5a866f0'}}
Volvemos a ver el estado y sus valores
InputPythonnew_state = graph.get_state(config)print(f"Siguiente nodo: {new_state.next}")print("Valores:")for value in new_state.values["messages"]:print(f" {value.content}")Copied
Siguiente nodo: ('chatbot_node',)Valores:Hello, how are you? My name is MáximoHow are you?
El último mensaje es más corto, por lo que intentamos reanudar la ejecución del grafo
InputPythonfor event in graph.stream(None, config, stream_mode="updates"):if 'chatbot_node' in event:print(event['chatbot_node']['messages'][-1].pretty_print())else:print(event)Copied
================================== Ai Message ==================================Hello Máximo! I'm doing well, thank you for asking. How about you? How can I assist you today?None
---
➡️ **Continúa en la Parte 4: personalización del estado y checkpoints**.