Compare commits

..
Author SHA1 Message Date
Sydney Runkle 6f2453024a target version 2025-08-21 11:21:45 -04:00
Sydney Runkle b22227588c remove 3.9 stuff 2025-08-21 11:15:25 -04:00
f4cdeea6ad feat(prebuilt): native structured output support w/ all sorts of models (#5961)
* Adds support for `NativeOutput` via a new `NativeOutput` dataclass
* Adds support for structured output specification via the following
(pydantic models already supported)
  * dataclasses
  * typed dicts
  * json schemas 
* Adds mocking support to support native strategies with
`FakeToolCallingModel`
* Add new default tool message when `tool_message_content` not provided
* Smart "selection" of native vs tool output based on provider support,
necessitates profiles down the line
  
Considered questions
* do we want to enforce docstrings? -- decided on no for now
* do we want to enforce names (titles) on json schemas? -- decided no
for now, defaulting to `structured_output`
* do we want to validate that json schemas coming in are valid? --
decided no for now
* do we want to validate model results against a given json schema? we
validate against all other types (typed dict, dataclass, etc) w/
pydantic -- decided no for now

TODO in future PRs:
* Figure out retry policy
* Add standard testing (handed off to @casparb)
* Further privatize certain structures (like the bindings) -- this is
low prio

---------

Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-08-21 10:43:50 -04:00
Sydney RunkleandGitHub 1cd1373788 chore(prebuilt): clean up public state (#5973)
precursor to `prepare_call` PR, cleaning up existing logic w/ pre model
hook

* use one combined `AgentState` instead of the one w/ and w/o structured
response
* remove exposed pydantic agent state + loosen bounds on state type
* remove llm input messages pattern, should be made possible with
prepare_call

also
* some remaining test fixes in `langgraph` to adapt to new `model` node
name (used to be `agent`)
2025-08-20 11:23:14 -04:00
Sydney RunkleandGitHub f994d16b49 chore(prebuilt): critical renaming (#5971)
* `create_react_agent` -> `create_agent`
* `agent` node -> `model` node
2025-08-20 09:15:42 -04:00
Sydney RunkleandGitHub 20953b4728 chore(prebuilt): remove config schema deprecation for new version (#5970)
don't need this deprecation warning as we're migrating to langchain
2025-08-20 09:04:11 -04:00
Sydney RunkleandGitHub e670815780 chore(prebuilt): rework structured outputs -- type safety, etc (#5962)
* make `_SchemaSpec` private
* Add ability to customize message used in artificial tool response
2025-08-20 08:52:51 -04:00
Sydney RunkleandGitHub 4151861ca2 chore(prebuilt): remove v1 (#5960) 2025-08-19 14:30:32 -04:00
Sydney RunkleandGitHub 5239184ba6 chore(prebuilt): revert optional multiple nodes for tools (#5959) 2025-08-19 14:19:57 -04:00
Sydney RunkleandGitHub a5aa9ce27d chore(prebuilt): remove support for models that used bind_X (#5958)
Remove support for models w/ `.bind` used to streamline public API +
recommendations
Also cleaning up `typing.py` file as requested :)
2025-08-19 13:56:16 -04:00
Eugene YurtsevandGitHub b58a7fb2fe feat(prebuilt): support ToolOutput response_format (#5915)
* Add support for ToolOutput response format.
* I don't love the name -- it's confusing unless you know that it's parameterizing a strategy.

We should determine if we want to support our old strategy for doing
things -- it has a higher latency (one extra LLM call), but it's a
reasonable built-in strategy as it doesn't do anything awkward with
conversation history. (Wouldn't surprising if it has overall better
performance than tool choice for longer conversations)
2025-08-14 23:34:46 -04:00
Eugene Yurtsev ebae60045f Fix spelling typo 2025-08-14 14:51:13 -04:00
Eugene YurtsevandGitHub 42f9683d73 chore(prebuilt): breaking do not support prebound tools on model (#5912)
Do not support prebound tools on the model. There's reason users should be prebinding tools to the model!

This is a breaking change that might affect some users, but the work-around is simple -- provide tools into the create_react_agent api.
2025-08-14 14:46:11 -04:00
Eugene YurtsevandGitHub 69249e724d chore(prebuilt): Separate prompt from model (#5909)
Quick clean up to simplify the logic by which messages into the model are prepared.
2025-08-14 12:59:09 -04:00
Eugene YurtsevandGitHub e6d71a586d chore(prebuilt): remove structured tool support from ToolNode (#5902)
Remove structured tool support from ToolNode

We'll handle structured tools directly in the call_model nodes.
2025-08-13 22:32:21 -04:00
Eugene YurtsevandGitHub 50601dc02c feat(prebuilt): Add structured output tools to ToolNode (#5899)
* Add structured output tools to ToolNode
* Fix default tool node name to match the actual default ('tools')
* Update doc-strings to explain what inputs/outputs are for the ToolNode.
* Mark internal attributes as private (potentially breaking -- although hopefully users aren't accessing these)


## Decisions points

* OK with two properties? Done since users may be relying on
`tools_by_name` and expanding the return type will break user code.

## Changes in public/private interface

### Marked as public

* Make `tools_by_name` an official public property
* Make `structured_output_tools` a public property

### Marked as private

There should be no reason why users are accessing these attributes

```python
_tool_to_state_args
_tool_to_store_arg
_handle_tool_errors
_messages_key
```


### Usage

```python

    class OutputSchema(BaseModel):
        name: str
        age: int
        location: str

    tool_node = ToolNode([OutputSchema])

    # Test that the structured output tool is registered correctly
    assert "OutputSchema" in tool_node.structured_output_tools

    # Create a tool call that matches the schema
    tool_call = {
        "name": "OutputSchema",
        "args": {"name": "Alice", "age": 30, "location": "NYC"},
        "id": "call_123",
        "type": "tool_call",
    }

    # Test sync execution
    result = tool_node.invoke(
        {"messages": [AIMessage(content="", tool_calls=[tool_call])]}
    )

    # Should return a Command with structured response
    assert isinstance(result, list)
    assert len(result) == 1
    command = result[0]
    assert isinstance(command, Command)

    # Check the update structure
    assert "messages" in command.update
    assert "structured_response" in command.update

    # Check the tool message
    tool_message = command.update["messages"][0]
    assert isinstance(tool_message, ToolMessage)
    assert tool_message.name == "OutputSchema"
    assert tool_message.tool_call_id == "call_123"

    # Check the structured response
    structured_response = command.update["structured_response"]
    assert isinstance(structured_response, OutputSchema)
    assert structured_response.name == "Alice"
    assert structured_response.age == 30
    assert structured_response.location == "NYC"
```
2025-08-13 15:16:53 -04:00
Eugene YurtsevandGitHub 9e174e7e8b chore(prebuilt): move unit tests for ToolNode into the tool node testing code (#5893)
Move unit tests for ToolNode into the tool node testing code
2025-08-13 11:15:10 -04:00
Eugene YurtsevandGitHub 9e9a5d2498 feat(prebuilt): Split tool node to individual tool nodes (#5888)
Add option to split tool node to individual nodes. 

Summary:
* User code (specifically streaming) may break if it's relying on the
name of the `tools` node
* The boolean flag in the interface is likely **temporary** (especially
if there are no major breaking changes)
* We'll need to decide if we can get rid of the version in create react
agent. "v1" is not consistent conceptually with a node per tool.
2025-08-13 09:49:27 -04:00
Eugene Yurtsev 7e257dadd6 x 2025-08-12 21:52:21 -04:00
Eugene Yurtsev 2fed0e4852 Internal refactor of create react-agent 2025-08-12 21:50:19 -04:00
213 changed files with 7611 additions and 15188 deletions
+1 -4
View File
@@ -1,9 +1,6 @@
blank_issues_enabled: false blank_issues_enabled: false
version: 2.1 version: 2.1
contact_links: contact_links:
- name: Documentation
url: https://github.com/langchain-ai/docs/issues/new?template=langgraph.yml
about: Report an issue related to the LangGraph documentation
- name: LangChain Forum - name: LangChain Forum
url: https://forum.langchain.com/ url: https://forum.langchain.com/
about: General community discussions and support about: General community discussions, support, and feature requests
+19
View File
@@ -0,0 +1,19 @@
name: Documentation
description: Report an issue related to the LangGraph documentation.
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
labels: [documentation]
body:
- type: textarea
attributes:
label: "Issue with current documentation:"
description: >
Please make sure to leave a reference to the document/code you're
referring to.
- type: textarea
attributes:
label: "Idea or request for content:"
description: >
Please describe as clearly as possible what topics you think are missing
from the current documentation.
+2 -7
View File
@@ -1,15 +1,10 @@
import ast import ast
import os import os
from itertools import filterfalse from itertools import filterfalse
from typing import Dict, List, Tuple from typing import List, Tuple
ROOT_PATH = os.path.abspath(os.path.join(__file__, "..", "..", "..")) ROOT_PATH = os.path.abspath(os.path.join(__file__, "..", "..", ".."))
CLIENT_PATH = os.path.join(ROOT_PATH, "libs", "sdk-py", "langgraph_sdk", "client.py") CLIENT_PATH = os.path.join(ROOT_PATH, "libs", "sdk-py", "langgraph_sdk", "client.py")
ASYNC_TO_SYNC_METHOD_MAP: Dict[str, str] = {
"aclose": "close",
"__aenter__": "__enter__",
"__aexit__": "__exit__",
}
def get_class_methods(node: ast.ClassDef) -> List[str]: def get_class_methods(node: ast.ClassDef) -> List[str]:
@@ -27,7 +22,7 @@ def find_classes(tree: ast.AST) -> List[Tuple[str, List[str]]]:
def compare_sync_async_methods(sync_methods: List[str], async_methods: List[str]) -> List[str]: def compare_sync_async_methods(sync_methods: List[str], async_methods: List[str]) -> List[str]:
sync_set = set(sync_methods) sync_set = set(sync_methods)
async_set = {ASYNC_TO_SYNC_METHOD_MAP.get(async_method, async_method) for async_method in async_methods} async_set = set(async_methods)
missing_in_sync = list(async_set - sync_set) missing_in_sync = list(async_set - sync_set)
missing_in_async = list(sync_set - async_set) missing_in_async = list(sync_set - async_set)
return missing_in_sync + missing_in_async return missing_in_sync + missing_in_async
+86 -124
View File
@@ -1,145 +1,107 @@
import asyncio
import json
import os
import pathlib import pathlib
import sys import sys
import time
from urllib import request, error
import langgraph_cli import langgraph_cli
import langgraph_cli.config
import langgraph_cli.docker import langgraph_cli.docker
from langgraph_cli.cli import prepare_args_and_stdin import langgraph_cli.config
from langgraph_cli.constants import DEFAULT_PORT
from langgraph_cli.exec import Runner, subp_exec from langgraph_cli.exec import Runner, subp_exec
from langgraph_cli.progress import Progress from langgraph_cli.progress import Progress
from langgraph_cli.constants import DEFAULT_PORT
def test(config: pathlib.Path, port: int, tag: str, verbose: bool): def test(
"""Spin up API with Postgres/Redis via docker compose and wait until ready.""" config: pathlib.Path,
port: int,
tag: str,
verbose: bool,
):
with Runner() as runner, Progress(message="Pulling...") as set: with Runner() as runner, Progress(message="Pulling...") as set:
# Detect docker/compose capabilities # check docker available
capabilities = langgraph_cli.docker.check_capabilities(runner) capabilities = langgraph_cli.docker.check_capabilities(runner)
# open config
# Validate config and prepare compose stdin/args using built image
config_json = langgraph_cli.config.validate_config_file(config) config_json = langgraph_cli.config.validate_config_file(config)
args, stdin = prepare_args_and_stdin(
capabilities=capabilities,
config_path=config,
config=config_json,
docker_compose=None,
port=port,
watch=False,
debugger_port=None,
debugger_base_url=f"http://127.0.0.1:{port}",
postgres_uri=None,
api_version=None,
image=tag,
base_image=None,
)
# Compose up with wait (implies detach), similar to `langgraph up --wait` set("Running...")
args_up = [*args, "up", "--remove-orphans", "--wait"] args = [
"run",
"--rm",
"-p",
f"{port}:8000",
]
if isinstance(config_json["env"], str):
args.extend(
[
"--env-file",
str(config.parent / config_json["env"]),
]
)
else:
for k, v in config_json["env"].items():
args.extend(
[
"-e",
f"{k}={v}",
]
)
if capabilities.healthcheck_start_interval:
args.extend(
[
"--health-interval",
"5s",
"--health-retries",
"1",
"--health-start-period",
"10s",
"--health-start-interval",
"1s",
]
)
else:
args.extend(
[
"--health-interval",
"5s",
"--health-retries",
"2",
]
)
compose_cmd = ["docker", "compose"] _task = None
if capabilities.compose_type == "standalone":
compose_cmd = ["docker-compose"] def on_stdout(line: str):
nonlocal _task
if "GET /ok" in line or "Uvicorn running on" in line:
set("")
sys.stdout.write(
f"""Ready!
- API: http://localhost:{port}
"""
)
sys.stdout.flush()
_task.cancel()
return True
return False
async def subp_exec_task(*args, **kwargs):
nonlocal _task
_task = asyncio.create_task(subp_exec(*args, **kwargs))
await _task
set("Starting...")
try: try:
runner.run( runner.run(
subp_exec( subp_exec_task(
*compose_cmd, "docker",
*args_up, *args,
input=stdin, tag,
verbose=verbose, verbose=verbose,
on_stdout=on_stdout,
) )
) )
except Exception as e: # noqa: BLE001 except asyncio.CancelledError:
# On failure, show diagnostics then ensure clean teardown pass
sys.stderr.write(f"docker compose up failed: {e}\n")
try:
sys.stderr.write("\n== docker compose ps ==\n")
runner.run(subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False))
except Exception:
pass
try:
sys.stderr.write("\n== docker compose logs (api) ==\n")
runner.run(
subp_exec(
*compose_cmd,
*args,
"logs",
"langgraph-api",
input=stdin,
verbose=False,
)
)
except Exception:
pass
finally:
try:
runner.run(
subp_exec(
*compose_cmd,
*args,
"down",
"-v",
"--remove-orphans",
input=stdin,
verbose=False,
)
)
finally:
raise
set("")
base_url = f"http://localhost:{port}"
ok_url = f"{base_url}/ok"
print(f"Waiting for {ok_url} to respond with 200...")
deadline = time.time() + 30
last_err: Exception | None = None
while time.time() < deadline:
try:
with request.urlopen(ok_url, timeout=2) as resp:
if resp.status == 200:
sys.stdout.write(
f"""Ready!\n- API: {base_url}\n- /ok: 200 OK\n"""
)
sys.stdout.flush()
break
else:
last_err = RuntimeError(f"Unexpected status: {resp.status}")
print(f"Unexpected status: {resp.status}")
except error.URLError as e:
last_err = e
except Exception as e: # noqa: BLE001
last_err = e
time.sleep(0.5)
else:
# Bring stack down before raising
args_down = [*args, "down", "-v", "--remove-orphans"]
try:
runner.run(
subp_exec(
*compose_cmd,
*args_down,
input=stdin,
verbose=verbose,
)
)
finally:
raise SystemExit(
f"/ok did not return 202 within timeout. Last error: {last_err}"
)
# Clean up: bring compose stack down to free ports for next test
args_down = [*args, "down", "-v", "--remove-orphans"]
runner.run(
subp_exec(
*compose_cmd,
*args_down,
input=stdin,
verbose=verbose,
)
)
if __name__ == "__main__": if __name__ == "__main__":
@@ -148,6 +110,6 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-t", "--tag", type=str) parser.add_argument("-t", "--tag", type=str)
parser.add_argument("-c", "--config", type=str, default="./langgraph.json") parser.add_argument("-c", "--config", type=str, default="./langgraph.json")
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT) parser.add_argument("-p", "--port", default=DEFAULT_PORT)
args = parser.parse_args() args = parser.parse_args()
test(pathlib.Path(args.config), args.port, args.tag, verbose=True) test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
+29 -65
View File
@@ -14,25 +14,12 @@ jobs:
python-version: python-version:
- "3.10" - "3.10"
- "3.11" - "3.11"
example:
- name: A
workdir: libs/cli/examples
tag: langgraph-test-a
- name: B
workdir: libs/cli/examples/graphs
tag: langgraph-test-b
- name: C
workdir: libs/cli/examples/graphs_reqs_a
tag: langgraph-test-c
- name: D
workdir: libs/cli/examples/graphs_reqs_b
tag: langgraph-test-d
name: "CLI integration test" name: "CLI integration test"
defaults: defaults:
run: run:
working-directory: libs/cli working-directory: libs/cli
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: Ana06/get-changed-files@v2.3.0 uses: Ana06/get-changed-files@v2.3.0
@@ -46,65 +33,42 @@ jobs:
enable-cache: true enable-cache: true
cache-suffix: "cli-integration-test" cache-suffix: "cli-integration-test"
ignore-nothing-to-cache: true ignore-nothing-to-cache: true
- name: Setup env
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: cat .env.example > .env
- name: Install cli globally - name: Install cli globally
if: steps.changed-files.outputs.all if: steps.changed-files.outputs.all
run: pip install -e . run: pip install -e .
- name: Build and test service ${{ matrix.example.name }} - name: Build and test service A
if: steps.changed-files.outputs.all if: steps.changed-files.outputs.all
working-directory: ${{ matrix.example.workdir }} working-directory: libs/cli/examples
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: | run: |
# Build the image for this example # The build-arg isn't used; just testing that we accept other args
langgraph build -t ${{ matrix.example.tag }} langgraph build -t langgraph-test-a --base-image "langchain/langgraph-trial"
# Prepare environment file from local or parent example directory cp .env.example .envg
if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi; fi - name: Build and test service B
# Run the integration test using the built tag if: steps.changed-files.outputs.all
# Compute repo root to reference the shared script robustly working-directory: libs/cli/examples/graphs
REPO_ROOT=$(git rev-parse --show-toplevel) run: |
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }} langgraph build -t langgraph-test-b --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
- name: Build and test service C
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_a
run: |
langgraph build -t langgraph-test-c --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
- name: Build and test service D
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_b
run: |
langgraph build -t langgraph-test-d --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
- name: Build JS service - name: Build JS service
if: steps.changed-files.outputs.all if: steps.changed-files.outputs.all
working-directory: libs/cli/js-examples working-directory: libs/cli/js-examples
run: | run: |
langgraph build -t langgraph-test-e langgraph build -t langgraph-test-e
- name: Build JS monorepo service
if: steps.changed-files.outputs.all
working-directory: libs/cli/js-monorepo-example
run: |
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
- name: Build Python monorepo service
if: steps.changed-files.outputs.all
working-directory: libs/cli/python-monorepo-example
run: |
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
cp apps/agent/.env.example apps/agent/.env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env; fi
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build and test prerelease reqs service
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graph_prerelease_reqs
run: |
langgraph build -t langgraph-test-h
cp ../.env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
if [ "$LANGGRAPH_VERSION" != "1.0.0a2" ]; then
exit 1
fi
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
if [ "$LANGCHAIN_OPENAI_VERSION" != "0.3.0" ]; then
exit 1
fi
- name: Build and test prerelease reqs fail service
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
run: |
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- "3.12" - "3.12"
name: "lint #${{ matrix.python-version }}" name: "lint #${{ matrix.python-version }}"
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: Ana06/get-changed-files@v2.3.0 uses: Ana06/get-changed-files@v2.3.0
+1 -2
View File
@@ -17,7 +17,6 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: python-version:
- "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
@@ -25,7 +24,7 @@ jobs:
name: "test #${{ matrix.python-version }}" name: "test #${{ matrix.python-version }}"
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
with: with:
+1 -2
View File
@@ -12,7 +12,6 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: python-version:
- "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
@@ -23,7 +22,7 @@ jobs:
working-directory: libs/langgraph working-directory: libs/langgraph
name: "test #${{ matrix.python-version }}" name: "test #${{ matrix.python-version }}"
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
with: with:
+3 -3
View File
@@ -24,7 +24,7 @@ jobs:
version: ${{ steps.check-version.outputs.version }} version: ${{ steps.check-version.outputs.version }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python $${ env.PYTHON_VERSION }} - name: Set up Python $${ env.PYTHON_VERSION }}
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
@@ -75,9 +75,9 @@ jobs:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: actions/download-artifact@v5 - uses: actions/download-artifact@v4
with: with:
name: test-dist name: test-dist
path: ${{ inputs.working-directory }}/dist/ path: ${{ inputs.working-directory }}/dist/
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
run: run:
working-directory: libs/langgraph working-directory: libs/langgraph
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV - run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
- name: Set up Python 3.11 - name: Set up Python 3.11
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
run: run:
working-directory: libs/langgraph working-directory: libs/langgraph
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- id: files - id: files
name: Get changed files name: Get changed files
uses: Ana06/get-changed-files@v2.3.0 uses: Ana06/get-changed-files@v2.3.0
@@ -57,7 +57,7 @@ jobs:
echo EOF echo EOF
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: Annotation - name: Annotation
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
script: | script: |
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0] const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
+4 -5
View File
@@ -27,7 +27,7 @@ jobs:
python: ${{ steps.filter.outputs.python }} python: ${{ steps.filter.outputs.python }}
deps: ${{ steps.filter.outputs.deps }} deps: ${{ steps.filter.outputs.deps }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v3
id: filter id: filter
with: with:
@@ -78,7 +78,6 @@ jobs:
"libs/checkpoint-sqlite", "libs/checkpoint-sqlite",
"libs/checkpoint-postgres", "libs/checkpoint-postgres",
"libs/prebuilt", "libs/prebuilt",
"libs/sdk-py",
] ]
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true' if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
uses: ./.github/workflows/_test.yml uses: ./.github/workflows/_test.yml
@@ -100,9 +99,9 @@ jobs:
name: "Check SDK methods matching" name: "Check SDK methods matching"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
- name: Run check_sdk_methods script - name: Run check_sdk_methods script
@@ -118,7 +117,7 @@ jobs:
python-version: python-version:
- "3.11" - "3.11"
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
with: with:
+1 -1
View File
@@ -21,7 +21,7 @@
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Install Dependencies - name: Install Dependencies
run: | run: |
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
outputs: outputs:
changed-files: ${{ steps.changed-files.outputs.added_modified }} changed-files: ${{ steps.changed-files.outputs.added_modified }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: Ana06/get-changed-files@v2.3.0 uses: Ana06/get-changed-files@v2.3.0
@@ -41,7 +41,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -140,7 +140,7 @@ jobs:
- name: Upload Pages Artifact - name: Upload Pages Artifact
# if: github.ref == 'refs/heads/main' # if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v4 uses: actions/upload-pages-artifact@v3
with: with:
path: ./docs/site/ path: ./docs/site/
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
+1 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Validate PR Title - name: Validate PR Title
uses: amannn/action-semantic-pull-request@v6 uses: amannn/action-semantic-pull-request@v5
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -40,7 +40,6 @@ jobs:
sdk-py sdk-py
docs docs
ci ci
deps
requireScope: false requireScope: false
ignoreLabels: | ignoreLabels: |
ignore-lint-pr-title ignore-lint-pr-title
+8 -14
View File
@@ -26,7 +26,7 @@ jobs:
tag: ${{ steps.check-version.outputs.tag }} tag: ${{ steps.check-version.outputs.tag }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
@@ -62,13 +62,7 @@ jobs:
working-directory: ${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }}
run: | run: |
PKG_NAME=$(grep -m 1 "^name = " pyproject.toml | cut -d '"' -f 2) PKG_NAME=$(grep -m 1 "^name = " pyproject.toml | cut -d '"' -f 2)
if grep -q 'dynamic.*=.*\[.*"version".*\]' pyproject.toml; then VERSION=$(grep -m 1 "^version = " pyproject.toml | cut -d '"' -f 2)
# handle dynamic versioning
DIR_NAME=$(echo "$PKG_NAME" | tr '-' '_')
VERSION=$(grep -m 1 '^__version__' "${DIR_NAME}/__init__.py" | cut -d '"' -f 2)
else
VERSION=$(grep -m 1 "^version = " pyproject.toml | cut -d '"' -f 2)
fi
SHORT_PKG_NAME="$(echo "$PKG_NAME" | sed -e 's/langgraph//g' -e 's/-//g')" SHORT_PKG_NAME="$(echo "$PKG_NAME" | sed -e 's/langgraph//g' -e 's/-//g')"
if [ -z $SHORT_PKG_NAME ]; then if [ -z $SHORT_PKG_NAME ]; then
TAG="$VERSION" TAG="$VERSION"
@@ -87,7 +81,7 @@ jobs:
outputs: outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }} release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
repository: langchain-ai/langgraph repository: langchain-ai/langgraph
path: langgraph path: langgraph
@@ -158,7 +152,7 @@ jobs:
- test-pypi-publish - test-pypi-publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
# We explicitly *don't* set up caching here. This ensures our tests are # We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage. # maximally sensitive to catching breakage.
@@ -261,7 +255,7 @@ jobs:
working-directory: ${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
@@ -270,7 +264,7 @@ jobs:
enable-cache: true enable-cache: true
cache-suffix: "release" cache-suffix: "release"
- uses: actions/download-artifact@v5 - uses: actions/download-artifact@v4
with: with:
name: dist name: dist
path: ${{ inputs.working-directory }}/dist/ path: ${{ inputs.working-directory }}/dist/
@@ -302,7 +296,7 @@ jobs:
working-directory: ${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
@@ -311,7 +305,7 @@ jobs:
enable-cache: true enable-cache: true
cache-suffix: "release" cache-suffix: "release"
- uses: actions/download-artifact@v5 - uses: actions/download-artifact@v4
with: with:
name: dist name: dist
path: ${{ inputs.working-directory }}/dist/ path: ${{ inputs.working-directory }}/dist/
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- "latest" - "latest"
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up Python + Poetry - name: Set up Python + Poetry
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
with: with:
+4 -4
View File
@@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
with: with:
# use minimum supported Python version # use minimum supported Python version
python-version: "3.9" python-version: "3.10"
enable-cache: true enable-cache: true
cache-suffix: "uv-lock-upgrade" cache-suffix: "uv-lock-upgrade"
@@ -33,8 +33,8 @@ jobs:
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(deps): upgrade dependencies with `uv lock --upgrade`" commit-message: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
title: "chore(deps): upgrade dependencies with `uv lock --upgrade`" title: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
body: | body: |
This PR updates the dependencies in all Python packages using `uv lock --upgrade`. This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
+1 -1
View File
@@ -71,7 +71,7 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
## Additional resources ## Additional resources
- [Guides](https://langchain-ai.github.io/langgraph/guides/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.). - [Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components. - [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph. - [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback. - [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
+14 -117
View File
@@ -1,126 +1,24 @@
# LangGraph Documentation # Setup
For more information on contributing to our documentation, see the [Contributing Guide](../CONTRIBUTING.md). To setup requirements for building docs you can run:
## Structure ```bash
uv sync --group test
The primary documentation is located in the `docs/` directory. This directory contains both the source files for the main documentation as well as the API reference doc build process. ```
### Main Documentation ## Serving documentation locally
Main documentation files are located in `docs/docs/` and are written in Markdown format. The site uses [**MkDocs**](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) and includes: To run the documentation server locally you can run:
- **Concepts**: Core LangGraph concepts and explanations
- **Tutorials**: Step-by-step learning guides
- **How-tos**: Task-focused guides for specific use cases
- **Examples**: Real-world applications and use cases
- **Jupyter Notebooks**: Interactive tutorials that are automatically converted to markdown
### API Reference
API reference documentation is defined in `docs/docs/reference/`. Each `.md` file outlines the "template" that each page is built from. Reference content is automatically generated from docstrings in the codebase using the **mkdocstrings** plugin. Once generated, the content is plugged into the corresponding markdown file where it is referenced by using manual directives to specify which classes and/or functions are documented:
```markdown
::: langgraph.graph.state.StateGraph
options:
show_if_no_docstring: true
show_root_heading: true
show_root_full_path: false
members:
- add_node
- add_edge
- add_conditional_edges
- add_sequence
- compile
```
## Build Process
Docs are built following these steps:
1. **Content Processing:**
- `_scripts/notebook_hooks.py` - Main processing pipeline that:
- Converts how-tos/tutorial Jupyter notebooks to markdown using `notebook_convert.py`
- Adds automatic API reference links to code blocks using `generate_api_reference_links.py`
- Handles conditional rendering for Python/JS versions
- Processes highlight comments and custom syntax
2. **API Reference Generation:**
- **mkdocstrings** plugin extracts docstrings from Python source code
- Manual `::: module.Class` directives in reference pages (`/docs/docs/*`) specify what to document
- Cross-references are automatically generated between docs and API
3. **Site Generation:**
- **MkDocs** processes all markdown files and generates static HTML
- Custom hooks handle redirects and inject additional functionality
4. **Deployment:**
- Site is deployed with Vercel
- `make build-docs` generates production build (also usable for local testing)
- Automatic redirects handle URL changes between versions
### Local Development
For local development, use the Makefile targets:
```bash ```bash
# Serve docs locally with hot reloading
make serve-docs make serve-docs
# Clean build for production testing
make build-docs
# Serve with clean build
make serve-clean-docs
``` ```
The `serve-docs` command: This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
- Watches source files for changes
- Includes dirty builds for faster iteration
- Serves on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/)
## Standards
**Docstring Format:**
The API reference uses **Google-style docstrings** with Markdown markup. The `mkdocstrings` plugin processes these to generate documentation.
**Required format:**
```python
def example_function(param1: str, param2: int = 5) -> bool:
"""Brief description of the function.
Longer description can go here. Use Markdown syntax for
rich formatting like **bold** and *italic*.
Args:
param1: Description of the first parameter.
param2: Description of the second parameter with default value.
Returns:
Description of the return value.
Raises:
ValueError: When param1 is empty.
TypeError: When param2 is not an integer.
!!! warning
This function is experimental and may change.
!!! version-added "Added in version 0.2.0"
"""
```
**Special Markers:**
- **MkDocs admonitions**: `!!! warning`, `!!! note`, `!!! version-added`
- **Code blocks**: Standard markdown ``` syntax
- **Cross-references**: Automatic linking via `generate_api_reference_links.py`
## Execute notebooks ## Execute notebooks
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GitHub action, you can run: If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
```bash ```bash
python _scripts/prepare_notebooks_for_ci.py python _scripts/prepare_notebooks_for_ci.py
@@ -135,9 +33,8 @@ python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
``` ```
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that: `prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
- when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file * when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
- when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
## Adding new notebooks ## Adding new notebooks
+2 -14
View File
@@ -1,5 +1,3 @@
"""Generate API reference links for imports in Python code blocks within markdown files."""
import ast import ast
import importlib import importlib
import logging import logging
@@ -72,18 +70,8 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"), ([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"), ([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
# other prebuilts # other prebuilts
( (["langgraph_supervisor"], "langgraph_supervisor.supervisor", "create_supervisor", "supervisor"),
["langgraph_supervisor"], (["langgraph_supervisor"], "langgraph_supervisor.handoff", "create_handoff_tool", "supervisor"),
"langgraph_supervisor.supervisor",
"create_supervisor",
"supervisor",
),
(
["langgraph_supervisor"],
"langgraph_supervisor.handoff",
"create_handoff_tool",
"supervisor",
),
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"), ([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"), (["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"), (["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
@@ -2108,9 +2108,9 @@ __metadata:
linkType: hard linkType: hard
"hono@npm:^4.5.4": "hono@npm:^4.5.4":
version: 4.9.7 version: 4.8.9
resolution: "hono@npm:4.9.7" resolution: "hono@npm:4.8.9"
checksum: 10c0/089184660a9211ea216ab95bafa45260e371651cb019db49828064b7982b0ae61cc3c4715324bfeb9037aa2460c39ffa2c91d84ad0c8d500fa77cbcc7fc07a8f checksum: 10c0/385539d1787fdc747bc869ef0e5ccc9f39cbe40289b94f23eecfc82c6ca440f059704647cd6381a5066d2cf7baa43ab25184c78d44af4c5c98a5c5b07670059e
languageName: node languageName: node
linkType: hard linkType: hard
-2
View File
@@ -1,5 +1,3 @@
"""Convert Jupyter notebooks to markdown with custom processing."""
import ast import ast
import os import os
import re import re
+5 -5
View File
@@ -144,9 +144,9 @@ REDIRECT_MAP = {
"concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli", "concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli",
"concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio", "concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio",
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio", "cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio",
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/use-studio#run-application", "cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/invoke-studio",
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/use-studio#manage-assistants", "cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/manage-assistants-studio",
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/use-studio#manage-threads", "cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/threads-studio",
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio", "cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio",
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio", "cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio",
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio", "cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio",
@@ -190,11 +190,11 @@ REDIRECT_MAP = {
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langgraph-platform/cloud", "concepts/langgraph_cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/hybrid", "concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/hybrid",
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/self-hosted", "concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/self-hosted",
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langgraph-platform/self-hosted#standalone-server", "concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langgraph-platform/self-hosted#data-plane-only",
"cloud/deployment/cloud.md": "https://docs.langchain.com/langgraph-platform/cloud", "cloud/deployment/cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-hybrid", "cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-hybrid",
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-self-hosted-full-platform", "cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-self-hosted-full-platform",
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langgraph-platform/deploy-standalone-server", "cloud/deployment/standalone_container.md": "https://docs.langchain.com/langgraph-platform/deploy-data-plane-only",
"concepts/server-mcp.md": "https://docs.langchain.com/langgraph-platform/server-mcp", "concepts/server-mcp.md": "https://docs.langchain.com/langgraph-platform/server-mcp",
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langgraph-platform/human-in-the-loop-time-travel", "cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langgraph-platform/human-in-the-loop-time-travel",
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langgraph-platform/add-human-in-the-loop", "cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langgraph-platform/add-human-in-the-loop",
+1 -1
View File
@@ -90,7 +90,7 @@ graph.invoke( # (1)!
from langgraph.runtime import Runtime from langgraph.runtime import Runtime
# highlight-next-line # highlight-next-line
def node(state: State, runtime: Runtime[ContextSchema]): def node(state: State, config: Runtime[ContextSchema]):
user_name = runtime.context.user_name user_name = runtime.context.user_name
... ...
``` ```
+2 -2
View File
@@ -99,8 +99,8 @@ Starting from the `LangGraph Platform` view...
1. In the top-right corner, select the gear icon (`Deployment Settings`). 1. In the top-right corner, select the gear icon (`Deployment Settings`).
1. Update the `Git Branch` to the desired branch. 1. Update the `Git Branch` to the desired branch.
1. Check/uncheck checkbox to `Automatically update deployment on push to branch`. 1. Check/uncheck checkbox to `Automatically update deployment on push to branch`.
1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update. 1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update.
1. Pushes in quick succession to a branch will queue subsequent updates. Once a build completes, the most recent commit will begin building and the other queued builds will be skipped. 1. Pushes in quick succession to a branch will not trigger subsequent updates. In the future, this functionality may be changed/improved.
## Add or Remove GitHub Repositories ## Add or Remove GitHub Repositories
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -483,19 +483,19 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
ADD ./graphs /deps/outer-graphs/src ADD ./graphs /deps/__outer_graphs/src
RUN set -ex && \ RUN set -ex && \
for line in '[project]' \ for line in '[project]' \
'name = "graphs"' \ 'name = "graphs"' \
'version = "0.1"' \ 'version = "0.1"' \
'[tool.setuptools.package-data]' \ '[tool.setuptools.package-data]' \
'"*" = ["**/*"]'; do \ '"*" = ["**/*"]'; do \
echo "$line" >> /deps/outer-graphs/pyproject.toml; \ echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
done done
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-graphs/src/agent.py:graph", "storm": "/deps/outer-graphs/src/storm.py:graph"}' ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
``` ```
???+ note "Updating your langgraph.json file" ???+ note "Updating your langgraph.json file"
+1 -1
View File
@@ -1040,7 +1040,7 @@ def node_a(state: State, runtime: Runtime[ContextSchema]):
... ...
``` ```
See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full breakdown on configuration. See [this guide](../how-tos/graph-api.ipynb#add-runtime-configuration) for a full breakdown on configuration.
::: :::
:::js :::js
+1 -1
View File
@@ -134,7 +134,7 @@ def update_instructions(state: State, store: BaseStore):
namespace = ("instructions",) namespace = ("instructions",)
current_instructions = store.search(namespace)[0] current_instructions = store.search(namespace)[0]
# Memory logic # Memory logic
prompt = prompt_template.format(instructions=current_instructions.value["instructions"], conversation=state["messages"]) prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
output = llm.invoke(prompt) output = llm.invoke(prompt)
new_instructions = output['new_instructions'] new_instructions = output['new_instructions']
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions}) store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
+2 -2
View File
@@ -897,5 +897,5 @@ There are two high-level approaches to achieve that:
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph: An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.md#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs. - Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.ipynb#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
- Define agent node functions with a [private input state schema](../how-tos/graph-api.md#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent. - Define agent node functions with a [private input state schema](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
+1 -2
View File
@@ -315,8 +315,7 @@ In our example, the output of `get_state_history` will look like this:
tasks=(), tasks=(),
), ),
StateSnapshot( StateSnapshot(
values={'foo': 'a', 'bar': ['a']}, values={'foo': 'a', 'bar': ['a']}, next=('node_b',),
next=('node_b',),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}},
metadata={'source': 'loop', 'writes': {'node_a': {'foo': 'a', 'bar': ['a']}}, 'step': 1}, metadata={'source': 'loop', 'writes': {'node_a': {'foo': 'a', 'bar': ['a']}}, 'step': 1},
created_at='2024-08-29T19:19:38.819946+00:00', created_at='2024-08-29T19:19:38.819946+00:00',
+1 -1
View File
@@ -12,7 +12,7 @@ There are three different plans for using it.
- **Developer**: All [LangSmith](https://smith.langchain.com/) users have access to this plan. You can sign up for this plan simply by creating a LangSmith account. This gives you access to the [local deployment](./deployment_options.md#free-deployment) option. - **Developer**: All [LangSmith](https://smith.langchain.com/) users have access to this plan. You can sign up for this plan simply by creating a LangSmith account. This gives you access to the [local deployment](./deployment_options.md#free-deployment) option.
- **Plus**: All [LangSmith](https://smith.langchain.com/) users with a [Plus account](https://docs.smith.langchain.com/administration/pricing) have access to this plan. You can sign up for this plan simply by upgrading your LangSmith account to the Plus plan type. This gives you access to the [Cloud](./deployment_options.md#cloud-saas) deployment option. - **Plus**: All [LangSmith](https://smith.langchain.com/) users with a [Plus account](https://docs.smith.langchain.com/administration/pricing) have access to this plan. You can sign up for this plan simply by upgrading your LangSmith account to the Plus plan type. This gives you access to the [Cloud](./deployment_options.md#cloud-saas) deployment option.
- **Enterprise**: This is separate from LangSmith plans. You can sign up for this plan by [contacting our sales team](https://www.langchain.com/contact-sales). This gives you access to all [deployment options](./deployment_options.md). - **Enterprise**: This is separate from LangSmith plans. You can sign up for this plan by contacting sales@langchain.dev. This gives you access to all [deployment options](./deployment_options.md).
## Plan Details ## Plan Details
+13 -12
View File
@@ -1019,7 +1019,7 @@ console.log(await graph.invoke({}, { configurable: { myRuntimeValue: "b" } }));
# Usage # Usage
input_message = {"role": "user", "content": "hi"} input_message = {"role": "user", "content": "hi"}
# With no configuration, uses default (Anthropic) # With no configuration, uses default (Anthropic)
response_1 = graph.invoke({"messages": [input_message]}, context=ContextSchema())["messages"][-1] response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
# Or, can set OpenAI # Or, can set OpenAI
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1] response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
@@ -1205,7 +1205,7 @@ There are many use cases where you may wish for your node to have a custom retry
To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node: To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node:
```python ```python
from langgraph.types import RetryPolicy from langgraph.pregel import RetryPolicy
builder.add_node( builder.add_node(
"node_name", "node_name",
@@ -1260,7 +1260,7 @@ By default, the retry policy retries on any exception except for the following:
from typing_extensions import TypedDict from typing_extensions import TypedDict
from langchain.chat_models import init_chat_model from langchain.chat_models import init_chat_model
from langgraph.graph import END, MessagesState, StateGraph, START from langgraph.graph import END, MessagesState, StateGraph, START
from langgraph.types import RetryPolicy from langgraph.pregel import RetryPolicy
from langchain_community.utilities import SQLDatabase from langchain_community.utilities import SQLDatabase
from langchain_core.messages import AIMessage from langchain_core.messages import AIMessage
@@ -1422,15 +1422,15 @@ const builder = new StateGraph(State)
::: :::
??? info "Why split application steps into a sequence with LangGraph?" ??? info "Why split application steps into a sequence with LangGraph?"
LangGraph makes it easy to add an underlying persistence layer to your application. LangGraph makes it easy to add an underlying persistence layer to your application.
This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern: This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern:
- How state updates are [checkpointed](../concepts/persistence.md) - How state updates are [checkpointed](../concepts/persistence.md)
- How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows - How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows
- How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features - How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features
They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized
and debugged using [LangGraph Studio](../concepts/langgraph_studio.md). and debugged using [LangGraph Studio](../concepts/langgraph_studio.md).
Let's demonstrate an end-to-end example. We will create a sequence of three steps: Let's demonstrate an end-to-end example. We will create a sequence of three steps:
@@ -2110,6 +2110,7 @@ builder.add_edge(START, "generate_topics")
builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"]) builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
builder.add_edge("generate_joke", "best_joke") builder.add_edge("generate_joke", "best_joke")
builder.add_edge("best_joke", END) builder.add_edge("best_joke", END)
builder.add_edge("generate_topics", END)
graph = builder.compile() graph = builder.compile()
``` ```
@@ -2332,7 +2333,7 @@ from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png())) display(Image(graph.get_graph().draw_mermaid_png()))
``` ```
![Simple loop graph](assets/graph_api_image_7.png) ![Simple loop graph](assets/graph_api_image_3.png)
::: :::
:::js :::js
@@ -3271,7 +3272,7 @@ from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeSt
display(Image(app.get_graph().draw_mermaid_png())) display(Image(app.get_graph().draw_mermaid_png()))
``` ```
![Fractal graph visualization](assets/graph_api_image_10.png) ![Fractal graph visualization](assets/graph_api_image_5.png)
**Using Mermaid + Pyppeteer** **Using Mermaid + Pyppeteer**
@@ -366,8 +366,8 @@ result = graph.invoke(
# Resume with mapping of interrupt IDs to values # Resume with mapping of interrupt IDs to values
resume_map = { resume_map = {
i.id: f"edited text for {i.value['text_to_revise']}" i.interrupt_id: f"human input for prompt {i.value}"
for i in graph.get_state(config).interrupts for i in parent.get_state(thread_config).interrupts
} }
print(graph.invoke(Command(resume=resume_map), config=config)) print(graph.invoke(Command(resume=resume_map), config=config))
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'} # > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
+1 -1
View File
@@ -1948,7 +1948,7 @@ const llmWithTools = llm.bindTools(tools);
# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call # Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
def should_continue(state: MessagesState) -> Literal["Action", END]: def should_continue(state: MessagesState) -> Literal["environment", END]:
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call""" """Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
messages = state["messages"] messages = state["messages"]
+2 -2
View File
@@ -291,7 +291,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
} }
.md-banner { .md-banner {
background-color: #FFAE42; background-color: #CFC9FA;
color: #000000; color: #000000;
} }
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
{% endblock %} {% endblock %}
{% block announce %} {% block announce %}
These docs will be deprecated and removed with the release of LangGraph v1.0 in October 2025. <a href="https://docs.langchain.com/oss/python/langgraph/overview" target="_blank">Visit the v1.0 alpha docs</a> <strong>LangGraph Platform docs have moved!</strong> Find the LangGraph Platform docs at the new <a href="https://docs.langchain.com/langgraph-platform" target="_blank">LangChain Docs</a> site.
{% endblock %} {% endblock %}
+4 -4
View File
@@ -7,14 +7,14 @@ name = "langgraph-docs"
version = "0.0.1" version = "0.0.1"
description = "LangGraph docs" description = "LangGraph docs"
authors = [] authors = []
requires-python = ">=3.11.0,<4.0.0" requires-python = "~=3.11"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
dependencies = [ dependencies = [
"aiohappyeyeballs==2.4.3", "aiohappyeyeballs==2.4.3",
"hub>=3.0.1,<4.0.0", "hub>=3.0.1,<4",
"xxhash>=3.5.0,<4.0.0", "xxhash>=3.5.0,<4",
"black>=25.1.0,<26.0.0", "black>=25.1.0,<26",
] ]
[dependency-groups] [dependency-groups]
Generated
+4 -5
View File
@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 3 revision = 2
requires-python = ">=3.11, <4" requires-python = ">=3.11, <4"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'", "python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
@@ -2337,7 +2337,7 @@ wheels = [
[[package]] [[package]]
name = "langgraph" name = "langgraph"
version = "0.6.7" version = "0.6.2"
source = { editable = "../libs/langgraph" } source = { editable = "../libs/langgraph" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
@@ -2380,7 +2380,6 @@ dev = [
{ name = "pytest-repeat" }, { name = "pytest-repeat" },
{ name = "pytest-watcher" }, { name = "pytest-watcher" },
{ name = "pytest-xdist", extras = ["psutil"] }, { name = "pytest-xdist", extras = ["psutil"] },
{ name = "redis" },
{ name = "ruff" }, { name = "ruff" },
{ name = "syrupy" }, { name = "syrupy" },
{ name = "types-requests" }, { name = "types-requests" },
@@ -2414,7 +2413,6 @@ dev = [
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-mock" }, { name = "pytest-mock" },
{ name = "pytest-watcher" }, { name = "pytest-watcher" },
{ name = "redis" },
{ name = "ruff" }, { name = "ruff" },
] ]
@@ -2645,7 +2643,7 @@ test = [
[[package]] [[package]]
name = "langgraph-prebuilt" name = "langgraph-prebuilt"
version = "0.6.4" version = "0.6.2"
source = { editable = "../libs/prebuilt" } source = { editable = "../libs/prebuilt" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
@@ -2676,6 +2674,7 @@ dev = [
[[package]] [[package]]
name = "langgraph-sdk" name = "langgraph-sdk"
version = "0.2.0"
source = { editable = "../libs/sdk-py" } source = { editable = "../libs/sdk-py" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "18526f23", "id": "18526f23",
"metadata": {}, "metadata": {},
"source": [ "source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md" "This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_postgres.ipynb"
] ]
} }
], ],
+1 -3
View File
@@ -707,9 +707,7 @@
" \"\"\"\n", " \"\"\"\n",
" Find all tool calls in the messages returned\n", " Find all tool calls in the messages returned\n",
" \"\"\"\n", " \"\"\"\n",
" tool_calls = [\n", " tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
" ]\n",
" return tool_calls\n", " return tool_calls\n",
"\n", "\n",
"\n", "\n",
@@ -7,6 +7,11 @@ from contextlib import contextmanager
from typing import Any from typing import Any
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
WRITES_IDX_MAP, WRITES_IDX_MAP,
ChannelVersions, ChannelVersions,
@@ -16,15 +21,10 @@ from langgraph.checkpoint.base import (
get_checkpoint_id, get_checkpoint_id,
get_checkpoint_metadata, get_checkpoint_metadata,
) )
from langgraph.checkpoint.serde.base import SerializerProtocol
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.postgres import _internal from langgraph.checkpoint.postgres import _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
Conn = _internal.Conn # For backward compatibility Conn = _internal.Conn # For backward compatibility
@@ -450,7 +450,7 @@ class PostgresSaver(BasePostgresSaver):
{ {
**value["checkpoint"], **value["checkpoint"],
"channel_values": { "channel_values": {
**(value["checkpoint"].get("channel_values") or {}), **value["checkpoint"].get("channel_values"),
**self._load_blobs(value["channel_values"]), **self._load_blobs(value["channel_values"]),
}, },
}, },
@@ -7,6 +7,11 @@ from contextlib import asynccontextmanager
from typing import Any from typing import Any
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
WRITES_IDX_MAP, WRITES_IDX_MAP,
ChannelVersions, ChannelVersions,
@@ -16,15 +21,10 @@ from langgraph.checkpoint.base import (
get_checkpoint_id, get_checkpoint_id,
get_checkpoint_metadata, get_checkpoint_metadata,
) )
from langgraph.checkpoint.serde.base import SerializerProtocol
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal from langgraph.checkpoint.postgres import _ainternal
from langgraph.checkpoint.postgres.base import BasePostgresSaver from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
Conn = _ainternal.Conn # For backward compatibility Conn = _ainternal.Conn # For backward compatibility
@@ -409,7 +409,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
{ {
**value["checkpoint"], **value["checkpoint"],
"channel_values": { "channel_values": {
**(value["checkpoint"].get("channel_values") or {}), **value["checkpoint"].get("channel_values"),
**self._load_blobs(value["channel_values"]), **self._load_blobs(value["channel_values"]),
}, },
}, },
@@ -5,6 +5,8 @@ from collections.abc import Sequence
from typing import Any, Optional, cast from typing import Any, Optional, cast
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from psycopg.types.json import Jsonb
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
WRITES_IDX_MAP, WRITES_IDX_MAP,
BaseCheckpointSaver, BaseCheckpointSaver,
@@ -12,7 +14,6 @@ from langgraph.checkpoint.base import (
get_checkpoint_id, get_checkpoint_id,
) )
from langgraph.checkpoint.serde.types import TASKS from langgraph.checkpoint.serde.types import TASKS
from psycopg.types.json import Jsonb
MetadataInput = Optional[dict[str, Any]] MetadataInput = Optional[dict[str, Any]]
@@ -6,16 +6,6 @@ from contextlib import asynccontextmanager, contextmanager
from typing import Any, Optional from typing import Any, Optional
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_metadata,
)
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.types import TASKS
from psycopg import ( from psycopg import (
AsyncConnection, AsyncConnection,
AsyncCursor, AsyncCursor,
@@ -29,8 +19,18 @@ from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool, ConnectionPool from psycopg_pool import AsyncConnectionPool, ConnectionPool
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_metadata,
)
from langgraph.checkpoint.postgres import _ainternal, _internal from langgraph.checkpoint.postgres import _ainternal, _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.types import TASKS
""" """
To add a new migration, add a new string to the MIGRATIONS list. To add a new migration, add a new string to the MIGRATIONS list.
@@ -1,4 +1,4 @@
from langgraph.store.postgres.aio import AsyncPostgresStore from langgraph.store.postgres.aio import AsyncPostgresStore
from langgraph.store.postgres.base import PoolConfig, PostgresStore from langgraph.store.postgres.base import PostgresStore
__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"] __all__ = ["AsyncPostgresStore", "PostgresStore"]
@@ -8,6 +8,11 @@ from types import TracebackType
from typing import Any, Callable, cast from typing import Any, Callable, cast
import orjson import orjson
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal
from langgraph.store.base import ( from langgraph.store.base import (
GetOp, GetOp,
ListNamespacesOp, ListNamespacesOp,
@@ -17,11 +22,6 @@ from langgraph.store.base import (
SearchOp, SearchOp,
) )
from langgraph.store.base.batch import AsyncBatchedBaseStore from langgraph.store.base.batch import AsyncBatchedBaseStore
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal
from langgraph.store.postgres.base import ( from langgraph.store.postgres.base import (
PLACEHOLDER, PLACEHOLDER,
BasePostgresStore, BasePostgresStore,
@@ -22,6 +22,14 @@ from typing import (
) )
import orjson import orjson
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from typing_extensions import TypedDict
from langgraph.checkpoint.postgres import _ainternal as _ainternal
from langgraph.checkpoint.postgres import _internal as _pg_internal
from langgraph.store.base import ( from langgraph.store.base import (
BaseStore, BaseStore,
GetOp, GetOp,
@@ -38,14 +46,6 @@ from langgraph.store.base import (
get_text_at_path, get_text_at_path,
tokenize_path, tokenize_path,
) )
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from typing_extensions import TypedDict
from langgraph.checkpoint.postgres import _ainternal as _ainternal
from langgraph.checkpoint.postgres import _internal as _pg_internal
if TYPE_CHECKING: if TYPE_CHECKING:
from langchain_core.embeddings import Embeddings from langchain_core.embeddings import Embeddings
+5 -36
View File
@@ -6,6 +6,10 @@ from uuid import uuid4
import pytest import pytest
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS, EXCLUDED_METADATA_KEYS,
Checkpoint, Checkpoint,
@@ -13,15 +17,11 @@ from langgraph.checkpoint.base import (
create_checkpoint, create_checkpoint,
empty_checkpoint, empty_checkpoint,
) )
from langgraph.checkpoint.serde.types import TASKS
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres.aio import ( from langgraph.checkpoint.postgres.aio import (
AsyncPostgresSaver, AsyncPostgresSaver,
AsyncShallowPostgresSaver, AsyncShallowPostgresSaver,
) )
from langgraph.checkpoint.serde.types import TASKS
from tests.conftest import DEFAULT_POSTGRES_URI from tests.conftest import DEFAULT_POSTGRES_URI
@@ -344,34 +344,3 @@ async def test_pending_sends_migration(saver_name: str) -> None:
TASKS: ["send-1", "send-2", "send-3"] TASKS: ["send-1", "send-2", "send-3"]
} }
assert TASKS in search_results[0].checkpoint["channel_versions"] assert TASKS in search_results[0].checkpoint["channel_versions"]
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
async def test_get_checkpoint_no_channel_values(
monkeypatch, saver_name: str, test_data
) -> None:
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
async with _saver(saver_name) as saver:
config = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "",
"__super_private_key": "super_private_value",
},
"metadata": {"run_id": "my_run_id"},
}
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
await saver.aput(config, chkpnt, {}, {})
load_checkpoint_tuple = saver._load_checkpoint_tuple
def patched_load_checkpoint_tuple(value):
value["checkpoint"].pop("channel_values", None)
return load_checkpoint_tuple(value)
monkeypatch.setattr(
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
)
checkpoint = await saver.aget_tuple(config)
assert checkpoint.checkpoint["channel_values"] == {}
@@ -12,6 +12,8 @@ from typing import Any
import pytest import pytest
from langchain_core.embeddings import Embeddings from langchain_core.embeddings import Embeddings
from psycopg import AsyncConnection
from langgraph.store.base import ( from langgraph.store.base import (
GetOp, GetOp,
Item, Item,
@@ -19,8 +21,6 @@ from langgraph.store.base import (
PutOp, PutOp,
SearchOp, SearchOp,
) )
from psycopg import AsyncConnection
from langgraph.store.postgres import AsyncPostgresStore from langgraph.store.postgres import AsyncPostgresStore
from tests.conftest import ( from tests.conftest import (
DEFAULT_URI, DEFAULT_URI,
+2 -40
View File
@@ -9,6 +9,8 @@ from uuid import uuid4
import pytest import pytest
from langchain_core.embeddings import Embeddings from langchain_core.embeddings import Embeddings
from psycopg import Connection
from langgraph.store.base import ( from langgraph.store.base import (
GetOp, GetOp,
Item, Item,
@@ -17,8 +19,6 @@ from langgraph.store.base import (
PutOp, PutOp,
SearchOp, SearchOp,
) )
from psycopg import Connection
from langgraph.store.postgres import PostgresStore from langgraph.store.postgres import PostgresStore
from tests.conftest import ( from tests.conftest import (
DEFAULT_URI, DEFAULT_URI,
@@ -861,41 +861,3 @@ def test_store_ttl(store):
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2 # Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
res = store.search(ns, query="bar", refresh_ttl=False) res = store.search(ns, query="bar", refresh_ttl=False)
assert len(res) == 0 assert len(res) == 0
@pytest.mark.parametrize(
"vector_type,distance_type",
[
("vector", "cosine"),
("vector", "inner_product"),
("halfvec", "cosine"),
("halfvec", "inner_product"),
],
)
def test_non_ascii(
request: Any,
fake_embeddings: CharacterEmbeddings,
vector_type: str,
distance_type: str,
) -> None:
"""Test support for non-ascii characters"""
with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(
("user_123", "memories"), "2", {"text": "これは日本語です"}
) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
+5 -35
View File
@@ -7,6 +7,10 @@ from uuid import uuid4
import pytest import pytest
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from psycopg import Connection
from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS, EXCLUDED_METADATA_KEYS,
Checkpoint, Checkpoint,
@@ -14,12 +18,8 @@ from langgraph.checkpoint.base import (
create_checkpoint, create_checkpoint,
empty_checkpoint, empty_checkpoint,
) )
from langgraph.checkpoint.serde.types import TASKS
from psycopg import Connection
from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
from langgraph.checkpoint.serde.types import TASKS
from tests.conftest import DEFAULT_POSTGRES_URI from tests.conftest import DEFAULT_POSTGRES_URI
@@ -332,33 +332,3 @@ def test_pending_sends_migration(saver_name: str) -> None:
TASKS: ["send-1", "send-2", "send-3"] TASKS: ["send-1", "send-2", "send-3"]
} }
assert TASKS in search_results[0].checkpoint["channel_versions"] assert TASKS in search_results[0].checkpoint["channel_versions"]
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
def test_get_checkpoint_no_channel_values(
monkeypatch, saver_name: str, test_data
) -> None:
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
with _saver(saver_name) as saver:
config = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "",
"__super_private_key": "super_private_value",
},
}
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
saver.put(config, chkpnt, {}, {})
load_checkpoint_tuple = saver._load_checkpoint_tuple
def patched_load_checkpoint_tuple(value):
value["checkpoint"].pop("channel_values", None)
return load_checkpoint_tuple(value)
monkeypatch.setattr(
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
)
checkpoint = saver.get_tuple(config)
assert checkpoint.checkpoint["channel_values"] == {}
+498 -483
View File
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@ from contextlib import closing, contextmanager
from typing import Any, cast from typing import Any, cast
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
WRITES_IDX_MAP, WRITES_IDX_MAP,
BaseCheckpointSaver, BaseCheckpointSaver,
@@ -20,7 +21,6 @@ from langgraph.checkpoint.base import (
get_checkpoint_metadata, get_checkpoint_metadata,
) )
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite.utils import search_where from langgraph.checkpoint.sqlite.utils import search_where
_AIO_ERROR_MSG = ( _AIO_ERROR_MSG = (
@@ -8,6 +8,7 @@ from typing import Any, Callable, TypeVar, cast
import aiosqlite import aiosqlite
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
WRITES_IDX_MAP, WRITES_IDX_MAP,
BaseCheckpointSaver, BaseCheckpointSaver,
@@ -20,7 +21,6 @@ from langgraph.checkpoint.base import (
get_checkpoint_metadata, get_checkpoint_metadata,
) )
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite.utils import search_where from langgraph.checkpoint.sqlite.utils import search_where
T = TypeVar("T", bound=Callable) T = TypeVar("T", bound=Callable)
@@ -5,6 +5,7 @@ from collections.abc import Sequence
from typing import Any from typing import Any
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import get_checkpoint_id from langgraph.checkpoint.base import get_checkpoint_id
@@ -11,6 +11,7 @@ from typing import Any, Callable, cast
import aiosqlite import aiosqlite
import orjson import orjson
import sqlite_vec # type: ignore[import-untyped] import sqlite_vec # type: ignore[import-untyped]
from langgraph.store.base import ( from langgraph.store.base import (
GetOp, GetOp,
ListNamespacesOp, ListNamespacesOp,
@@ -21,7 +22,6 @@ from langgraph.store.base import (
TTLConfig, TTLConfig,
) )
from langgraph.store.base.batch import AsyncBatchedBaseStore from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.sqlite.base import ( from langgraph.store.sqlite.base import (
_PLACEHOLDER, _PLACEHOLDER,
BaseSqliteStore, BaseSqliteStore,
@@ -507,9 +507,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
results: List to store results in. results: List to store results in.
cur: Database cursor. cur: Database cursor.
""" """
prepared_queries, embedding_requests = self._prepare_batch_search_queries( queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
search_ops
)
# Setup dot_product function if it doesn't exist # Setup dot_product function if it doesn't exist
if embedding_requests and self.embeddings: if embedding_requests and self.embeddings:
@@ -517,60 +515,23 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
[query for _, query in embedding_requests] [query for _, query in embedding_requests]
) )
for (embed_req_idx, _), embedding in zip(embedding_requests, vectors): for (idx, _), embedding in zip(embedding_requests, vectors):
# Find the corresponding query in prepared_queries _params_list: list = queries[idx][1]
# The embed_req_idx is the original index in search_ops, which should map to prepared_queries for i, param in enumerate(_params_list):
if embed_req_idx < len(prepared_queries): if param is _PLACEHOLDER:
_params_list: list = prepared_queries[embed_req_idx][1] _params_list[i] = sqlite_vec.serialize_float32(embedding)
for i, param in enumerate(_params_list):
if param is _PLACEHOLDER:
_params_list[i] = sqlite_vec.serialize_float32(embedding)
else:
logger.warning(
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
)
for (original_op_idx, _), (query, params, needs_refresh) in zip( for (idx, _), (query, params) in zip(search_ops, queries):
search_ops, prepared_queries
):
await cur.execute(query, params) await cur.execute(query, params)
rows = await cur.fetchall() rows = await cur.fetchall()
if needs_refresh and rows and self.ttl_config: if "score" in query:
keys_to_refresh = []
for row_data in rows:
# Assuming row_data[0] is prefix (text), row_data[1] is key (text)
# These are raw text values directly from the DB.
keys_to_refresh.append((row_data[0], row_data[1]))
if keys_to_refresh:
updates_by_prefix = defaultdict(list)
for prefix_text, key_text in keys_to_refresh:
updates_by_prefix[prefix_text].append(key_text)
for prefix_text, key_list in updates_by_prefix.items():
placeholders = ",".join(["?"] * len(key_list))
update_query = f"""
UPDATE store
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
"""
update_params = (prefix_text, *key_list)
try:
await cur.execute(update_query, update_params)
except Exception as e:
logger.error(
f"Error during TTL refresh update for search: {e}"
)
# Process rows into items
if "score" in query: # Vector search query
items = [ items = [
_row_to_search_item( _row_to_search_item(
_decode_ns_text(row[0]), # prefix _decode_ns_text(row[0]),
{ {
"key": row[1], # key "key": row[1],
"value": row[2], # value "value": row[2],
"created_at": row[3], "created_at": row[3],
"updated_at": row[4], "updated_at": row[4],
"expires_at": row[5] if len(row) > 5 else None, "expires_at": row[5] if len(row) > 5 else None,
@@ -584,10 +545,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
else: # Regular search query else: # Regular search query
items = [ items = [
_row_to_search_item( _row_to_search_item(
_decode_ns_text(row[0]), # prefix _decode_ns_text(row[0]),
{ {
"key": row[1], # key "key": row[1],
"value": row[2], # value "value": row[2],
"created_at": row[3], "created_at": row[3],
"updated_at": row[4], "updated_at": row[4],
"expires_at": row[5] if len(row) > 5 else None, "expires_at": row[5] if len(row) > 5 else None,
@@ -598,7 +559,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
for row in rows for row in rows
] ]
results[original_op_idx] = items results[idx] = items
async def _batch_list_namespaces_ops( async def _batch_list_namespaces_ops(
self, self,
@@ -13,6 +13,7 @@ from typing import Any, Callable, Literal, NamedTuple, cast
import orjson import orjson
import sqlite_vec # type: ignore[import-untyped] import sqlite_vec # type: ignore[import-untyped]
from langgraph.store.base import ( from langgraph.store.base import (
BaseStore, BaseStore,
GetOp, GetOp,
@@ -371,15 +372,13 @@ class BaseSqliteStore:
def _prepare_batch_search_queries( def _prepare_batch_search_queries(
self, search_ops: Sequence[tuple[int, SearchOp]] self, search_ops: Sequence[tuple[int, SearchOp]]
) -> tuple[ ) -> tuple[
list[ list[tuple[str, list[None | str | list[float]]]], # queries, params
tuple[str, list[None | str | list[float]], bool]
], # queries, params, needs_refresh
list[tuple[int, str]], # idx, query_text pairs to embed list[tuple[int, str]], # idx, query_text pairs to embed
]: ]:
""" """
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests. Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
Returns: Returns:
- queries: list of (SQL, param_list, needs_ttl_refresh_flag) - queries: list of (SQL, param_list)
- embedding_requests: list of (original_index_in_search_ops, text_query) - embedding_requests: list of (original_index_in_search_ops, text_query)
""" """
queries = [] queries = []
@@ -520,18 +519,30 @@ class BaseSqliteStore:
logger.debug(f"Search query: {base_query}") logger.debug(f"Search query: {base_query}")
logger.debug(f"Search params: {params}") logger.debug(f"Search params: {params}")
# Determine if TTL refresh is needed # Handle TTL refresh if requested
needs_ttl_refresh = bool( if (
op.refresh_ttl op.refresh_ttl
and self.ttl_config and self.ttl_config
and self.ttl_config.get("refresh_on_read", False) and self.ttl_config.get("refresh_on_read", False)
) ):
final_sql = f"""
WITH search_results AS (
{base_query}
),
updated AS (
UPDATE store
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
AND ttl_minutes IS NOT NULL
)
SELECT * FROM search_results
"""
final_params = params[:] # copy params
else:
final_sql = base_query
final_params = params
# The base_query is now the final_sql, and we pass the refresh flag queries.append((final_sql, final_params))
final_sql = base_query
final_params = params
queries.append((final_sql, final_params, needs_ttl_refresh))
return queries, embedding_requests return queries, embedding_requests
@@ -1320,9 +1331,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
results: list[Result], results: list[Result],
cur: sqlite3.Cursor, cur: sqlite3.Cursor,
) -> None: ) -> None:
prepared_queries, embedding_requests = self._prepare_batch_search_queries( queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
search_ops
)
# Setup similarity functions if they don't exist # Setup similarity functions if they don't exist
if embedding_requests and self.embeddings: if embedding_requests and self.embeddings:
@@ -1332,48 +1341,16 @@ class SqliteStore(BaseSqliteStore, BaseStore):
) )
# Replace placeholders with actual embeddings # Replace placeholders with actual embeddings
for (embed_req_idx, _), embedding in zip(embedding_requests, embeddings): for (idx, _), embedding in zip(embedding_requests, embeddings):
if embed_req_idx < len(prepared_queries): _params_list: list = queries[idx][1]
_params_list: list = prepared_queries[embed_req_idx][1] for i, param in enumerate(_params_list):
for i, param in enumerate(_params_list): if param is _PLACEHOLDER:
if param is _PLACEHOLDER: _params_list[i] = sqlite_vec.serialize_float32(embedding)
_params_list[i] = sqlite_vec.serialize_float32(embedding)
else:
logger.warning(
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
)
for (original_op_idx, _), (query, params, needs_refresh) in zip( for (idx, _), (query, params) in zip(search_ops, queries):
search_ops, prepared_queries
):
cur.execute(query, params) cur.execute(query, params)
rows = cur.fetchall() rows = cur.fetchall()
if needs_refresh and rows and self.ttl_config:
keys_to_refresh = []
for row_data in rows:
keys_to_refresh.append((row_data[0], row_data[1]))
if keys_to_refresh:
updates_by_prefix = defaultdict(list)
for prefix_text, key_text in keys_to_refresh:
updates_by_prefix[prefix_text].append(key_text)
for prefix_text, key_list in updates_by_prefix.items():
placeholders = ",".join(["?"] * len(key_list))
update_query = f"""
UPDATE store
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
"""
update_params = (prefix_text, *key_list)
try:
cur.execute(update_query, update_params)
except Exception as e:
logger.error(
f"Error during TTL refresh update for search: {e}"
)
if "score" in query: # Vector search query if "score" in query: # Vector search query
items = [ items = [
_row_to_search_item( _row_to_search_item(
@@ -1408,7 +1385,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
for row in rows for row in rows
] ]
results[original_op_idx] = items results[idx] = items
def _batch_list_namespaces_ops( def _batch_list_namespaces_ops(
self, self,
@@ -2,13 +2,13 @@ from typing import Any
import pytest import pytest
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
Checkpoint, Checkpoint,
CheckpointMetadata, CheckpointMetadata,
create_checkpoint, create_checkpoint,
empty_checkpoint, empty_checkpoint,
) )
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
@@ -8,6 +8,7 @@ from contextlib import asynccontextmanager
from typing import Optional, Union, cast from typing import Optional, Union, cast
import pytest import pytest
from langgraph.store.base import ( from langgraph.store.base import (
GetOp, GetOp,
Item, Item,
@@ -15,7 +16,6 @@ from langgraph.store.base import (
PutOp, PutOp,
SearchOp, SearchOp,
) )
from langgraph.store.sqlite import AsyncSqliteStore from langgraph.store.sqlite import AsyncSqliteStore
from langgraph.store.sqlite.base import SqliteIndexConfig from langgraph.store.sqlite.base import SqliteIndexConfig
from tests.test_store import CharacterEmbeddings from tests.test_store import CharacterEmbeddings
+2 -12
View File
@@ -2,13 +2,13 @@ from typing import Any, cast
import pytest import pytest
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import ( from langgraph.checkpoint.base import (
Checkpoint, Checkpoint,
CheckpointMetadata, CheckpointMetadata,
create_checkpoint, create_checkpoint,
empty_checkpoint, empty_checkpoint,
) )
from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
@@ -116,17 +116,7 @@ class TestSqliteSaver:
search_results_5[1].config["configurable"]["checkpoint_ns"], search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"} } == {"", "inner"}
# search with before param # TODO: test before and limit params
search_results_6 = list(saver.list(None, before=search_results_5[1].config))
assert len(search_results_6) == 1
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-1"
# search with limit param
search_results_7 = list(
saver.list({"configurable": {"thread_id": "thread-2"}}, limit=1)
)
assert len(search_results_7) == 1
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-2"
def test_search_where(self) -> None: def test_search_where(self) -> None:
# call method / assertions # call method / assertions
+1 -29
View File
@@ -9,6 +9,7 @@ from typing import Any, Literal, Optional, Union, cast
import pytest import pytest
from langchain_core.embeddings import Embeddings from langchain_core.embeddings import Embeddings
from langgraph.store.base import ( from langgraph.store.base import (
GetOp, GetOp,
Item, Item,
@@ -17,7 +18,6 @@ from langgraph.store.base import (
PutOp, PutOp,
SearchOp, SearchOp,
) )
from langgraph.store.sqlite import SqliteStore from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.base import SqliteIndexConfig from langgraph.store.sqlite.base import SqliteIndexConfig
@@ -1067,31 +1067,3 @@ def test_sql_injection_vulnerability(store: SqliteStore) -> None:
with pytest.raises(ValueError, match="Invalid filter key"): with pytest.raises(ValueError, match="Invalid filter key"):
store.search(("docs",), filter={malicious_key: "dummy"}) store.search(("docs",), filter={malicious_key: "dummy"})
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_non_ascii(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test support for non-ascii characters"""
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(
("user_123", "memories"), "2", {"text": "これは日本語です"}
) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
+2 -76
View File
@@ -7,7 +7,6 @@ import time
from collections.abc import Generator from collections.abc import Generator
import pytest import pytest
from langgraph.store.base import TTLConfig
from langgraph.store.sqlite import SqliteStore from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.aio import AsyncSqliteStore from langgraph.store.sqlite.aio import AsyncSqliteStore
@@ -94,13 +93,9 @@ def test_ttl_sweeper(temp_db_file: str) -> None:
ttl_seconds = 2 ttl_seconds = 2
ttl_minutes = ttl_seconds / 60 ttl_minutes = ttl_seconds / 60
ttl_config: TTLConfig = {
"default_ttl": ttl_minutes,
"sweep_interval_minutes": ttl_minutes / 2,
}
with SqliteStore.from_conn_string( with SqliteStore.from_conn_string(
temp_db_file, temp_db_file,
ttl=ttl_config, ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
) as store: ) as store:
store.setup() store.setup()
@@ -303,14 +298,9 @@ async def test_async_ttl_sweeper(temp_db_file: str) -> None:
ttl_seconds = 2 ttl_seconds = 2
ttl_minutes = ttl_seconds / 60 ttl_minutes = ttl_seconds / 60
ttl_config: TTLConfig = {
"default_ttl": ttl_minutes,
"sweep_interval_minutes": ttl_minutes / 2,
}
async with AsyncSqliteStore.from_conn_string( async with AsyncSqliteStore.from_conn_string(
temp_db_file, temp_db_file,
ttl=ttl_config, ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
) as store: ) as store:
await store.setup() await store.setup()
@@ -363,67 +353,3 @@ async def test_async_search_with_ttl(temp_db_file: str) -> None:
# Search after expiration # Search after expiration
results = await store.asearch(("test",), filter={"value": "apple"}) results = await store.asearch(("test",), filter={"value": "apple"})
assert len(results) == 0 assert len(results) == 0
@pytest.mark.asyncio
@pytest.mark.flaky(retries=3)
async def test_async_asearch_refresh_ttl(temp_db_file: str) -> None:
"""Test TTL refresh on asearch with async API."""
ttl_seconds = 4.0 # Increased TTL for less sensitivity to timing
ttl_minutes = ttl_seconds / 60.0
async with AsyncSqliteStore.from_conn_string(
temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
) as store:
await store.setup()
namespace = ("docs", "user1")
# t=0: items put, expire at t=4.0s
await store.aput(namespace, "item1", {"text": "content1", "id": 1})
await store.aput(namespace, "item2", {"text": "content2", "id": 2})
# t=3.0s: (after sleep ttl_seconds * 0.75 = 3s)
await asyncio.sleep(ttl_seconds * 0.75)
# Perform asearch with refresh_ttl=True for item1.
# item1's TTL should be refreshed. New expiry: t=3.0s + 4.0s = t=7.0s.
# item2's TTL is not affected. Expires at t=4.0s.
searched_items = await store.asearch(
namespace, filter={"id": 1}, refresh_ttl=True
)
assert len(searched_items) == 1
assert searched_items[0].key == "item1"
# t=5.0s: (after sleep ttl_seconds * 0.5 = 2s more. Total elapsed: 3s + 2s = 5s)
await asyncio.sleep(ttl_seconds * 0.5)
# At this point:
# - item1 (refreshed by asearch) should expire at t=7.0s. Should be ALIVE.
# - item2 (original TTL) should have expired at t=4.0s. Should be GONE after sweep.
await store.sweep_ttl()
# Check item1 (should exist due to asearch refresh)
item1_check1 = await store.aget(namespace, "item1", refresh_ttl=False)
assert item1_check1 is not None, (
"Item1 should exist after asearch refresh and first sweep"
)
assert item1_check1.value["text"] == "content1"
# Check item2 (should be gone)
item2_check1 = await store.aget(namespace, "item2", refresh_ttl=False)
assert item2_check1 is None, (
"Item2 should be gone after its original TTL expired"
)
# t=7.5s: (after sleep ttl_seconds * 0.625 = 2.5s more. Total elapsed: 5s + 2.5s = 7.5s)
await asyncio.sleep(ttl_seconds * 0.625)
# At this point:
# - item1 (refreshed by asearch, expired at t=7.0s) should be GONE after sweep.
await store.sweep_ttl()
# Check item1 again (should be gone now)
item1_final_check = await store.aget(namespace, "item1", refresh_ttl=False)
assert item1_final_check is None, (
"Item1 should be gone after its refreshed TTL expired"
)
+439 -425
View File
File diff suppressed because it is too large Load Diff
@@ -238,7 +238,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
- Nested paths in multi-field: "{field1,nested.field2}" - Nested paths in multi-field: "{field1,nested.field2}"
""" """
if not path or path == "$": if not path or path == "$":
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)] return [json.dumps(obj, sort_keys=True)]
tokens = tokenize_path(path) if isinstance(path, str) else path tokens = tokenize_path(path) if isinstance(path, str) else path
@@ -249,7 +249,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
elif obj is None: elif obj is None:
return [] return []
elif isinstance(obj, (list, dict)): elif isinstance(obj, (list, dict)):
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)] return [json.dumps(obj, sort_keys=True)]
return [] return []
token = tokens[pos] token = tokens[pos]
@@ -295,11 +295,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
if isinstance(current_obj, (str, int, float, bool)): if isinstance(current_obj, (str, int, float, bool)):
results.append(str(current_obj)) results.append(str(current_obj))
elif isinstance(current_obj, (list, dict)): elif isinstance(current_obj, (list, dict)):
results.append( results.append(json.dumps(current_obj, sort_keys=True))
json.dumps(
current_obj, sort_keys=True, ensure_ascii=False
)
)
# Handle wildcard # Handle wildcard
elif token == "*": elif token == "*":
+48 -51
View File
@@ -5,13 +5,12 @@ import time
import pytest import pytest
import redis import redis
from langgraph.cache.base import FullKey
from langgraph.cache.redis import RedisCache from langgraph.cache.redis import RedisCache
class TestRedisCache: class TestRedisCache:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self) -> None: def setup(self):
"""Set up test Redis client and cache.""" """Set up test Redis client and cache."""
self.client = redis.Redis( self.client = redis.Redis(
host="localhost", port=6379, db=0, decode_responses=False host="localhost", port=6379, db=0, decode_responses=False
@@ -21,21 +20,21 @@ class TestRedisCache:
except redis.ConnectionError: except redis.ConnectionError:
pytest.skip("Redis server not available") pytest.skip("Redis server not available")
self.cache: RedisCache = RedisCache(self.client, prefix="test:cache:") self.cache = RedisCache(self.client, prefix="test:cache:")
# Clean up before each test # Clean up before each test
self.client.flushdb() self.client.flushdb()
def teardown_method(self) -> None: def teardown_method(self):
"""Clean up after each test.""" """Clean up after each test."""
try: try:
self.client.flushdb() self.client.flushdb()
except Exception: except Exception:
pass pass
def test_basic_set_and_get(self) -> None: def test_basic_set_and_get(self):
"""Test basic set and get operations.""" """Test basic set and get operations."""
keys: list[FullKey] = [(("graph", "node"), "key1")] keys = [(("graph", "node"), "key1")]
values = {keys[0]: ({"result": 42}, None)} values = {keys[0]: ({"result": 42}, None)}
# Set value # Set value
@@ -46,9 +45,9 @@ class TestRedisCache:
assert len(result) == 1 assert len(result) == 1
assert result[keys[0]] == {"result": 42} assert result[keys[0]] == {"result": 42}
def test_batch_operations(self) -> None: def test_batch_operations(self):
"""Test batch set and get operations.""" """Test batch set and get operations."""
keys: list[FullKey] = [ keys = [
(("graph", "node1"), "key1"), (("graph", "node1"), "key1"),
(("graph", "node2"), "key2"), (("graph", "node2"), "key2"),
(("other", "node"), "key3"), (("other", "node"), "key3"),
@@ -69,9 +68,9 @@ class TestRedisCache:
assert result[keys[1]] == {"result": 2} assert result[keys[1]] == {"result": 2}
assert result[keys[2]] == {"result": 3} assert result[keys[2]] == {"result": 3}
def test_ttl_behavior(self) -> None: def test_ttl_behavior(self):
"""Test TTL (time-to-live) functionality.""" """Test TTL (time-to-live) functionality."""
key: FullKey = (("graph", "node"), "ttl_key") key = (("graph", "node"), "ttl_key")
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
# Set with TTL # Set with TTL
@@ -89,10 +88,10 @@ class TestRedisCache:
result = self.cache.get([key]) result = self.cache.get([key])
assert len(result) == 0 assert len(result) == 0
def test_namespace_isolation(self) -> None: def test_namespace_isolation(self):
"""Test that different namespaces are isolated.""" """Test that different namespaces are isolated."""
key1: FullKey = (("graph1", "node"), "same_key") key1 = (("graph1", "node"), "same_key")
key2: FullKey = (("graph2", "node"), "same_key") key2 = (("graph2", "node"), "same_key")
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)} values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
@@ -102,12 +101,9 @@ class TestRedisCache:
assert result[key1] == {"graph": 1} assert result[key1] == {"graph": 1}
assert result[key2] == {"graph": 2} assert result[key2] == {"graph": 2}
def test_clear_all(self) -> None: def test_clear_all(self):
"""Test clearing all cached values.""" """Test clearing all cached values."""
keys: list[FullKey] = [ keys = [(("graph", "node1"), "key1"), (("graph", "node2"), "key2")]
(("graph", "node1"), "key1"),
(("graph", "node2"), "key2"),
]
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)} values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
self.cache.set(values) self.cache.set(values)
@@ -123,9 +119,9 @@ class TestRedisCache:
result = self.cache.get(keys) result = self.cache.get(keys)
assert len(result) == 0 assert len(result) == 0
def test_clear_by_namespace(self) -> None: def test_clear_by_namespace(self):
"""Test clearing cached values by namespace.""" """Test clearing cached values by namespace."""
keys: list[FullKey] = [ keys = [
(("graph1", "node"), "key1"), (("graph1", "node"), "key1"),
(("graph2", "node"), "key2"), (("graph2", "node"), "key2"),
(("graph1", "other"), "key3"), (("graph1", "other"), "key3"),
@@ -146,7 +142,7 @@ class TestRedisCache:
assert len(result) == 1 assert len(result) == 1
assert result[keys[1]] == {"result": 2} assert result[keys[1]] == {"result": 2}
def test_empty_operations(self) -> None: def test_empty_operations(self):
"""Test behavior with empty keys/values.""" """Test behavior with empty keys/values."""
# Empty get # Empty get
result = self.cache.get([]) result = self.cache.get([])
@@ -155,25 +151,27 @@ class TestRedisCache:
# Empty set # Empty set
self.cache.set({}) # Should not raise error self.cache.set({}) # Should not raise error
def test_nonexistent_keys(self) -> None: def test_nonexistent_keys(self):
"""Test getting keys that don't exist.""" """Test getting keys that don't exist."""
keys: list[FullKey] = [(("graph", "node"), "nonexistent")] keys = [(("graph", "node"), "nonexistent")]
result = self.cache.get(keys) result = self.cache.get(keys)
assert len(result) == 0 assert len(result) == 0
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_async_operations(self) -> None: async def test_async_operations(self):
"""Test async set and get operations with sync Redis client.""" """Test async set and get operations with sync Redis client."""
# Create sync Redis client and cache (like main integration tests) # Create sync Redis client and cache (like main integration tests)
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False) client = redis.Redis(
host="localhost", port=6379, db=1, decode_responses=False
)
try: try:
client.ping() client.ping()
except Exception: except Exception:
pytest.skip("Redis not available") pytest.skip("Redis not available")
cache: RedisCache = RedisCache(client, prefix="test:async:") cache = RedisCache(client, prefix="test:async:")
keys: list[FullKey] = [(("graph", "node"), "async_key")] keys = [(("graph", "node"), "async_key")]
values = {keys[0]: ({"async": True}, None)} values = {keys[0]: ({"async": True}, None)}
# Async set (delegates to sync) # Async set (delegates to sync)
@@ -188,18 +186,20 @@ class TestRedisCache:
client.flushdb() client.flushdb()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_async_clear(self) -> None: async def test_async_clear(self):
"""Test async clear operations with sync Redis client.""" """Test async clear operations with sync Redis client."""
# Create sync Redis client and cache (like main integration tests) # Create sync Redis client and cache (like main integration tests)
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False) client = redis.Redis(
host="localhost", port=6379, db=1, decode_responses=False
)
try: try:
client.ping() client.ping()
except Exception: except Exception:
pytest.skip("Redis not available") pytest.skip("Redis not available")
cache: RedisCache = RedisCache(client, prefix="test:async:") cache = RedisCache(client, prefix="test:async:")
keys: list[FullKey] = [(("graph", "node"), "key")] keys = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)} values = {keys[0]: ({"data": "test"}, None)}
await cache.aset(values) await cache.aset(values)
@@ -218,44 +218,44 @@ class TestRedisCache:
# Cleanup # Cleanup
client.flushdb() client.flushdb()
def test_redis_unavailable_get(self) -> None: def test_redis_unavailable_get(self):
"""Test behavior when Redis is unavailable during get operations.""" """Test behavior when Redis is unavailable during get operations."""
# Create cache with non-existent Redis server # Create cache with non-existent Redis server
bad_client = redis.Redis( bad_client = redis.Redis(
host="nonexistent", port=9999, socket_connect_timeout=0.1 host="nonexistent", port=9999, socket_connect_timeout=0.1
) )
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:") cache = RedisCache(bad_client, prefix="test:cache:")
keys: list[FullKey] = [(("graph", "node"), "key")] keys = [(("graph", "node"), "key")]
result = cache.get(keys) result = cache.get(keys)
# Should return empty dict when Redis unavailable # Should return empty dict when Redis unavailable
assert result == {} assert result == {}
def test_redis_unavailable_set(self) -> None: def test_redis_unavailable_set(self):
"""Test behavior when Redis is unavailable during set operations.""" """Test behavior when Redis is unavailable during set operations."""
# Create cache with non-existent Redis server # Create cache with non-existent Redis server
bad_client = redis.Redis( bad_client = redis.Redis(
host="nonexistent", port=9999, socket_connect_timeout=0.1 host="nonexistent", port=9999, socket_connect_timeout=0.1
) )
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:") cache = RedisCache(bad_client, prefix="test:cache:")
keys: list[FullKey] = [(("graph", "node"), "key")] keys = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)} values = {keys[0]: ({"data": "test"}, None)}
# Should not raise exception when Redis unavailable # Should not raise exception when Redis unavailable
cache.set(values) # Should silently fail cache.set(values) # Should silently fail
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_redis_unavailable_async(self) -> None: async def test_redis_unavailable_async(self):
"""Test async behavior when Redis is unavailable.""" """Test async behavior when Redis is unavailable."""
# Create sync cache with non-existent Redis server (like main integration tests) # Create sync cache with non-existent Redis server (like main integration tests)
bad_client = redis.Redis( bad_client = redis.Redis(
host="nonexistent", port=9999, socket_connect_timeout=0.1 host="nonexistent", port=9999, socket_connect_timeout=0.1
) )
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:") cache = RedisCache(bad_client, prefix="test:cache:")
keys: list[FullKey] = [(("graph", "node"), "key")] keys = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)} values = {keys[0]: ({"data": "test"}, None)}
# Should return empty dict for get (delegates to sync) # Should return empty dict for get (delegates to sync)
@@ -265,10 +265,10 @@ class TestRedisCache:
# Should not raise exception for set (delegates to sync) # Should not raise exception for set (delegates to sync)
await cache.aset(values) # Should silently fail await cache.aset(values) # Should silently fail
def test_corrupted_data_handling(self) -> None: def test_corrupted_data_handling(self):
"""Test handling of corrupted data in Redis.""" """Test handling of corrupted data in Redis."""
# Set some valid data first # Set some valid data first
keys: list[FullKey] = [(("graph", "node"), "valid_key")] keys = [(("graph", "node"), "valid_key")]
values = {keys[0]: ({"data": "valid"}, None)} values = {keys[0]: ({"data": "valid"}, None)}
self.cache.set(values) self.cache.set(values)
@@ -277,36 +277,33 @@ class TestRedisCache:
self.client.set(corrupted_key, b"invalid:data:format:too:many:colons") self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
# Should skip corrupted entry and return only valid ones # Should skip corrupted entry and return only valid ones
all_keys: list[FullKey] = [keys[0], (("graph", "node"), "corrupted_key")] all_keys = [keys[0], (("graph", "node"), "corrupted_key")]
result = self.cache.get(all_keys) result = self.cache.get(all_keys)
assert len(result) == 1 assert len(result) == 1
assert result[keys[0]] == {"data": "valid"} assert result[keys[0]] == {"data": "valid"}
def test_key_parsing_edge_cases(self) -> None: def test_key_parsing_edge_cases(self):
"""Test key parsing with edge cases.""" """Test key parsing with edge cases."""
# Test empty namespace # Test empty namespace
key1: FullKey = ((), "empty_ns") key1 = ((), "empty_ns")
values = {key1: ({"data": "empty_ns"}, None)} values = {key1: ({"data": "empty_ns"}, None)}
self.cache.set(values) self.cache.set(values)
result = self.cache.get([key1]) result = self.cache.get([key1])
assert result[key1] == {"data": "empty_ns"} assert result[key1] == {"data": "empty_ns"}
# Test namespace with special characters # Test namespace with special characters
key2: FullKey = ( key2 = (("graph:with:colons", "node-with-dashes"), "key_with_underscores")
("graph:with:colons", "node-with-dashes"),
"key_with_underscores",
)
values = {key2: ({"data": "special_chars"}, None)} values = {key2: ({"data": "special_chars"}, None)}
self.cache.set(values) self.cache.set(values)
result = self.cache.get([key2]) result = self.cache.get([key2])
assert result[key2] == {"data": "special_chars"} assert result[key2] == {"data": "special_chars"}
def test_large_data_serialization(self) -> None: def test_large_data_serialization(self):
"""Test handling of large data objects.""" """Test handling of large data objects."""
# Create a large data structure # Create a large data structure
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}} large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
key: FullKey = (("graph", "node"), "large_key") key = (("graph", "node"), "large_key")
values = {key: (large_data, None)} values = {key: (large_data, None)}
self.cache.set(values) self.cache.set(values)
+1 -25
View File
@@ -950,8 +950,8 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
assert results[0].key != results[1].key assert results[0].key != results[1].key
ascore = results[0].score ascore = results[0].score
bscore = results[1].score bscore = results[1].score
assert ascore == bscore
assert ascore is not None and bscore is not None assert ascore is not None and bscore is not None
assert ascore == pytest.approx(bscore, abs=1e-5)
results = await store.asearch(("test",), query="uuu") results = await store.asearch(("test",), query="uuu")
assert len(results) == 2 assert len(results) == 2
@@ -1021,27 +1021,3 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
assert len(results) == 3 assert len(results) == 3
doc5_result = next(r for r in results if r.key == "doc5") doc5_result = next(r for r in results if r.key == "doc5")
assert doc5_result.score is None assert doc5_result.score is None
def test_non_ascii(fake_embeddings: CharacterEmbeddings) -> None:
"""Test support for non-ascii characters"""
store = InMemoryStore(
index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
)
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(("user_123", "memories"), "2", {"text": "これは日本語です"}) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
+543 -552
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -4,9 +4,8 @@
# TESTING AND COVERAGE # TESTING AND COVERAGE
###################### ######################
TEST?= "tests/unit_tests"
test: test:
uv run pytest $(TEST) uv run pytest tests/unit_tests
test-integration: test-integration:
uv run pytest tests/integration_tests uv run pytest tests/integration_tests
+7
View File
@@ -1,3 +1,10 @@
OPENAI_API_KEY=placeholder OPENAI_API_KEY=placeholder
ANTHROPIC_API_KEY=placeholder ANTHROPIC_API_KEY=placeholder
TAVILY_API_KEY=placeholder TAVILY_API_KEY=placeholder
LANGCHAIN_TRACING_V2=false
LANGCHAIN_ENDPOINT=placeholder
LANGCHAIN_API_KEY=placeholder
LANGCHAIN_PROJECT=placeholder
LANGGRAPH_AUTH_TYPE=noop
LANGSMITH_AUTH_ENDPOINT=placeholder
LANGSMITH_TENANT_ID=placeholder
@@ -1,89 +0,0 @@
from collections.abc import Sequence
from typing import Annotated, Literal, TypedDict
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
tools = [TavilySearchResults(max_results=1)]
model_oai = ChatOpenAI(temperature=0)
model_oai = model_oai.bind_tools(tools)
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
# Define the function that determines whether to continue or not
def should_continue(state):
messages = state["messages"]
last_message = messages[-1]
# If there are no tool calls, then we finish
if not last_message.tool_calls:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
# Define the function that calls the model
def call_model(state, config):
model = model_oai
messages = state["messages"]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph
workflow = StateGraph(AgentState, context_schema=ContextSchema)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END,
},
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("action", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
graph = workflow.compile()
@@ -1,9 +0,0 @@
[project]
name = "graph-prerelease-reqs-additional-deps"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langgraph==0.6.0"
]
@@ -1,9 +0,0 @@
[project]
name = "graph-prerelease-reqs-zuper-deps"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==0.3.0"
]
@@ -1,13 +0,0 @@
{
"python_version": "3.12",
"dependencies": [
".",
"./deps/additional_deps",
"./deps/zuper_deps"
],
"graphs": {
"agent": "./agent.py:graph"
},
"env": "../.env"
}
@@ -1,14 +0,0 @@
[project]
name = "graph-prerelease-reqs"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.0.0a2",
"langgraph==1.0.0a2",
"langchain_community>=0.3.0",
]
[tool.uv]
prerelease = "allow"
@@ -1,89 +0,0 @@
from collections.abc import Sequence
from typing import Annotated, Literal, TypedDict
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
tools = [TavilySearchResults(max_results=1)]
model_oai = ChatOpenAI(temperature=0)
model_oai = model_oai.bind_tools(tools)
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
# Define the function that determines whether to continue or not
def should_continue(state):
messages = state["messages"]
last_message = messages[-1]
# If there are no tool calls, then we finish
if not last_message.tool_calls:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
# Define the function that calls the model
def call_model(state, config):
model = model_oai
messages = state["messages"]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph
workflow = StateGraph(AgentState, context_schema=ContextSchema)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END,
},
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("action", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
graph = workflow.compile()
@@ -1,11 +0,0 @@
{
"python_version": "3.12",
"dependencies": [
"."
],
"graphs": {
"agent": "./agent.py:graph"
},
"env": "../.env"
}
@@ -1,11 +0,0 @@
[project]
name = "graph-prerelease-reqs"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.0.0a2",
"langgraph==1.0.0a2",
"langchain_community>=0.3.0",
]
+7 -8
View File
@@ -7,7 +7,6 @@ from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
tools = [TavilySearchResults(max_results=1)] tools = [TavilySearchResults(max_results=1)]
@@ -18,10 +17,6 @@ model_anth = model_anth.bind_tools(tools)
model_oai = model_oai.bind_tools(tools) model_oai = model_oai.bind_tools(tools)
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict): class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages] messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -39,8 +34,8 @@ def should_continue(state):
# Define the function that calls the model # Define the function that calls the model
def call_model(state, runtime: Runtime[AgentContext]): def call_model(state, config):
if runtime.context.get("model", "anthropic") == "anthropic": if config["configurable"].get("model", "anthropic") == "anthropic":
model = model_anth model = model_anth
else: else:
model = model_oai model = model_oai
@@ -54,8 +49,12 @@ def call_model(state, runtime: Runtime[AgentContext]):
tool_node = ToolNode(tools) tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph # Define a new graph
workflow = StateGraph(AgentState, context_schema=AgentContext) workflow = StateGraph(AgentState, context_schema=ContextSchema)
# Define the two nodes we will cycle between # Define the two nodes we will cycle between
workflow.add_node("agent", call_model) workflow.add_node("agent", call_model)
-1
View File
@@ -1,5 +1,4 @@
{ {
"$schema": "https://langgra.ph/schema.json",
"python_version": "3.12", "python_version": "3.12",
"dependencies": [ "dependencies": [
"langchain_community", "langchain_community",
@@ -1,6 +1,6 @@
from collections.abc import Sequence from collections.abc import Sequence
from pathlib import Path from pathlib import Path
from typing import Annotated, Literal, TypedDict from typing import Annotated, TypedDict
from langchain_anthropic import ChatAnthropic from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults from langchain_community.tools.tavily_search import TavilySearchResults
@@ -8,7 +8,6 @@ from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
tools = [TavilySearchResults(max_results=1)] tools = [TavilySearchResults(max_results=1)]
@@ -22,10 +21,6 @@ prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
subprompt = open(Path(__file__).parent / "subprompt.txt").read() subprompt = open(Path(__file__).parent / "subprompt.txt").read()
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict): class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages] messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -43,8 +38,8 @@ def should_continue(state):
# Define the function that calls the model # Define the function that calls the model
def call_model(state, runtime: Runtime[AgentContext]): def call_model(state, config):
if runtime.context.get("model", "anthropic") == "anthropic": if config["configurable"].get("model", "anthropic") == "anthropic":
model = model_anth model = model_anth
else: else:
model = model_oai model = model_oai
@@ -57,8 +52,9 @@ def call_model(state, runtime: Runtime[AgentContext]):
# Define the function to execute tools # Define the function to execute tools
tool_node = ToolNode(tools) tool_node = ToolNode(tools)
# Define a new graph # Define a new graph
workflow = StateGraph(AgentState, context_schema=AgentContext) workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between # Define the two nodes we will cycle between
workflow.add_node("agent", call_model) workflow.add_node("agent", call_model)
@@ -1,5 +1,4 @@
{ {
"$schema": "https://langgra.ph/schema.json",
"dependencies": [ "dependencies": [
"." "."
], ],
@@ -1,6 +1,6 @@
from collections.abc import Sequence from collections.abc import Sequence
from pathlib import Path from pathlib import Path
from typing import Annotated, Literal, TypedDict from typing import Annotated, TypedDict
from langchain_anthropic import ChatAnthropic from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults from langchain_community.tools.tavily_search import TavilySearchResults
@@ -8,7 +8,6 @@ from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
tools = [TavilySearchResults(max_results=1)] tools = [TavilySearchResults(max_results=1)]
@@ -22,10 +21,6 @@ prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
subprompt = open(Path(__file__).parent / "subprompt.txt").read() subprompt = open(Path(__file__).parent / "subprompt.txt").read()
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict): class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages] messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -43,8 +38,8 @@ def should_continue(state):
# Define the function that calls the model # Define the function that calls the model
def call_model(state, runtime: Runtime[AgentContext]): def call_model(state, config):
if runtime.context.get("model", "anthropic") == "anthropic": if config["configurable"].get("model", "anthropic") == "anthropic":
model = model_anth model = model_anth
else: else:
model = model_oai model = model_oai
@@ -59,7 +54,7 @@ tool_node = ToolNode(tools)
# Define a new graph # Define a new graph
workflow = StateGraph(AgentState, context_schema=AgentContext) workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between # Define the two nodes we will cycle between
workflow.add_node("agent", call_model) workflow.add_node("agent", call_model)
@@ -1,5 +1,4 @@
{ {
"$schema": "https://langgra.ph/schema.json",
"dependencies": [ "dependencies": [
"." "."
], ],
+8 -1
View File
@@ -163,7 +163,14 @@ def generate_schema():
# Add enum constraint for python_version # Add enum constraint for python_version
if "python_version" in python_schema["properties"]: if "python_version" in python_schema["properties"]:
python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12", "3.13"] python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12"]
# Add enum constraint for image_distro
if "image_distro" in python_schema["properties"]:
python_schema["properties"]["image_distro"]["anyOf"] = [
{"type": "string", "enum": ["debian", "wolfi"]},
{"type": "null"},
]
# Create Node.js schema with node_version # Create Node.js schema with node_version
node_schema = { node_schema = {
-1
View File
@@ -1,5 +1,4 @@
{ {
"$schema": "https://langgra.ph/schema.json",
"node_version": "20", "node_version": "20",
"graphs": { "graphs": {
"agent": "./src/agent/graph.ts:graph" "agent": "./src/agent/graph.ts:graph"
@@ -1,62 +0,0 @@
module.exports = {
extends: [
"eslint:recommended",
"prettier",
"plugin:@typescript-eslint/recommended",
],
parserOptions: {
ecmaVersion: 12,
parser: "@typescript-eslint/parser",
project: "./tsconfig.json",
sourceType: "module",
},
plugins: ["import", "@typescript-eslint", "no-instanceof"],
ignorePatterns: [
".eslintrc.cjs",
"scripts",
"src/utils/lodash/*",
"node_modules",
"dist",
"dist-cjs",
"*.js",
"*.cjs",
"*.d.ts",
],
rules: {
"no-process-env": 2,
"no-instanceof/no-instanceof": 2,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-shadow": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-use-before-define": ["error", "nofunc"],
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
camelcase: 0,
"class-methods-use-this": 0,
"import/extensions": [2, "ignorePackages"],
"import/no-extraneous-dependencies": [
"error",
{ devDependencies: ["**/*.test.ts"] },
],
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
"keyword-spacing": "error",
"max-classes-per-file": 0,
"max-len": 0,
"no-await-in-loop": 0,
"no-bitwise": 0,
"no-console": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-continue": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-useless-constructor": 0,
"no-return-await": 0,
"consistent-return": 0,
"no-else-return": 0,
"new-cap": ["error", { properties: false, capIsNew: false }],
},
};
@@ -1,7 +0,0 @@
{
"node_version": "20",
"graphs": {
"agent": "./src/graph.ts:graph"
},
"env": "../../.env"
}
@@ -1,18 +0,0 @@
{
"name": "@js-monorepo-example/agent",
"version": "0.0.1",
"type": "module",
"main": "src/graph.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist"
},
"dependencies": {
"@js-monorepo-example/shared": "*",
"@langchain/core": "^0.3.2",
"@langchain/langgraph": "^0.2.5"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
@@ -1,47 +0,0 @@
/**
* Simple LangGraph.js example for monorepo testing
*/
import { StateGraph } from "@langchain/langgraph";
import { RunnableConfig } from "@langchain/core/runnables";
import { StateAnnotation } from "./state.js";
import { getGreeting } from "@js-monorepo-example/shared";
/**
* Simple node that uses the shared library
*/
const callModel = async (
state: typeof StateAnnotation.State,
_config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> => {
// Use functions from the shared library
const greeting = getGreeting();
return {
messages: [
{
role: "assistant",
content: `${greeting}`,
},
],
};
};
/**
* Simple routing function
*/
export const route = (
state: typeof StateAnnotation.State,
): "__end__" | "callModel" => {
if (state.messages.length > 0) {
return "__end__";
}
return "callModel";
};
// Create the graph
const builder = new StateGraph(StateAnnotation)
.addNode("callModel", callModel)
.addEdge("__start__", "callModel")
.addConditionalEdges("callModel", route);
export const graph = builder.compile();
@@ -1,15 +0,0 @@
import { BaseMessage, BaseMessageLike } from "@langchain/core/messages";
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
/**
* Simple state annotation for the agent
*/
export const StateAnnotation = Annotation.Root({
/**
* Messages track the primary execution state of the agent.
*/
messages: Annotation<BaseMessage[], BaseMessageLike[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
@@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -1,14 +0,0 @@
{
"name": "@js-monorepo-example/shared",
"version": "0.0.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
@@ -1,6 +0,0 @@
/**
* Simple utility functions for monorepo testing
*/
export function getGreeting(): string {
return "Hello from shared library!";
}
@@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
-34
View File
@@ -1,34 +0,0 @@
{
"name": "js-monorepo-example",
"version": "0.0.1",
"packageManager": "yarn@1.22.22",
"description": "A simple monorepo example for LangGraph integration testing.",
"private": true,
"workspaces": [
"libs/*",
"apps/*"
],
"type": "module",
"scripts": {
"build": "turbo build",
"clean": "turbo clean",
"test": "turbo test",
"format": "prettier --write .",
"lint": "eslint 'apps/**/*.ts' 'libs/**/*.ts'"
},
"devDependencies": {
"turbo": "^2.5.0",
"typescript": "^5.3.3",
"@tsconfig/recommended": "^1.0.7",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.9.1",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^4.2.1",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"prettier": "^3.3.3"
}
}
@@ -1,16 +0,0 @@
{
"extends": "@tsconfig/recommended",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"declaration": true,
"outDir": "./dist"
},
"include": ["apps/**/*", "libs/**/*"],
"exclude": ["node_modules", "dist"]
}
-15
View File
@@ -1,15 +0,0 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"clean": {
"dependsOn": ["^clean"]
},
"test": {
"dependsOn": ["^test"]
}
}
}

Some files were not shown because too many files have changed in this diff Show More