Files
langgraph/examples/sdk/python_sdk.ipynb
T
f27d8e16ad docs: Add initial LangGraph Cloud docs (#725)
* Create Deploy docs.

* Add LangGraph CLI docs.

* Update API concepts page.

* Create quick start page.

* first draft (#695)

* first draft

* fmt

* fmt

* Revert "fmt"

This reverts commit e599030ab5.

* Revert "fmt"

This reverts commit ec8fca977e.

* var change

* import lint

* changed locations

* second draft

* fmt

* Revert "fmt"

This reverts commit 68a1c5c872.

* double texting lint

* concepts (#704)

* concepts

* python sdk

* docs structure

* fmt

* fmt

* Small touch ups, rename Hosted LangGraph API to LangGraph Cloud. (#724)

* Fix small typos.

* Fix broken links in quick start page.

* Add LangGraph Cloud reference docs.

---------

Co-authored-by: Isaac Francisco <78627776+isahers1@users.noreply.github.com>
2024-06-20 13:38:54 -07:00

34 KiB
Raw Blame History

Initialization

Initializing client

To get started we need to initialize our client. The process for initializing our client is almost identical for both the local deployment and cloud deployment using Langsmith.

In [5]:
from langgraph_sdk import get_client

# If you deployed using Langsmith use this option
# Find this url on your Langsmith deployment page
example_deployed_url = (
    "https://ht-unhealthy-buffalo25-39d00f953458585aa9f7b5a4fa-g3ps4aazkq-uc.a.run.app"
)

# If you deployed locally using langgraph up -c langgraph.json use this option
# This is the default URL, and you can just call get_client() to use it
example_local_url = "http://localhost:8123"

client = get_client(url="whatever-your-url-is")

Selecting an Assistant

To select an assistant we can search the assistants that are hosted on our client, and then select the one we want,

In [6]:
assistants = await client.assistants.search()
assistants = [a for a in assistants if not a["config"]]
assistant = assistants[0]

In our example we are only hosting a single assistant, but you have the option to host many, in which case you will most likely want to do more filtering than just selecting the first one. Each assistant is a JSON object with the following format, allowing you to select based on a variety of parameters.

In [7]:
assistant
Out [7]:
{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',
 'graph_id': 'agent',
 'created_at': '2024-06-11T20:12:45.862108+00:00',
 'updated_at': '2024-06-11T20:12:45.862108+00:00',
 'config': {},
 'metadata': {'created_by': 'system'}}

Creating a thread

Threads are what we will actually use to run our graphs (assistants). Each thread will update the same state for the graph, meaning we can run the graph multiple times while the state will persist. We can also look back at our thread history, add meta data to different steps of our thread, and update the thread state manually if we wish. We will dive into all of those topics later in this article, but for now lets just see how to start a thread:

In [8]:
thread = await client.threads.create()

We can examine the structure of our thread, which similar to the assistants object provides us with some information about the thread itself, including its id, timestamps, and metadata:

In [9]:
thread
Out [9]:
{'thread_id': '6c2e8002-5712-4388-bed6-0747e9a86e31',
 'created_at': '2024-06-19T15:58:59.243657+00:00',
 'updated_at': '2024-06-19T15:58:59.243657+00:00',
 'metadata': {}}

Now we are ready to actually use our graph!

Invoking the graph

The graph used in this example is a simple example of a StateGraph, but it allows us to show most of the API functionality. The state of our graph is defined as follows:

In [10]:
from typing import Annotated, TypedDict

from langchain_core.messages import AnyMessage

from langgraph.graph import add_messages


def update_user_info(old_info, new_info):
    if "name" not in new_info or new_info["age"] == -1:
        return old_info
    return new_info


class UserInformation(TypedDict):
    age: int
    name: str


class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    user_info: Annotated[UserInformation, update_user_info]

It is important to note that our member variables can be updated by using the Annotated class. This is especially important when we make API calls that will update our variables. This is also a good example that your graph can hold much more information than just messages. In our example we use a very simple UserInformation class, but you can imagine holding much richer information in your state.

Our graph looks like follows:

The workflow is as follows: first the user inputs some message, our llm decides how to configure the call to our tool get_user_info , and after getting the results of the tool call we respond to our user using another LLM.

Simple Invocation

Ok, now that we have set up our client, assistant, and thread we can actually invoke the graph above. Lets first define the function we will use to invoke the graph, since we dont want to have to rewrite this code every single run.

In [48]:
async def run_input(client, thread, assistant, input, metadata={}):
    # client.runs.stream will stream the results of running our graph
    async for chunk in client.runs.stream(
        thread["thread_id"],
        assistant["assistant_id"],
        input=input,
        config={"configurable": metadata},
        stream_mode="updates",
    ):
        if chunk.data and "run_id" not in chunk.data:
            print(chunk.data)

Lets now see what happens to our graph when we run it with a simple sentence:

In [28]:
input = {
    "messages": [
        {"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}
    ]
}

await run_input(client, thread, assistant, input)
{'llm': {'messages': [{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ioeUDw39bXQ2nap5f593xZMW', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-75a0d9db-df99-4b0c-b356-f24e76f5ca5a', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_ioeUDw39bXQ2nap5f593xZMW'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'get_user_info': {'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'abafa365-1280-439d-b2ac-d4c547284ff9', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_E6NK18NXOHFkp8CeIuF7iI3K', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-c018c6a2-b93a-4787-aae3-46df0ef24c5b', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_E6NK18NXOHFkp8CeIuF7iI3K'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-fb1ac0ca-b1f8-454b-ba3f-849c266dcc05', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello! What is my name?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8bf0637b-8214-4c85-85ff-873ec193a7b8', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_6EFvGDGWHuoOKm4p7dq3qazN', 'function': {'arguments': '{"age":-1,"name":"John Doe"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-30a91dfa-4526-4bb7-b4a2-017e5c530ed2', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1, 'name': 'John Doe'}, 'id': 'call_6EFvGDGWHuoOKm4p7dq3qazN'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Your name is Bagatur. How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-f3dcd3ad-b438-4011-9353-83978758ee81', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello! My name is Bagatur and I am 26 years old.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '88e7bb12-552d-4607-8bc5-ce2b342f0604', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ioeUDw39bXQ2nap5f593xZMW', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-75a0d9db-df99-4b0c-b356-f24e76f5ca5a', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_ioeUDw39bXQ2nap5f593xZMW'}], 'invalid_tool_calls': [], 'usage_metadata': None}], 'user_info': {'age': 26, 'name': 'Bagatur'}}}
{'respond_to_user': {'messages': [{'content': 'Hello Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-49176b69-aae5-41ac-8c1e-7bab373b2b9b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

We can see our graph ran as expected, by calling our three nodes sequentially. Lets now examine the state to take a further look under the hood. We can get the state by using the following command:

In [17]:
state = await client.threads.get_state(thread_id=thread["thread_id"])
state.keys()
Out [17]:
dict_keys(['values', 'next', 'config', 'metadata', 'created_at', 'parent_config'])

Our state variable contains a variety of important information. Here is a quick summary of the keys and what they represent:

  • values contains the actual state values, so in our case you could call state['values']['messages'] or state['values']['user_info'] and get the actual values of each of the state variables.
  • next tells us what action in the graph is next at the current state. Since we just finished running our graph and reached the end node, it is currently empty because there is no next action to take. However, if you go through the state at each point of the run you will see that the next value goes from __start__llmget_user_inforespond_to_user .
  • metadata stores the metadata associated with our state. This is data that is outside of the agent state, but is important to keep track of across multiple runs. An example of this is shown in the next section.
  • config tells us what the configuration of the state is. This is important for when we want to run a query starting at a previous state instead of the one we are at. An example of this is shown in the Invoking from a previous checkpoint section

Invoking with Metadata

Lets create a new thread to reset our state and start fresh.

In [49]:
thread = await client.threads.create()

Now lets add some metadata to our request. In this example we are going to treat each run of our assistant as a separate “node”. For each run, we will pass in a “node_id” as well as a “parent_node” in the metadata. This way we can easily go “back in time” and rerun our graph from a previous checkpoint.

NOTE: The reason we add this metadata instead of using the parent_config attribute is because parent_config tracks every individual step of a run, not the entire run itself.

In [50]:
input = {
    "messages": [
        {"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}
    ]
}

metadata = {"node_id": 1, "parent_node": None}

await run_input(client, thread, assistant, input, metadata)
{'llm': {'messages': [{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-067609c1-4f1a-4d6d-bc1c-224a29153d37', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'get_user_info': {'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '05faf88c-9f85-462e-84ee-4667de35625d', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-067609c1-4f1a-4d6d-bc1c-224a29153d37', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk'}], 'invalid_tool_calls': [], 'usage_metadata': None}], 'user_info': {'age': 26, 'name': 'Bagatur'}}}
{'respond_to_user': {'messages': [{'content': 'Hello Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-48041247-fe04-419f-9f12-511e28c5f8aa', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

Using our run_input function makes it easy to pass in metadata and you can inspect the function as well as the API docs to see exactly how metadata gets passed.

We can continue our thread by creating a second node as follows:

In [51]:
input = {"messages": [{"role": "user", "content": "Hello! What is my name?"}]}
metadata = {"node_id": 2, "parent_node": 1}

await run_input(client, thread, assistant, input, metadata)
{'llm': {'messages': [{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_b2T2e0doVrh6uB1aHLIQglYR', 'function': {'arguments': '{"age":-1,"name":"John Doe"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-808dca3a-0188-4c6b-9fc7-0037949fd820', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1, 'name': 'John Doe'}, 'id': 'call_b2T2e0doVrh6uB1aHLIQglYR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'get_user_info': {'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '05faf88c-9f85-462e-84ee-4667de35625d', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-067609c1-4f1a-4d6d-bc1c-224a29153d37', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-48041247-fe04-419f-9f12-511e28c5f8aa', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello! What is my name?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '0970d6dd-e071-49bf-bf71-5345185b8291', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_b2T2e0doVrh6uB1aHLIQglYR', 'function': {'arguments': '{"age":-1,"name":"John Doe"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-808dca3a-0188-4c6b-9fc7-0037949fd820', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1, 'name': 'John Doe'}, 'id': 'call_b2T2e0doVrh6uB1aHLIQglYR'}], 'invalid_tool_calls': [], 'usage_metadata': None}], 'user_info': {'age': -1, 'name': 'John Doe'}}}
{'respond_to_user': {'messages': [{'content': 'Your name is Bagatur. How can I assist you today, Bagatur?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-9ada26f0-951e-4dd5-a2c5-4357bdeeac9d', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

Perfect! The state persisted across separate runs, and the LLM remembers the name of our user. In a future we will explore non-sequential runs, i.e. not having each run just follow the last one but choosing which checkpoint we start our run from.

Querying and Updating the thread

Getting checkpoints by metadata

Lets say we want to start a new run from a previous state (not the current state). This state lives somewhere in our history, so we can utilize the get_history function to try and find it.

In [52]:
history = await client.threads.get_history(thread_id=thread["thread_id"])

This is helpful for inspecting the specifics of our current thread, but remember that the history contains all the intermediate steps a graph takes. In our case, where the graph has 5 nodes (remember that Start and End both count as nodes), our history array grows quickly. Luckily, there is a way to query by using metadata. For example if we wanted to start a run from Node 1(from the example from above) we need to find the state from the end of run with metadata node_id:1 , which we can do like so:

In [53]:
node_1_history = await client.threads.get_history(
    thread_id=thread["thread_id"], metadata={"node_id": 1}
)
# At the end of the run there will be no 'next' for the graph to execute
node_1_end_of_run = [h for h in node_1_history if h["next"] == []][0]

Now lets explore how we could use this information to create a new branch in our thread.

Invoking from a previous checkpoint

The following diagram describes what we would like to happen:

Basically, we want to have 3 runs of our graph, but instead of having them sequentially - we want both the second and third run to originate from the same state. We can do this by utilizing the code we used above, and passing additional metadata to our run.
In [54]:
input = {"messages": [{"role": "user", "content": "Hello! What is my age?"}]}
metadata = {
    **{"node_id": 3, "parent_node": 1},
    **{
        "thread_ts": node_1_end_of_run["checkpoint_id"],
        "thread_id": thread["thread_id"],
    },
}

await run_input(client, thread, assistant, input, metadata)
{'llm': {'messages': [{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_yC7HUAQfcLzheepMD8SnAojR', 'function': {'arguments': '{"age":-1}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-b6a653e2-11ea-4f82-aa6a-ef321deed9ce', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1}, 'id': 'call_yC7HUAQfcLzheepMD8SnAojR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'get_user_info': {'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '05faf88c-9f85-462e-84ee-4667de35625d', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-067609c1-4f1a-4d6d-bc1c-224a29153d37', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-48041247-fe04-419f-9f12-511e28c5f8aa', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello! What is my age?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'e94fa535-f821-4bfb-9d96-68c414adad8d', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_yC7HUAQfcLzheepMD8SnAojR', 'function': {'arguments': '{"age":-1}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-b6a653e2-11ea-4f82-aa6a-ef321deed9ce', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1}, 'id': 'call_yC7HUAQfcLzheepMD8SnAojR'}], 'invalid_tool_calls': [], 'usage_metadata': None}], 'user_info': {'age': -1}}}
{'respond_to_user': {'messages': [{'content': 'You are 26 years old, Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-e3b6e627-b4b1-4249-a1bc-a6359ad7b961', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

To check that everything actually worked as planned, lets check our current state and check that message history to ensure that the message we passed to Node 2 is nowhere to be found.

In [55]:
"Hello! What is my name?" in [
    message["content"] for message in state["values"]["messages"]
]
Out [55]:
False

Great! This has worked as expected. Being able to go back to previous states and execute new graph runs from those checkpoints is a great way to develop flexible applications that dont require reloading or restarting everything when an error is detected or a user changes their mind.

Updating/Patching the thread state

Lastly, lets discuss the ability to manually change both the thread state as well as the metadata for a given state. Lets say we incorrectly inputted data to the LLM and we want to rectify it.

Continuing our previous example, lets say the user mistyped their age and we want to let the graph know that without actually running it. In this case we can rectify this by using update_state

In [56]:
new_state = await client.threads.update_state(
    thread_id=thread["thread_id"], values={"user_info": {"name": "Bagatur", "age": 35}}
)

Lets make sure that the state did in fact update and ask our LLM again how old we are by invoking the graph again:

In [57]:
input = {"messages": [{"role": "user", "content": "Hello! What is my age?"}]}

await run_input(client, thread, assistant, input)
{'llm': {'messages': [{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_Ph3tYdt2UNdwqAF3kVL198Bg', 'function': {'arguments': '{"age":-1}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-373f3e22-8549-4f77-9ff2-05133099bb09', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1}, 'id': 'call_Ph3tYdt2UNdwqAF3kVL198Bg'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'get_user_info': {'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '05faf88c-9f85-462e-84ee-4667de35625d', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk', 'function': {'arguments': '{"age":26,"name":"Bagatur"}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-067609c1-4f1a-4d6d-bc1c-224a29153d37', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': 26, 'name': 'Bagatur'}, 'id': 'call_VrA7UKg2w99BvIpsqrwjoawk'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-48041247-fe04-419f-9f12-511e28c5f8aa', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello! What is my age?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'e94fa535-f821-4bfb-9d96-68c414adad8d', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_yC7HUAQfcLzheepMD8SnAojR', 'function': {'arguments': '{"age":-1}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-b6a653e2-11ea-4f82-aa6a-ef321deed9ce', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1}, 'id': 'call_yC7HUAQfcLzheepMD8SnAojR'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'You are 26 years old, Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-e3b6e627-b4b1-4249-a1bc-a6359ad7b961', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Hello! What is my age?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '24ac02be-7b08-4e91-9d20-90507ed25391', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_Ph3tYdt2UNdwqAF3kVL198Bg', 'function': {'arguments': '{"age":-1}', 'name': 'PersonalInfo'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-373f3e22-8549-4f77-9ff2-05133099bb09', 'example': False, 'tool_calls': [{'name': 'PersonalInfo', 'args': {'age': -1}, 'id': 'call_Ph3tYdt2UNdwqAF3kVL198Bg'}], 'invalid_tool_calls': [], 'usage_metadata': None}], 'user_info': {'age': -1}}}
{'respond_to_user': {'messages': [{'content': 'My apologies for the confusion earlier. You are 35 years old, Bagatur! How can I assist you today?', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-298306df-34a5-459a-a8c3-a0f16440c429', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

Voila! The LLM knows our users age updated without us having to prompt it at all.

The last thing we will talk about is patching the thread, which is used when we want to update the metadata of a state. For example, say we actually wanted to update our last state to have node_id:4 instead of node_id:3. To do this, we can call:

In [58]:
await client.threads.patch_state(thread_id=thread["thread_id"], metadata={"node_id": 4})
Out [58]:
{'configurable': {'thread_id': '4f044e5a-6f6e-4663-923e-6333c052ce9f',
  'thread_ts': '1ef2e5d6-e39f-6d26-800e-22245f8220a7'}}

We can check that this worked by checking the metadata of our state

In [59]:
state = await client.threads.get_state(thread_id=thread["thread_id"])
print(f"Current node id is {state['metadata']['node_id']}")
Current node id is 4

Perfect! The patch worked as expected.