mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 10:49:56 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebe01c2639 | ||
|
|
272219e410 | ||
|
|
bb1324cdc3 | ||
|
|
85af0603fa | ||
|
|
e6e5911aae | ||
|
|
9fdbd0dd49 | ||
|
|
8a20c6f7e4 | ||
|
|
03f9b27e3b | ||
|
|
19f6f7d5ca | ||
|
|
745b96eb63 | ||
|
|
b90d44d97d | ||
|
|
d99d3e05c7 | ||
|
|
ca6aef4746 | ||
|
|
f5b9e463e3 | ||
|
|
82cbe25be8 | ||
|
|
36505d9656 | ||
|
|
b72ea0ea1b | ||
|
|
03bf1e5414 |
@@ -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,3 +1,4 @@
|
||||
*.ipynb
|
||||
site/
|
||||
docs/tutorials/**/*.png
|
||||
docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
@@ -101,6 +101,7 @@ _HIDE = set(
|
||||
"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",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+10
-10
File diff suppressed because one or more lines are too long
+88
-11
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -722,8 +722,10 @@ class Pregel(
|
||||
if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None:
|
||||
# if being called as a node in another graph, always use values mode
|
||||
stream_mode = ["values"]
|
||||
if config is not None and config.get("configurable", {}).get(
|
||||
CONFIG_KEY_CHECKPOINTER
|
||||
if (
|
||||
config is not None
|
||||
and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER)
|
||||
and (interrupt_after or interrupt_before)
|
||||
):
|
||||
checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][
|
||||
CONFIG_KEY_CHECKPOINTER
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -102,6 +102,7 @@ class PregelLoop:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.1.11"
|
||||
version = "0.1.14"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -8570,6 +8593,7 @@ def test_nested_graph_interrupts_parallel(checkpointer: BaseCheckpointSaver) ->
|
||||
checkpointer.__exit__(None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer",
|
||||
[
|
||||
|
||||
@@ -7099,6 +7099,7 @@ async def test_nested_graph_interrupts_parallel(
|
||||
await checkpointer.__aexit__(None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer",
|
||||
[
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user