Skip to content

如何编辑已部署图的状态

在创建LangGraph代理时,通常希望添加一个人机交互组件。这在向它们提供工具访问权限时特别有用。在这种情况下,您可能希望在继续之前编辑图的状态(例如,编辑正在调用的工具或调用方式)。

这可以通过多种方式实现,但主要支持的方式是在一个节点执行之前添加一个“中断”。这会在该节点处中断执行。然后,您可以使用update_state更新状态,然后从该位置继续执行。

设置

我们不会展示托管图的完整代码,但如果你想查看的话,可以在这里查看:这里。一旦该图被托管,我们就可以调用它并等待用户输入。

SDK初始化

首先,我们需要设置客户端,以便能够与托管的图进行通信:

from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
# 使用名称为"agent"的图
assistant_id = "agent"
thread = await client.threads.create()
import { Client } from "@langchain/langgraph-sdk";

const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// 使用名称为"agent"的图
const assistantId = "agent";
const thread = await client.threads.create();
curl --request POST \
  --url <DEPLOYMENT_URL>/threads \
  --header 'Content-Type: application/json' \
  --data '{}'

编辑状态

初始调用

现在让我们调用我们的图,确保在action节点之前中断。

input = { 'messages':[{ "role":"user", "content":"搜索旧金山的天气" }] }

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id,
    input=input,
    stream_mode="updates",
    interrupt_before=["action"],
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
const input = { messages: [{ role: "human", content: "搜索旧金山的天气" }] };

const streamResponse = client.runs.stream(
  thread["thread_id"],
  assistantId,
  {
    input: input,
    streamMode: "updates",
    interruptBefore: ["action"],
  }
);

for await (const chunk of streamResponse) {
  if (chunk.data && chunk.event !== "metadata") {
    console.log(chunk.data);
  }
}
curl --request POST \
 --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
 --header 'Content-Type: application/json' \
 --data "{
   \"assistant_id\": \"agent\",
   \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"搜索旧金山的天气\"}]},
   \"interrupt_before\": [\"action\"],
   \"stream_mode\": [
     \"updates\"
   ]
 }" | \
 sed 's/\r$//' | \
 awk '
 /^event:/ {
     if (data_content != "" && event_type != "metadata") {
         print data_content "\n"
     }
     sub(/^event: /, "", $0)
     event_type = $0
     data_content = ""
 }
 /^data:/ {
     sub(/^data: /, "", $0)
     data_content = $0
 }
 END {
     if (data_content != "" && event_type != "metadata") {
         print data_content "\n"
     }
 }
 '

输出:

{'agent': {'messages': [{'content': [{'text': "当然!我将为您搜索旧金山当前的天气。这是我的操作步骤:", 'type': 'text'}, {'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-6dbb0167-f8f6-4e2a-ab68-229b2d1fbb64', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

编辑状态

现在,假设我们实际上想搜索塞迪弗雷(另一个以SF为缩写的城镇)的天气。我们可以编辑状态以正确反映这一点:

# 首先,让我们获取当前状态
current_state = await client.threads.get_state(thread['thread_id'])

# 现在获取状态中的最后一条消息
# 这是包含我们想要更新的工具调用的消息
last_message = current_state['values']['messages'][-1]

# 现在更新该工具调用的参数
last_message['tool_calls'][0]['args'] = {'query': 'current weather in Sidi Frej'}

# 现在调用`update_state`以在`messages`键中传递此消息
# 这将被视为对状态的任何其他更新
# 它将传递给`messages`键的reducer函数
# 该reducer函数将使用消息的ID进行更新
# 重要的是它具有正确的ID!否则它会被添加为一条新消息
await client.threads.update_state(thread['thread_id'], {"messages": last_message})
// 首先,让我们获取当前状态
const currentState = await client.threads.getState(thread["thread_id"]);

// 现在获取状态中的最后一条消息
// 这是包含我们想要更新的工具调用的消息
let lastMessage = currentState.values.messages.slice(-1)[0];

// 现在更新该工具调用的参数
lastMessage.tool_calls[0].args = { query: "current weather in Sidi Frej" };

// 现在调用`update_state`以在`messages`键中传递此消息
// 这将被视为对状态的任何其他更新
// 它将传递给`messages`键的reducer函数
// 该reducer函数将使用消息的ID进行更新
// 重要的是它具有正确的ID!否则它会被添加为一条新消息
await client.threads.updateState(thread["thread_id"], { values: { messages: lastMessage } });
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \                                                                                      
jq '.values.messages[-1] | (.tool_calls[0].args = {"query": "current weather in Sidi Frej"})' | \
curl --request POST \
  --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
  --header 'Content-Type: application/json' \
  --data @-

输出:

{'configurable': {'thread_id': '9c8f1a43-9dd8-4017-9271-2c53e57cf66a',
  'checkpoint_ns': '',
  'checkpoint_id': '1ef58e7e-3641-649f-8002-8b4305a64858'}}

恢复调用

现在我们可以恢复图的运行,但使用更新后的状态:

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id,
    input=None,
    stream_mode="updates",
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
const streamResponse = client.runs.stream(
  thread["thread_id"],
  assistantId,
  {
    input: null,
    streamMode: "updates",
  }
);

for await (const chunk of streamResponse) {
  if (chunk.data && chunk.event !== "metadata") {
    console.log(chunk.data);
  }
}
curl --request POST \                                                                             
 --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
 --header 'Content-Type: application/json' \
 --data "{
   \"assistant_id\": \"agent\",
   \"stream_mode\": [
     \"updates\"
   ]
 }"| \ 
 sed 's/\r$//' | \
 awk '
 /^event:/ {
     if (data_content != "" && event_type != "metadata") {
         print data_content "\n"
     }
     sub(/^event: /, "", $0)
     event_type = $0
     data_content = ""
 }
 /^data:/ {
     sub(/^data: /, "", $0)
     data_content = $0
 }
 END {
     if (data_content != "" && event_type != "metadata") {
         print data_content "\n"
     }
 }
 '

输出:

{'action': {'messages': [{'content': '["我查看了:当前塞迪弗雷的天气。结果:旧金山阳光明媚,但如果你是双子座,你最好小心。 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '1161b8d1-bee4-4188-9be8-698aecb69f10', 'tool_call_id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}]}} 
{'agent': {'messages': [{'content': [{'text': '我为我的搜索查询中的混淆道歉。似乎搜索功能将“SF”解释为“塞迪弗雷”而不是我们所期望的“旧金山”。让我再次使用完整的城市名称来获取正确的信息:', 'type': 'text'}, {'id': 'toolu_0111rrwgfAcmurHZn55qjqTR', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b8c25779-cfb4-46fc-a421-48553551242f', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_0111rrwgfAcmurHZn55qjqTR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'action': {'messages': [{'content': '["我查看了:当前旧金山的天气。结果:旧金山阳光明媚,但如果你是双子座,你最好小心。 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '6bc632ae-5ee6-4d01-9532-79c524a2d443', 'tool_call_id': 'toolu_0111rrwgfAcmurHZn55qjqTR'}]}} 
{'agent': {'messages': [{'content': "现在,根据搜索结果,我可以为您提供旧金山当前天气的信息:\n\n旧金山目前天气晴好。\n\n值得注意的是,搜索结果中包含了一个关于双子座的不寻常评论,这似乎与天气无关。这可能是由于搜索引擎在结果中包含了某些占星信息或笑话。然而,为了天气信息,我们可以专注于目前旧金山阳光明媚。\n\n您是否还想了解旧金山或其他地方的天气信息?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-227a042b-dd97-476e-af32-76a3703af5d8', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}

如您所见,它现在搜索了塞迪弗雷的当前天气(尽管我们的虚拟搜索节点仍然返回旧金山的天气结果,因为我们在这个示例中实际上并没有进行搜索,而是每次都返回相同的“旧金山阳光明媚...”结果)。

Comments