Compare commits

..
35 Commits
Author SHA1 Message Date
Nuno Campos 85af0603fa lib0.1.13 2024-07-24 09:49:23 -07:00
Nuno CamposandGitHub e6e5911aae Merge pull request #1119 from langchain-ai/nc/24jul/avoid-crash-missing-node
Avoid crash when a node in pending sends is removed
2024-07-24 09:41:20 -07:00
Nuno CamposandGitHub 9fdbd0dd49 Merge pull request #1118 from langchain-ai/nc/24jul/fix-recursion-limit-thread
Fix recursion limit considering steps taken in previous runs on same thread
2024-07-24 09:35:12 -07:00
Nuno Campos 8a20c6f7e4 Avoid crash when a node in pending sends is removed 2024-07-24 09:29:38 -07:00
Nuno Campos 03f9b27e3b Fix 2024-07-24 09:26:49 -07:00
Nuno CamposandGitHub 19f6f7d5ca Merge pull request #1110 from langchain-ai/wfh/test_pending_writes_null
Test Null Pending Writes
2024-07-24 08:51:33 -07:00
Nuno Campos 745b96eb63 Fix recursion limit considering steps taken in previous runs on same thread 2024-07-24 08:50:32 -07:00
b90d44d97d Add local tool calling agent example (#1109)
* Add local tool calling agent example

* Update copy_notebooks.py

---------

Co-authored-by: Nuno Campos <nuno@langchain.dev>
2024-07-24 07:39:53 -07:00
Vadym BardaandGitHub d99d3e05c7 langgraph: release 0.1.12 (#1116) 2024-07-24 10:38:07 -04:00
Vadym BardaandGitHub ca6aef4746 langgraph: bring back tool content stringify (#1114) 2024-07-24 10:36:32 -04:00
William Fu-Hinthorn f5b9e463e3 Merge branch 'main' into wfh/test_pending_writes_null 2024-07-23 21:28:51 -07:00
William FHandGitHub 82cbe25be8 Update typedoc build (#1087) 2024-07-23 21:27:40 -07:00
William Fu-Hinthorn 36505d9656 Add test 2024-07-23 21:15:53 -07:00
Lance MartinandGitHub b72ea0ea1b Update MR docs (#1098) 2024-07-23 17:13:20 -07:00
Vadym BardaandGitHub 603a137d33 langgraph: release 0.1.11 (#1107) 2024-07-23 18:06:07 -04:00
Nuno CamposandGitHub 4cdb38d337 Fix null pending writes (#1106) 2024-07-23 17:59:16 -04:00
Nuno Campos 03bf1e5414 Fix null pending writes 2024-07-23 14:55:07 -07:00
Nuno CamposandGitHub 4fa5c8bab0 Merge pull request #1089 from mihaic/patch-1
Fix StateGraph.add_edge end_key check
2024-07-23 13:52:33 -07:00
Nuno CamposandGitHub 9711f39995 Merge pull request #1090 from langchain-ai/harrison/private-state
Harrison/private state
2024-07-23 13:50:02 -07:00
Nuno CamposandGitHub 091bc003ae Merge pull request #1088 from langchain-ai/vb/update-return-type
langgraph: add proper type annotation for StateGraph.compile
2024-07-23 13:49:12 -07:00
Nuno CamposandGitHub 57c40026f8 Update pass_private_state.ipynb 2024-07-23 13:46:56 -07:00
Nuno CamposandGitHub bf61f6f869 Merge pull request #1102 from langchain-ai/an/23jul/docs-langgraph-cloud-constraints
docs: Add constraints info to LangGraph Cloud setup docs
2024-07-23 13:45:30 -07:00
Nuno Campos 6c299a1b5a Fix docs build 2024-07-23 13:42:57 -07:00
Nuno CamposandGitHub aeae796e60 Update setup_pyproject.md 2024-07-23 13:29:56 -07:00
Nuno CamposandGitHub 7ec37575de Update setup.md 2024-07-23 13:29:43 -07:00
Nuno CamposandGitHub 4fdd05532d Merge pull request #1100 from langchain-ai/harrison/migrations
migrations of graph
2024-07-23 13:26:18 -07:00
Nuno CamposandGitHub 43c793f7fe Update low_level.md 2024-07-23 13:24:46 -07:00
Lance MartinandGitHub 1765000263 Local RAG agent testing with Llama3.1 (#1104) 2024-07-23 13:05:51 -07:00
Andrew Nguonly 73e0871345 Add constraints info to LangGraph Cloud setup docs. 2024-07-23 08:56:56 -07:00
Harrison Chase c8f046869b migrations of graph 2024-07-23 08:20:03 -07:00
Harrison Chase 9483da1bdb cr 2024-07-22 16:31:14 -07:00
Harrison Chase 8d3da565c4 cr 2024-07-22 16:30:34 -07:00
Harrison Chase fdbb33ddad add private state 2024-07-22 16:26:31 -07:00
Mihai CapotăandGitHub 3f43e0d945 Fix StateGraph.add_edge end_key check
It looks like a typo to me: `END` should be `START`.

`END` should be accepted as `end_key` and it is accepted with the `super` call path.
2024-07-22 15:55:19 -07:00
vbarda d00503ecc4 langgraph: add proper type annotation for StateGraph.compile 2024-07-22 18:03:16 -04:00
25 changed files with 1501 additions and 2415 deletions
+11 -3
View File
@@ -1,6 +1,14 @@
.PHONY: build-docs serve-docs serve-clean-docs clean-docs codespell
.PHONY: build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc
build-docs:
build-typedoc:
cd libs/sdk-js && yarn install --include-dev && yarn typedoc
cd libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
# Add links to the monorepo
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md
build-docs: build-typedoc
poetry run python docs/_scripts/copy_notebooks.py
poetry run python -m mkdocs build --clean -f docs/mkdocs.yml --strict
@@ -8,7 +16,7 @@ serve-clean-docs: clean-docs
poetry run python docs/_scripts/copy_notebooks.py
poetry run python -m mkdocs serve -c -f docs/mkdocs.yml --strict -w ./libs/langgraph
serve-docs:
serve-docs: build-typedoc
poetry run python docs/_scripts/copy_notebooks.py
poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph --dirty
+1
View File
@@ -1,3 +1,4 @@
*.ipynb
site/
docs/tutorials/**/*.png
docs/cloud/reference/sdk/js_ts_sdk_ref.md
+4
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,8 @@ _HIDE = set(
"learning.ipynb",
"docs/quickstart.ipynb",
"tutorials/rag-agent-testing.ipynb",
"tutorials/rag-agent-testing-local.ipynb",
"tutorials/tool-calling-agent-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"
File diff suppressed because it is too large Load Diff
+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
}
File diff suppressed because one or more lines are too long
+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
}
+88 -11
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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 []
+23 -2
View File
@@ -1,4 +1,5 @@
import asyncio
import json
from copy import copy
from typing import (
Any,
@@ -28,6 +29,16 @@ INVALID_TOOL_NAME_ERROR_TEMPLATE = (
TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
def str_output(output: Any) -> str:
if isinstance(output, str):
return output
else:
try:
return json.dumps(output)
except Exception:
return str(output)
class ToolNode(RunnableCallable):
"""A node that runs the tools called in the last AIMessage.
@@ -94,7 +105,12 @@ class ToolNode(RunnableCallable):
try:
input = {**call, **{"type": "tool_call"}}
return self.tools_by_name[call["name"]].invoke(input, config)
tool_message: ToolMessage = self.tools_by_name[call["name"]].invoke(
input, config
)
# TODO: handle this properly in core
tool_message.content = str_output(tool_message.content)
return tool_message
except Exception as e:
if not self.handle_tool_errors:
raise e
@@ -106,7 +122,12 @@ class ToolNode(RunnableCallable):
return invalid_tool_message
try:
input = {**call, **{"type": "tool_call"}}
return await self.tools_by_name[call["name"]].ainvoke(input, config)
tool_message: ToolMessage = await self.tools_by_name[call["name"]].ainvoke(
input, config
)
# TODO: handle this properly in core
tool_message.content = str_output(tool_message.content)
return tool_message
except Exception as e:
if not self.handle_tool_errors:
raise e
+3
View File
@@ -257,6 +257,9 @@ def prepare_next_tasks(
if not isinstance(packet, Send):
logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends")
continue
if packet.node not in processes:
logger.warn(f"Ignoring unknown node name {packet.node} in pending sends")
continue
if for_execution:
proc = processes[packet.node]
if node := proc.get_node():
+7 -4
View File
@@ -99,9 +99,10 @@ class PregelLoop:
checkpoint: Checkpoint
checkpoint_config: RunnableConfig
checkpoint_metadata: CheckpointMetadata
checkpoint_pending_writes: Optional[List[PendingWrite]]
checkpoint_pending_writes: List[PendingWrite]
step: int
stop: int
status: Literal[
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
]
@@ -203,7 +204,7 @@ class PregelLoop:
return False
# check if iteration limit is reached
if self.step > self.config["recursion_limit"]:
if self.step > self.stop:
self.status = "out_of_steps"
return False
@@ -406,7 +407,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(
@@ -419,6 +420,7 @@ class SyncPregelLoop(PregelLoop, ContextManager):
)
self.status = "pending"
self.step = self.checkpoint_metadata["step"] + 1
self.stop = self.step + self.config["recursion_limit"] + 1
return self
@@ -484,7 +486,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(
@@ -497,6 +499,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
)
self.status = "pending"
self.step = self.checkpoint_metadata["step"] + 1
self.stop = self.step + self.config["recursion_limit"] + 1
return self
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.1.10"
version = "0.1.13"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
+9
View File
@@ -7,6 +7,7 @@ from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
SerializerProtocol,
copy_checkpoint,
)
@@ -119,3 +120,11 @@ class MemorySaverAssertCheckpointMetadata(MemorySaver):
return await asyncio.get_running_loop().run_in_executor(
None, self.put, config, checkpoint, metadata
)
class MemorySaverNoPending(MemorySaver):
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
result = super().get_tuple(config)
if result:
return CheckpointTuple(result.config, result.checkpoint, result.metadata)
return result
+32
View File
@@ -267,6 +267,13 @@ async def test_tool_node():
raise ValueError("Test error")
return f"tool2: {some_val} - {some_other_val}"
async def tool3(some_val: int, some_other_val: str) -> str:
"""Tool 3 docstring."""
return [
{"key_1": some_val, "key_2": "foo"},
{"key_1": some_other_val, "key_2": "baz"},
]
result = ToolNode([tool1]).invoke(
{
"messages": [
@@ -377,6 +384,31 @@ async def test_tool_node():
)
assert tool_message.tool_call_id == "some 0"
# list of dicts tool content
result3 = await ToolNode([tool3]).ainvoke(
{
"messages": [
AIMessage(
"hi?",
tool_calls=[
{
"name": "tool3",
"args": {"some_val": 2, "some_other_val": "bar"},
"id": "some 0",
}
],
)
]
}
)
tool_message: ToolMessage = result3["messages"][-1]
assert tool_message.type == "tool"
assert (
tool_message.content
== '[{"key_1": 2, "key_2": "foo"}, {"key_1": "bar", "key_2": "baz"}]'
)
assert tool_message.tool_call_id == "some 0"
def my_function(some_val: int, some_other_val: str) -> str:
return f"{some_val} - {some_other_val}"
+24 -1
View File
@@ -66,6 +66,7 @@ from tests.any_str import AnyStr
from tests.memory_assert import (
MemorySaverAssertCheckpointMetadata,
MemorySaverAssertImmutable,
MemorySaverNoPending,
NoopSerializer,
)
@@ -1149,10 +1150,32 @@ def test_cond_edge_after_send() -> None:
builder.add_conditional_edges("1", send_for_fun)
builder.add_conditional_edges("2", route_to_three)
graph = builder.compile()
assert graph.invoke(["0"]) == ["0", "1", "2", "3"]
async def test_checkpointer_null_pending_writes() -> None:
class Node:
def __init__(self, name: str):
self.name = name
setattr(self, "__name__", name)
def __call__(self, state):
return [self.name]
builder = StateGraph(Annotated[list, operator.add])
builder.add_node(Node("1"))
builder.add_edge(START, "1")
graph = builder.compile(checkpointer=MemorySaverNoPending())
assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"]
assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2
assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [
"1"
] * 3
assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [
"1"
] * 4
def test_invoke_checkpoint_sqlite(mocker: MockerFixture) -> None:
adder = mocker.Mock(side_effect=lambda x: x["total"] + x["input"])
+13 -3
View File
@@ -38,9 +38,19 @@ const updateConfig = () => {
...json,
typedocOptions: {
...json.typedocOptions,
entryPoints: [...Object.keys(entrypoints)].map(
(key) => `src/${entrypoints[key]}.ts`,
),
entryPoints: [...Object.keys(entrypoints)].map((key) => {
const basePath = `src/${entrypoints[key]}`;
if (fs.existsSync(`${basePath}.mts`)) {
return `${basePath}.mts`;
} else if (fs.existsSync(`${basePath}.ts`)) {
return `${basePath}.ts`;
} else {
console.warn(
`Warning: Neither ${basePath}.mts nor ${basePath}.ts found for entrypoint ${key}`,
);
return `${basePath}.ts`; // Default to .ts if neither exists
}
}),
},
}));
+4 -4
View File
@@ -107,7 +107,7 @@ class BaseClient {
}
}
class CronsClient extends BaseClient {
export class CronsClient extends BaseClient {
/**
*
* @param threadId The ID of the thread.
@@ -195,7 +195,7 @@ class CronsClient extends BaseClient {
}
}
class AssistantsClient extends BaseClient {
export class AssistantsClient extends BaseClient {
/**
* Get an assistant by ID.
*
@@ -302,7 +302,7 @@ class AssistantsClient extends BaseClient {
}
}
class ThreadsClient extends BaseClient {
export class ThreadsClient extends BaseClient {
/**
* Get a thread by ID.
*
@@ -496,7 +496,7 @@ class ThreadsClient extends BaseClient {
}
}
class RunsClient extends BaseClient {
export class RunsClient extends BaseClient {
stream(
threadId: null,
assistantId: string,
+18 -7
View File
@@ -2,7 +2,11 @@
"extends": "@tsconfig/recommended",
"compilerOptions": {
"target": "ES2021",
"lib": ["ES2021", "ES2022.Object", "DOM"],
"lib": [
"ES2021",
"ES2022.Object",
"DOM"
],
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
@@ -17,19 +21,26 @@
"strict": true,
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "coverage"],
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"coverage"
],
"includeVersion": true,
"typedocOptions": {
"entryPoints": [
"src/client.mts",
"src/schema.ts",
"src/types.mts"
"src/client.mts"
],
"readme": "none",
"out": "docs",
"plugin": [
"typedoc-plugin-markdown"
]
],
"excludePrivate": true,
"excludeProtected": true,
"excludeExternals": false
}
}