Merge branch 'main' into vb/update-get-state

This commit is contained in:
vbarda
2024-07-23 18:51:36 -04:00
15 changed files with 1001 additions and 41 deletions
+3
View File
@@ -27,6 +27,8 @@ _MANUAL = {
"streaming-events-from-within-tools-without-langchain.ipynb",
"streaming-from-final-node.ipynb",
"persistence.ipynb",
"input_output_schema.ipynb",
"pass_private_state.ipynb",
"memory/manage-conversation-history.ipynb",
"memory/delete-messages.ipynb",
"memory/add-summary-conversation-history.ipynb",
@@ -98,6 +100,7 @@ _HIDE = set(
"learning.ipynb",
"docs/quickstart.ipynb",
"tutorials/rag-agent-testing.ipynb",
"tutorials/rag-agent-testing-local.ipynb",
"time-travel.ipynb",
"code_assistant/langgraph_code_assistant_mistral.ipynb",
]
+19 -1
View File
@@ -19,6 +19,24 @@ After each step, an example file directory is provided to demonstrate how code c
Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
```
langgraph>=0.1.7
langchain-core>=0.2.7
orjson>=3.10.1
langsmith>=0.1.50
httpx>=0.27.0
langchain-core>=0.2.8
langsmith>=0.1.63
tenacity>=8.3.0
uvicorn>=0.29.0
sse-starlette>=2.1.0
uvloop>=0.19.0
httptools>=0.6.1
jsonschema-rs>=0.18.0
croniter>=1.0.1
```
Example `requirements.txt` file:
```
langgraph
@@ -121,4 +139,4 @@ To deploy the LangGraph application to LangGraph Cloud, the code must be uploade
## Next
After you setup your repo, it's time to [deploy your app](./cloud.md).
After you setup your repo, it's time to [deploy your app](./cloud.md).
+19 -1
View File
@@ -20,6 +20,24 @@ After each step, an example file directory is provided to demonstrate how code c
Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
```
langgraph>=0.1.7
langchain-core>=0.2.7
orjson>=3.10.1
langsmith>=0.1.50
httpx>=0.27.0
langchain-core>=0.2.8
langsmith>=0.1.63
tenacity>=8.3.0
uvicorn>=0.29.0
sse-starlette>=2.1.0
uvloop>=0.19.0
httptools>=0.6.1
jsonschema-rs>=0.18.0
croniter>=1.0.1
```
Example `pyproject.toml` file:
```toml
@@ -33,7 +51,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9.0,<3.13"
langgraph = "^0.1.0"
langgraph = "^0.1.7"
langchain-fireworks = "^0.1.3"
+13
View File
@@ -46,6 +46,9 @@ The first thing you do when you define a graph is define the `State` of the grap
The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/state-model.ipynb) as your graph state to add **default values** and additional data validation.
By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [notebook here](../how-tos/input_output_schema.ipynb) for how to use.
By default, all nodes in the graph will share the same state. This means that they will read and write to the same state channels. It is possible to have nodes write to private state channels inside the graph for internal node communication - see [this notebook](../how-tos/pass_private_state.ipynb) for how to do that.
### Reducers
Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. Let's take a look at a few examples to understand them better.
@@ -327,6 +330,16 @@ The final thing you specify when calling `update_state` is `as_node`. This updat
The reason this matters is that the next steps in the graph to execute depend on the last node to have given an update, so this can be used to control which node executes next.
## Graph Migrations
LangGraph can easily handle migrations of graph definitions (nodes, edges, and state) even when using a checkpointer to track state.
- For threads at the end of the graph (i.e. not interrupted) you can change the entire topology of the graph (i.e. all nodes and edges, remove, add, rename, etc)
- For threads currently interrupted, we support all topology changes other than renaming / removing nodes (as that thread could now be about to enter a node that no longer exists) -- if this is a blocker please reach out and we can prioritize a solution.
- For modifying state, we have full backwards and forwards compatibility for adding and removing keys
- State keys that are renamed lose their saved state in existing threads
- State keys whose types change in incompatible ways could currently cause issues in threads with state from before the change -- if this is a blocker please reach out and we can prioritize a solution.
## Configuration
When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it.
+5 -2
View File
@@ -157,12 +157,15 @@ nav:
- Handle tool calling errors: how-tos/tool-calling-errors.ipynb
- Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb
- Pass config to tools: how-tos/pass-config-to-tools.ipynb
- State Management:
- Use Pydantic model as state: how-tos/state-model.ipynb
- Use a context object in state: how-tos/state-context-key.ipynb
- Have a separate input and output schema: how-tos/input_output_schema.ipynb
- Pass private state between nodes inside the graph: how-tos/pass_private_state.ipynb
- Other:
- Run graph asynchronously: how-tos/async.ipynb
- Visualize your graph: how-tos/visualization.ipynb
- Add runtime configuration: how-tos/configuration.ipynb
- Use Pydantic model as state: how-tos/state-model.ipynb
- Use a context object in state: how-tos/state-context-key.ipynb
- Add node retries: how-tos/node-retries.ipynb
- Prebuilt ReAct Agent:
- Create a ReAct agent: how-tos/create-react-agent.ipynb
+93
View File
@@ -0,0 +1,93 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "f262985e-e973-4a27-9c9e-dbb3a06a35b7",
"metadata": {},
"source": [
"# How to define input/output schema for your graph\n",
"\n",
"By default, `StateGraph` takes in a single schema and all nodes are expected to communicate with that schema. However, it is also possible to define explicit input and output schemas for a graph. This is helpful if you want to draw a distinction between input and output keys.\n",
"\n",
"In this notebook we'll walk through an example of this. At a high level, in order to do this you simply have to pass in `input=..., output=...` when defining the graph. Let's see an example below!"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "6ec0eb77-874e-443e-8c73-93125b515106",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'answer': 'bye'}"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from langgraph.graph import StateGraph, START, END\n",
"from typing import TypedDict\n",
"\n",
"class InputState(TypedDict):\n",
" question: str\n",
"\n",
"class OutputState(TypedDict):\n",
" answer: str\n",
"\n",
"def answer_node(state: InputState):\n",
" return {\"answer\": \"bye\"}\n",
"\n",
"check = SqliteSaver.from_conn_string(\":memory:\")\n",
"graph = StateGraph(input=InputState, output=OutputState)\n",
"graph.add_node(answer_node)\n",
"graph.add_edge(START, \"answer_node\")\n",
"graph.add_edge(\"answer_node\", END)\n",
"graph = graph.compile()\n",
"\n",
"graph.invoke({\"question\": \"hi\"})"
]
},
{
"cell_type": "markdown",
"id": "6a68836f-98e1-4684-a8a6-c1473c73460c",
"metadata": {},
"source": [
"Notice that the output of invoke only includes the output schema."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b952a554-f2a4-4be3-81ab-2e08f0f441c2",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+126
View File
@@ -0,0 +1,126 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "47ed5db3-bda5-49e1-bf75-23e08c9a3af0",
"metadata": {},
"source": [
"# How to pass private state\n",
"\n",
"Oftentimes, you may want nodes to be able to pass state to eachv other that should NOT be part of the main schema of the graph. This is often useful because there may be information that is not needed as input/output (and therefore doesn't really make sense to have in the main schema) but is ABSOLUTELY needed as part of the intermediate working logic.\n",
"\n",
"Let's take a look at an example below. In this example, we will create a RAG pipeline that:\n",
"1. Takes in a user question\n",
"2. Uses an LLM to generate a search query\n",
"3. Retrieves documents for that generated query\n",
"4. Generates a final answer based on those documents\n",
"\n",
"We will have a separate node for each step. We will only have the `question` and `answer` on the overall state. However, we will need separate states for the `search_query` and the `documents` - we will pass these as private state keys.\n",
"\n",
"Let's look at an example!"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "3114c3ad-0ade-47ba-9488-53d6f7671578",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'question': 'foo', 'answer': 'fo\\n\\nfo\\n\\nfoo'}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from langgraph.graph import StateGraph, START, END\n",
"from typing import TypedDict\n",
"\n",
"\n",
"# The overall state of the graph\n",
"class OverallState(TypedDict):\n",
" question: str\n",
" answer: str\n",
"\n",
"\n",
"# This is what the node that generates the query will return\n",
"class QueryOutputState(TypedDict):\n",
" query: str\n",
"\n",
"\n",
"# This is what the node that retrieves the documents will return\n",
"class DocumentOutputState(TypedDict):\n",
" docs: list[str]\n",
"\n",
"\n",
"# This is what the node that generates the final answer will take in\n",
"class GenerateInputState(OverallState, DocumentOutputState):\n",
" pass\n",
"\n",
"\n",
"# Node to generate query\n",
"def generate_query(state: OverallState) -> QueryOutputState:\n",
" # Replace this with real logic\n",
" return {\"query\": state[\"question\"][:2]}\n",
"\n",
"\n",
"# Node to retrieve documents\n",
"def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n",
" # Replace this with real logic\n",
" return {\"docs\": [state['query']] * 2}\n",
"\n",
"\n",
"# Node to generate answer\n",
"def generate(state: GenerateInputState) -> OverallState:\n",
" return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n",
"\n",
"\n",
"graph = StateGraph(OverallState)\n",
"graph.add_node(generate_query)\n",
"graph.add_node(retrieve_documents)\n",
"graph.add_node(generate)\n",
"graph.add_edge(START, \"generate_query\")\n",
"graph.add_edge(\"generate_query\", \"retrieve_documents\")\n",
"graph.add_edge(\"retrieve_documents\", \"generate\")\n",
"graph.add_edge(\"generate\", END)\n",
"graph = graph.compile()\n",
"\n",
"graph.invoke({\"question\": \"foo\"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3ffc2d8c-717f-42c9-b0aa-15b178a5cc8b",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
@@ -347,6 +347,49 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
),
)
async def alist_subgraph_checkpoints(
self, config: RunnableConfig
) -> AsyncIterator[CheckpointTuple]:
async with self.conn.cursor() as cur:
if config["configurable"].get("thread_ts"):
cur.execute(
"SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints WHERE thread_id LIKE ? || '%' AND thread_ts = ?",
(
str(config["configurable"]["thread_id"]),
str(config["configurable"]["thread_ts"]),
),
)
else:
cur.execute(
"""SELECT checkpoints.thread_id, checkpoints.thread_ts, checkpoints.parent_ts, checkpoints.checkpoint, checkpoints.metadata
FROM checkpoints
INNER JOIN (
SELECT thread_id, MAX(thread_ts) as thread_ts
FROM checkpoints
WHERE thread_id LIKE ? || '%'
GROUP BY thread_id
) latest_checkpoints
ON checkpoints.thread_id = latest_checkpoints.thread_id AND checkpoints.thread_ts = latest_checkpoints.thread_ts
ORDER BY checkpoints.thread_id, checkpoints.thread_ts DESC""",
(str(config["configurable"]["thread_id"]),),
)
async for thread_id, thread_ts, parent_ts, value, metadata in cur:
yield CheckpointTuple(
{"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}},
self.serde.loads(value),
self.serde.loads(metadata) if metadata is not None else {},
(
{
"configurable": {
"thread_id": thread_id,
"thread_ts": parent_ts,
}
}
if parent_ts
else None
),
)
async def aput(
self,
config: RunnableConfig,
@@ -328,6 +328,11 @@ class BaseCheckpointSaver(ABC):
raise NotImplementedError
yield
async def alist_subgraph_checkpoints(
self, config: RunnableConfig
) -> AsyncIterator[CheckpointTuple]:
raise NotImplementedError
async def aput(
self,
config: RunnableConfig,
+4 -4
View File
@@ -358,8 +358,8 @@ class StateGraph(Graph):
raise ValueError("END cannot be a start node")
if start not in self.nodes:
raise ValueError(f"Need to add_node `{start}` first")
if end_key == END:
raise ValueError("END cannot be an end node")
if end_key == START:
raise ValueError("START cannot be an end node")
if end_key not in self.nodes:
raise ValueError(f"Need to add_node `{end_key}` first")
@@ -371,7 +371,7 @@ class StateGraph(Graph):
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
debug: bool = False,
) -> CompiledGraph:
) -> "CompiledStateGraph":
"""Compiles the state graph into a `CompiledGraph` object.
The compiled graph implements the `Runnable` interface and can be invoked,
@@ -386,7 +386,7 @@ class StateGraph(Graph):
debug (bool): A flag indicating whether to enable debug mode.
Returns:
CompiledGraph: The compiled state graph.
CompiledStateGraph: The compiled state graph.
"""
# assign default values
interrupt_before = interrupt_before or []
+3 -3
View File
@@ -99,7 +99,7 @@ class PregelLoop:
checkpoint: Checkpoint
checkpoint_config: RunnableConfig
checkpoint_metadata: CheckpointMetadata
checkpoint_pending_writes: Optional[List[PendingWrite]]
checkpoint_pending_writes: List[PendingWrite]
step: int
status: Literal[
@@ -406,7 +406,7 @@ class SyncPregelLoop(PregelLoop, ContextManager):
}
self.checkpoint = copy_checkpoint(saved.checkpoint)
self.checkpoint_metadata = saved.metadata
self.checkpoint_pending_writes = saved.pending_writes
self.checkpoint_pending_writes = saved.pending_writes or []
self.submit = self.stack.enter_context(BackgroundExecutor(self.config))
self.channels = self.stack.enter_context(
@@ -484,7 +484,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
}
self.checkpoint = copy_checkpoint(saved.checkpoint)
self.checkpoint_metadata = saved.metadata
self.checkpoint_pending_writes = saved.pending_writes
self.checkpoint_pending_writes = saved.pending_writes or []
self.submit = await self.stack.enter_async_context(AsyncBackgroundExecutor())
self.channels = await self.stack.enter_async_context(
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.1.10"
version = "0.1.11"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
+4 -3
View File
@@ -8228,7 +8228,8 @@ def test_nested_graph_interrupts(
),
]
child_state_history = [
c for c in app.get_state_history({"configurable": {"thread_id": "6-inner"}})
c
for c in app.get_state_history({"configurable": {"thread_id": "6__inner"}})
]
assert child_state_history == [
StateSnapshot(
@@ -8236,7 +8237,7 @@ def test_nested_graph_interrupts(
next=(),
config={
"configurable": {
"thread_id": "6-inner",
"thread_id": "6__inner",
"thread_ts": AnyStr(),
}
},
@@ -8253,7 +8254,7 @@ def test_nested_graph_interrupts(
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "6-inner",
"thread_id": "6__inner",
"thread_ts": AnyStr(),
}
},
+26 -26
View File
@@ -283,7 +283,7 @@ async def test_cancel_graph_astream(
# test interrupting astream
got_event = False
thread1: RunnableConfig = {"configurable": {"thread_id": 1}}
thread1: RunnableConfig = {"configurable": {"thread_id": "1"}}
async with aclosing(graph.astream({"value": 1}, thread1)) as stream:
async for chunk in stream:
assert chunk == {"alittlewhile": {"value": 2}}
@@ -368,7 +368,7 @@ async def test_cancel_graph_astream_events_v2(
# test interrupting astream_events v2
got_event = False
thread2: RunnableConfig = {"configurable": {"thread_id": 2}}
thread2: RunnableConfig = {"configurable": {"thread_id": "2"}}
async with aclosing(
graph.astream_events({"value": 1}, thread2, version="v2")
) as stream:
@@ -685,53 +685,53 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
)
# start execution, stop at inbox
assert await app.ainvoke(2, {"configurable": {"thread_id": 1}}) is None
assert await app.ainvoke(2, {"configurable": {"thread_id": "1"}}) is None
# inbox == 3
checkpoint = await memory.aget({"configurable": {"thread_id": 1}})
checkpoint = await memory.aget({"configurable": {"thread_id": "1"}})
assert checkpoint is not None
assert checkpoint["channel_values"]["inbox"] == 3
# resume execution, finish
assert await app.ainvoke(None, {"configurable": {"thread_id": 1}}) == 4
assert await app.ainvoke(None, {"configurable": {"thread_id": "1"}}) == 4
# start execution again, stop at inbox
assert await app.ainvoke(20, {"configurable": {"thread_id": 1}}) is None
assert await app.ainvoke(20, {"configurable": {"thread_id": "1"}}) is None
# inbox == 21
checkpoint = await memory.aget({"configurable": {"thread_id": 1}})
checkpoint = await memory.aget({"configurable": {"thread_id": "1"}})
assert checkpoint is not None
assert checkpoint["channel_values"]["inbox"] == 21
# send a new value in, interrupting the previous execution
assert await app.ainvoke(3, {"configurable": {"thread_id": 1}}) is None
assert await app.ainvoke(None, {"configurable": {"thread_id": 1}}) == 5
assert await app.ainvoke(3, {"configurable": {"thread_id": "1"}}) is None
assert await app.ainvoke(None, {"configurable": {"thread_id": "1"}}) == 5
# start execution again, stopping at inbox
assert await app.ainvoke(20, {"configurable": {"thread_id": 2}}) is None
assert await app.ainvoke(20, {"configurable": {"thread_id": "2"}}) is None
# inbox == 21
snapshot = await app.aget_state({"configurable": {"thread_id": 2}})
snapshot = await app.aget_state({"configurable": {"thread_id": "2"}})
assert snapshot.values["inbox"] == 21
assert snapshot.next == ("two",)
# update the state, resume
await app.aupdate_state({"configurable": {"thread_id": 2}}, 25, as_node="one")
assert await app.ainvoke(None, {"configurable": {"thread_id": 2}}) == 26
await app.aupdate_state({"configurable": {"thread_id": "2"}}, 25, as_node="one")
assert await app.ainvoke(None, {"configurable": {"thread_id": "2"}}) == 26
# no pending tasks
snapshot = await app.aget_state({"configurable": {"thread_id": 2}})
snapshot = await app.aget_state({"configurable": {"thread_id": "2"}})
assert snapshot.next == ()
# list history
thread1 = {"configurable": {"thread_id": 1}}
thread1 = {"configurable": {"thread_id": "1"}}
assert [c async for c in app.aget_state_history(thread1)] == [
StateSnapshot(
values={"inbox": 4, "output": 5, "input": 3},
next=(),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -744,7 +744,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=("two",),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -757,7 +757,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=("one",),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -770,7 +770,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=("two",),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -783,7 +783,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=("one",),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -796,7 +796,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=(),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -809,7 +809,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=("two",),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -822,7 +822,7 @@ async def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> N
next=("one",),
config={
"configurable": {
"thread_id": 1,
"thread_id": "1",
"thread_ts": AnyStr(),
}
},
@@ -6754,7 +6754,7 @@ async def test_nested_graph_interrupts(
child_state_history = [
c
async for c in app.aget_state_history(
{"configurable": {"thread_id": "6-inner"}}
{"configurable": {"thread_id": "6__inner"}}
)
]
assert child_state_history == [
@@ -6763,7 +6763,7 @@ async def test_nested_graph_interrupts(
next=(),
config={
"configurable": {
"thread_id": "6-inner",
"thread_id": "6__inner",
"thread_ts": AnyStr(),
}
},
@@ -6780,7 +6780,7 @@ async def test_nested_graph_interrupts(
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "6-inner",
"thread_id": "6__inner",
"thread_ts": AnyStr(),
}
},