Compare commits

...
Author SHA1 Message Date
Tat Dat Duong 5b45bbc1ba feat(sdk): bump to 0.0.46 2025-02-25 18:28:22 +01:00
David DuongandGitHub edf707be51 feat(react): support interrupt_before/after (#3582) 2025-02-25 18:06:25 +01:00
Tat Dat Duong d3b9a96504 feat(react): support interrupt_before/after 2025-02-25 17:57:12 +01:00
David DuongandGitHub 3257e5ae76 fix(docs): remove @langchain/langgraph/web import in branching example (#3580) 2025-02-25 16:18:09 +01:00
Tat Dat Duong bedd0eb286 fix(docs): remove @langchain/langgraph/web import in branching example 2025-02-25 16:11:09 +01:00
David DuongandGitHub 42f0c351fd fix(react): avoid implicitly streaming values if not needed (#3579) 2025-02-25 16:07:41 +01:00
Tat Dat Duong a290984362 fix(react): avoid implicitly streaming values if not needed 2025-02-25 15:58:21 +01:00
David DuongandGitHub f1d6fd184f fix(docs): typo for npm install command (#3578) 2025-02-25 15:53:49 +01:00
Tat Dat Duong 503f716104 fix(docs): typo for npm install command 2025-02-25 15:49:50 +01:00
Nuno Campos 515c34d1ce Fix docs build 2025-02-24 17:14:42 -08:00
Nuno CamposandGitHub 5ea0d49d4d Add docs page on lgp scalability / resilience (#3510) 2025-02-24 16:52:37 -08:00
Andrew NguonlyandGitHub d9f71ef8b3 docs: Add section for Add or Remove GitHub Repositories (#3571) 2025-02-24 16:12:03 -08:00
Eugene YurtsevandGitHub 8658a5dc0b docs: add pregel conceptual doc (#3516)
* Update API Reference for Pregel
* Add conceptual page for Pregel
* The content for the two is very similar at the moment (i.e.,
duplicated content). This is usually a bad sign, but in this case I'm OK
duplicating information along both paths since the underlying algorithm
sets us apart from other implementations.
2025-02-24 17:48:03 -05:00
William FHandGitHub 2afee13d9e Docs on custom routes (#3568) 2025-02-24 11:21:41 -08:00
HackHuangandGitHub f7d9daa4eb docs(multi_agent.md) : Fix some code snippets (#3565)
Hey buddy! You forgot to import the `Command` in some code snippets.
2025-02-24 13:28:56 -05:00
HackHuangandGitHub fb28aa6d4b docs(concepts) : update human_in_the_loop.md (#3558)
Fix the false output result.
2025-02-24 04:52:48 +00:00
jessicaouandGitHub 3488945cdf Update adopters.md to include Klarna (#3560) 2025-02-24 04:39:25 +00:00
Sudar Selva Ganesh MandGitHub 57e8081921 chore(docs): make webhooks platform doc more readable (#3551)
1. Provided additional context on webhook usage and setup.
2. Structured supported endpoints into a table for better readability.
2025-02-23 00:17:21 +13:00
Nino RisteskiandGitHub 078b335448 chore(checkpoint): fix typos in README (#3553) 2025-02-22 11:10:09 +00:00
Vadym BardaandGitHub 39b2bb9c8f update opendeepresearch name (#3541) 2025-02-20 18:16:40 -05:00
5eb793d7d8 Update packages w/ Open Deep Research (#3539)
Here: 
https://github.com/langchain-ai/open_deep_research

---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
2025-02-20 15:04:11 -08:00
David DuongandGitHub 3dac1894cc fix(react): handle non-concatenable messages (#3536) 2025-02-20 21:28:02 +01:00
Tat Dat Duong 1500764b46 fix(react): handle non-concatenable messages 2025-02-20 21:24:00 +01:00
Vadym BardaandGitHub fbb89325f9 langgraph: allow passing config_schema to create_react_agent (#3534) 2025-02-20 19:36:17 +00:00
David DuongandGitHub 187c71a812 feat(react): add interrupt docs (#3533) 2025-02-20 20:10:35 +01:00
Tat Dat Duong b09b33070e feat(docs): add interrupt docs 2025-02-20 20:03:52 +01:00
Vadym BardaandGitHub 31a7bcf750 langgraph: handle non-overlapping subgraph updates in Command.PARENT (#3521) 2025-02-20 13:15:27 -05:00
langchain-infraandGitHub 1a12b0309c docs: add langgraph platform ips (#3528) 2025-02-19 22:52:02 -05:00
infra 660c15d072 docs: add langgraph platform ips 2025-02-19 22:39:54 -05:00
langchain-infraandGitHub 4a59da7cfd docs: add langgraph platform ips (#3527) 2025-02-19 22:27:38 -05:00
infra aacc079eed fmt 2025-02-19 22:23:42 -05:00
langchain-infraandGitHub 3d70a4ed65 Delete libs/cli/langgraph_cli/docker-compose.yaml 2025-02-19 22:16:10 -05:00
infra caad15f7ae docs: add langgraph platform ips 2025-02-19 22:14:29 -05:00
infra 1f4d4e7bfd docs: add langgraph platform ips 2025-02-19 22:14:09 -05:00
David DuongandGitHub 5b0bf861ac feat(react): add interrupts, clean up generic types (#3526) 2025-02-20 03:17:48 +01:00
Tat Dat Duong a69ea47ac2 Bump to 0.0.44 2025-02-20 03:06:11 +01:00
Tat Dat Duong f83d18188f feat(react): add interrupts, clean up generic types 2025-02-20 03:06:10 +01:00
David DuongandGitHub 688efdea3d fix(react): avoid streaming messages if they are not needed (#3525) 2025-02-20 03:05:46 +01:00
David DuongandGitHub 6641dcd3c9 fix(react): output non-abort errors in console, handle bogus message type (#3524) 2025-02-20 02:45:18 +01:00
William FHandGitHub ad14d92f5e [cli] Bump api floor (#3523) 2025-02-20 00:26:15 +00:00
Tat Dat Duong c63fbbfaa6 fix(react): avoid streaming messages if they are not needed 2025-02-20 01:22:08 +01:00
Tat Dat Duong 577b4413a9 fix(react): output non-abort errors in console, handle bogus message type 2025-02-20 01:20:19 +01:00
William FHandGitHub 277c341817 Configure loopback transports (#3522) 2025-02-19 15:58:57 -08:00
mathislindnerandGitHub 88d0f41c55 docs: Update map-reduce-fixed-typo.ipynb (#3509)
fixed a typo:
We will use this an edge in the graph
to 
We will use this as an edge in the graph
2025-02-19 17:28:21 -05:00
Eugene YurtsevandGitHub 0a4953c4bc Update index.md (#3519) 2025-02-19 21:52:17 +00:00
Eugene YurtsevandGitHub 059e16789c docs: fix anchor links (#3518) 2025-02-19 16:48:41 -05:00
Nuno Campos f613fdfcbc Add sections on postgres and redis 2025-02-19 11:25:56 -08:00
David DuongandGitHub 41b36dcf3f docs: playground studio integration (#3514) 2025-02-19 19:49:37 +01:00
Arjun Natarajan b39dcd7fad pr feedback 2025-02-19 10:43:02 -08:00
Arjun Natarajan c0ad92b6db update title 2025-02-19 10:20:13 -08:00
Eugene YurtsevandGitHub b8d25bc0ed docs build: test api link generation (#3513) 2025-02-19 13:15:53 -05:00
Arjun Natarajan 7884401ec8 docs for playground studio integration 2025-02-19 09:43:21 -08:00
David DuongandGitHub a3bc029344 feat(sdk-js/react): make configurable typed via generics (#3511) 2025-02-19 18:19:51 +01:00
William FHandGitHub 4875973ac5 [CLI] Support http config (#3505)
Right now requires that the file be in one of the local
packages/dependencies.
2025-02-19 08:52:00 -08:00
Tat Dat Duong 15e67fdd57 feat(sdk-js/react): make configurable typed 2025-02-19 17:27:26 +01:00
Nuno Campos 265466184c Add docs page on lgp scalability / resilience 2025-02-19 08:24:36 -08:00
Eugene YurtsevandGitHub 1393270664 concepts: add durable execution to nav (#3508) 2025-02-19 16:04:55 +00:00
Nuno CamposandGitHub 6e0295b4de feat(ci): only run lint/test if files have changed (#3507) 2025-02-19 08:04:42 -08:00
Tat Dat Duong 4aadfccf95 feat(ci): only run lint/test if files have changed 2025-02-19 16:38:35 +01:00
David DuongandGitHub 70b2da1301 fix(docs): add install command for sdk-js/react (#3506) 2025-02-19 16:33:47 +01:00
YkohandGitHub 209864da45 docs: Remove unused imports (#3500)
This PR removes the unused imports Literal and TypedDict from the typing
module.

These imports were not referenced in the code.

```python
from typing import Literal, TypedDict
```
2025-02-19 15:33:13 +00:00
Tat Dat Duong 8b6ef35f0c fix(docs): add install command for sdk-js/react 2025-02-19 16:24:34 +01:00
Eugene YurtsevandGitHub a01537d1a5 docs: concepts durable execution (#3355)
Conceptual page for durable execution
2025-02-19 10:15:26 -05:00
Vadym BardaandGitHub 05b4a30c04 langgraph: optionally add structured_response key to agent state in create_react_agent (#3493) 2025-02-19 15:06:46 +00:00
David DuongandGitHub 93e10fbe15 fix(sdk-js): mark ui-related peer deps as optional (#3503) 2025-02-19 15:53:45 +01:00
Tat Dat Duong f6989f2c7d Bump to 0.0.43 2025-02-19 15:44:03 +01:00
Tat Dat Duong c4f8346479 fix(sdk-js): mark ui-related peer deps as optional 2025-02-19 15:43:36 +01:00
William FHandGitHub 82c9d4b368 Update docstrings for command & send (#3492) 2025-02-19 05:28:45 -08:00
David DuongandGitHub 647f22fdd9 fix(sdk-js): add docs to gitignore path (#3498) 2025-02-19 09:11:19 +01:00
Tat Dat Duong 0aba1b4887 fix(sdk-js): add docs to gitignore path 2025-02-19 09:01:43 +01:00
Eugene YurtsevandGitHub 4c0c52d996 docs: revert changes to api reference generation (#3494)
* Temporarily revert. Need to add unit tests tomorrow and can then
restore
2025-02-19 02:01:03 +00:00
Andrew NguonlyandGitHub 580fe68c8e docs: Update note about LangGraph Deployments view in self-hosted deployment options (#3491) 2025-02-18 17:55:57 -08:00
YkohandGitHub 2f26268ff4 Fix issues in chatbot simulation evaluation tutorial (#3462)
## Description
While following the LangChain tutorial on [chatbot simulation
evaluation](https://langchain-ai.github.io/langgraph/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation/),
I encountered some issues and made the following fixes to ensure proper
functionality:

1. Updated deprecated `run_on_dataset` to `evaluate` to resolve
PydanticUserError
- Following the migration guide:
https://docs.smith.langchain.com/old/evaluation/migration

2. Added missing `langchain_community` import

3. Added required `simulation_utils.py` file to docs directory
- Source:
https://github.com/langchain-ai/langgraph/blob/main/examples/chatbot-simulation-evaluation/simulation_utils.py

## Testing
Successfully ran the `langsmith-agent-simulation-evaluation.ipynb`
notebook without any errors.
2025-02-19 00:26:50 +00:00
Eugene YurtsevandGitHub 84f8a43f6e docs: update measurement id (#3487) 2025-02-19 00:11:34 +00:00
Vadym BardaandGitHub 7ce0e3e15e langgraph: release 0.2.74 (#3489) 2025-02-18 18:56:04 -05:00
Vadym BardaandGitHub 1e6d958434 langgraph: fallback on no-op writer in get_stream_writer (#3488) 2025-02-18 23:53:11 +00:00
Vadym BardaandGitHub 68ba8aa393 docs: add mcp adapters (#3485) 2025-02-18 17:47:55 -05:00
Eugene YurtsevandGitHub e6a0f08561 docs: update prebuilt stub (#3486) 2025-02-18 17:09:33 -05:00
Eugene YurtsevandGitHub f38784a291 ci: remove markddown-exec from docs pipeline (#3482)
This PR removes the following changes:
* notebooks that were converted to markdown
* mkdocs.yml file to reference the ipython notebooks rather than the
markdown files
* Makefile install vercel reverted
* hooks for markdown-exec
* notebook conversion jinja2 templates (for converting notebooks to
markdown exec format)
2025-02-18 16:16:40 -05:00
Nuno CamposandGitHub fc834127fd checkpoint: Fix memory leak in checkpoint serializer (#3481)
- Packer instances can't be kept in memory as they retain memory after
used
2025-02-18 11:56:15 -08:00
Nuno Campos f3a0cbf294 Format 2025-02-18 11:46:48 -08:00
Nuno Campos f4fec76257 checkpoint: Fix memory leak in checkpoint serializer
- Packer instances can't be kept in memory as they retain memory after used
2025-02-18 11:44:26 -08:00
Eugene YurtsevandGitHub 457641f15e docs: remove beta from functional api (#3475) 2025-02-18 13:33:47 -05:00
Eugene YurtsevandGitHub 9f15e15e26 reference: remove incorrect example from pregel class (#3476)
Remove incorrect example from Pregel class. Will follow later this week
with better docs
2025-02-18 13:33:15 -05:00
Yan ZhaoandGitHub cb9989030e Very minor typo in docstring of State -> add_node (#3461)
Hi, I am a student and was going through the tutorial. While trying to
understand the different components by reading the docstring found this
super minor typo 😄 . I hope to contribute more meaningful changes in
future 😸
2025-02-18 13:05:26 -05:00
a008725c06 Ensure remote respects recursion_limit param if it's passed (#3470)
Currently when using RemoteGraph the recursion_limit cannot be set, due
to the sanitize_config.

---------

Co-authored-by: Simon Moxon <simon@together.ly>
Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
2025-02-18 17:49:31 +00:00
a93f17e624 langgraph(prebuilt): allow a PromptTemplate in react agent (#3463)
Currently a ChatPromptTemplate cannot be used as a `prompt` for
`create_react_agent` without complaints from type checkers, although it
is supported by `model` as input.

Add the missing types to remove the warning.

---------

Co-authored-by: vbarda <vadym@langchain.dev>
2025-02-18 17:39:46 +00:00
William FHandGitHub c7eddcc6e3 Add langmem link (#3473) 2025-02-18 08:15:09 -08:00
David DuongandGitHub 4cdad6c206 fix(cli): use '{{json .}}' format instead of 'json' (#3479)
Related to https://github.com/langchain-ai/langgraph/issues/1319
2025-02-18 16:01:38 +01:00
Tat Dat Duong b9fb155d59 Bump to 0.1.52 2025-02-18 15:51:57 +01:00
Tat Dat Duong 8b817a5b16 Fix lint 2025-02-18 15:51:05 +01:00
Tat Dat Duong 3a67f3a3eb fix(cli): use '{{json .}}' format instead of 'json'
Related to https://github.com/langchain-ai/langgraph/issues/1319
2025-02-18 14:58:24 +01:00
Nuno CamposandGitHub 1283539500 Add CONFIG_KEY_RUNNER_SUBMIT (#3474) 2025-02-17 17:42:35 -08:00
Nuno Campos 69ad42cac5 Add CONFIG_KEY_RUNNER_SUBMIT 2025-02-17 17:33:38 -08:00
Nuno CamposandGitHub 264b02e3ad cli: Add support for dependencies in parent directories (#3472)
- Now supporting local dependencies in directories that are not
contained in the docker context (ie. outside the folder containing
langgraph.json)
- This is achieved by passing each parent directorty as an additional
context to docker build
- This makes it a lot easier to build projects contained in monorepos
where you need to include some sibling/parent folder as a dependency
- Also include additional comments in the generated dockerfile to
delimit each section
2025-02-17 17:21:41 -08:00
Nuno Campos b5c659bc9f Resolve 2025-02-17 17:12:06 -08:00
Nuno Campos 4623f7b5da Fix 2025-02-17 17:09:31 -08:00
William Fu-Hinthorn 1641402341 Add sibling dep test 2025-02-17 14:59:13 -08:00
Nuno Campos d03ead2f43 Fix path in assertion 2025-02-17 11:47:38 -08:00
Nuno Campos 5a8624fdfd cli: Add support for dependencies in parent directories
- Now supporting local dependencies in directories that are not contained in the docker context (ie. outside the folder containing langgraph.json)
- This is achieved by passing each parent directorty as an additional context to docker build
- This makes it a lot easier to build projects contained in monorepos where you need to include some sibling/parent folder as a dependency
- Also include additional comments in the generated dockerfile to delimit each section
2025-02-17 11:43:55 -08:00
David DuongandGitHub c44ec55095 fix(docs): broken assistant-ui link (#3467) 2025-02-17 04:50:11 +01:00
Tat Dat Duong 25d682cc9e fix(docs): broken assistant-ui link 2025-02-17 04:38:22 +01:00
Vadym BardaandGitHub 6f37330141 langgraph: release 0.2.73 (#3456) 2025-02-15 16:13:30 -05:00
Vadym BardaandGitHub 1356a0ba42 langgraph: better typing for node functions (#3455) 2025-02-15 21:11:33 +00:00
Nuno Campos 9786be1ff7 Update requirement 2025-02-14 19:01:50 -08:00
94 changed files with 4942 additions and 2544 deletions
+38
View File
@@ -20,7 +20,30 @@ env:
POETRY_VERSION: "1.7.1"
jobs:
changes:
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
python:
- 'libs/langgraph/**'
- 'libs/sdk-py/**'
- 'libs/cli/**'
- 'libs/checkpoint/**'
- 'libs/checkpoint-sqlite/**'
- 'libs/checkpoint-postgres/**'
- 'libs/scheduler-kafka/**'
sdk-js:
- 'libs/sdk-js/**'
lint:
needs: changes
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
@@ -34,12 +57,14 @@ jobs:
"libs/checkpoint-postgres",
"libs/scheduler-kafka",
]
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_lint.yml
with:
working-directory: ${{ matrix.working-directory }}
secrets: inherit
test:
needs: changes
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
@@ -50,6 +75,7 @@ jobs:
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
]
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_test.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -57,17 +83,23 @@ jobs:
# NOTE: we're testing langgraph separately because it requires a different matrix
test-langgraph:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/langgraph"
uses: ./.github/workflows/_test_langgraph.yml
secrets: inherit
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
test-scheduler-kafka:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/scheduler-kafka"
uses: ./.github/workflows/_test_scheduler_kafka.yml
secrets: inherit
check-sdk-methods:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
@@ -80,11 +112,15 @@ jobs:
run: python .github/scripts/check_sdk_methods.py
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true'
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
lint-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
@@ -109,6 +145,8 @@ jobs:
run: yarn build
test-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
+2
View File
@@ -117,6 +117,7 @@ jobs:
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://academy\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://twitter.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "http://localhost:2024.*" \
@@ -143,6 +144,7 @@ jobs:
--check-links-ignore "http://localhost:2024.*" \
--check-links-ignore "http://127.0.0.1:.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://twitter.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links ${CHANGED_FILES} \
+1 -7
View File
@@ -26,15 +26,9 @@ install-vercel-deps:
# don't use vercel's python - it wasn't compiled with sqlite support, and it fails when installing ipython's kernel
poetry env use /usr/bin/python3.11
poetry install --with docs --with test --no-root
poetry run pip install "git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
poetry run python3 -m ipykernel install --name=python3
npm install -g tslab
poetry run tslab install --python=python3
poetry run jupyter kernelspec list
tests:
# RUn unit tests
# Run unit tests
poetry run pytest tests/unit_tests
@@ -1,75 +0,0 @@
import nock, { Definition } from "nock";
import msgpack from "msgpack-lite";
import zlib from "node:zlib";
import fs from "node:fs/promises";
import { Buffer } from "node:buffer";
// deno style imports here because we're running this in the deno jupyter kernel
interface NockCassetteData {
hash: string;
entries: Definition[];
}
// Utility functions for compression & serialization
function compressData(data: NockCassetteData, compressionLevel = 9): string {
const packed = msgpack.encode(data);
const compressed = zlib.deflateSync(packed, { level: compressionLevel });
return compressed.toString("base64");
}
function decompressData(compressedString: string): NockCassetteData {
const decoded = Buffer.from(compressedString, "base64");
const decompressed = zlib.inflateSync(decoded);
return msgpack.decode(decompressed) as NockCassetteData;
}
// deno-lint-ignore no-unused-vars
class HashedCassette {
private recording = true;
constructor(
private readonly cassettePath: string,
private readonly hash: string
) {}
async enter() {
try {
const rawCassette = await fs.readFile(this.cassettePath, "utf-8");
const data = decompressData(rawCassette);
if (data.hash === this.hash) {
this.recording = false;
nock.disableNetConnect();
nock.define(data.entries);
return;
}
} catch (error) {
if (error instanceof Error && error.message.includes("ENOENT")) {
this.recording = true;
} else {
throw error;
}
}
nock.recorder.rec({
dont_print: true,
output_objects: true,
});
}
async exit() {
if (this.recording) {
const entries = nock.recorder.play() as Definition[];
const data = {
hash: this.hash,
entries,
};
const compressed = compressData(data);
await fs.writeFile(this.cassettePath, compressed);
} else {
nock.enableNetConnect();
nock.restore();
nock.cleanAll();
}
}
}
-107
View File
@@ -1,107 +0,0 @@
import base64
import os
import zlib
from types import TracebackType
from typing import Optional, Any, Type
import msgpack
import vcr
os.environ.pop("LANGCHAIN_TRACING_V2", None)
custom_vcr = vcr.VCR()
def compress_data(data: Any, compression_level: int = 9) -> str:
packed = msgpack.packb(data, use_bin_type=True)
compressed = zlib.compress(packed, level=compression_level)
return base64.b64encode(compressed).decode("utf-8")
def decompress_data(compressed_string: str) -> Any:
decoded = base64.b64decode(compressed_string)
decompressed = zlib.decompress(decoded)
return msgpack.unpackb(decompressed, raw=False)
class AdvancedCompressedSerializer:
def serialize(self, cassette_dict: Any) -> str:
return compress_data(cassette_dict)
def deserialize(self, cassette_string: str) -> Any:
return decompress_data(cassette_string)
custom_vcr.register_serializer("advanced_compressed", AdvancedCompressedSerializer())
custom_vcr.serializer = "advanced_compressed"
class HashedCassette:
def __init__(self, cassette_path: str, hash_value: str) -> None:
"""A context manager for using VCR cassettes with an embedded hash value.
Args:
cassette_path (str): The file path of the cassette (independent of hash).
hash_value (str): The expected hash value (e.g. a uuid string).
This class provides a context manager for using VCR cassettes with an embedded hash value.
The hash value is used to ensure that the cassette matches the expected state, and if not,
the cassette is removed or updated with the new hash value.
"""
self.cassette_path: str = cassette_path
self.hash_value: str = hash_value
self.vcr: vcr.VCR = custom_vcr
self.cassette_context: Optional[Any] = None
self.exited: bool = False
def __enter__(self) -> Any:
self.exited: bool = False
# Get the serializer instance from the VCR instance.
serializer = self.vcr.serializers[self.vcr.serializer]
# If the cassette file exists, check its embedded hash.
if os.path.exists(self.cassette_path):
with open(self.cassette_path, "r") as f:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception as e:
os.remove(self.cassette_path)
else:
existing_hash = cassette_data.get("cassette_hash")
if existing_hash != self.hash_value:
os.remove(self.cassette_path)
# Now enter the VCR cassette context.
self.cassette_context = custom_vcr.use_cassette(
self.cassette_path,
filter_headers=["x-api-key", "authorization"],
record_mode="once",
serializer="advanced_compressed",
)
return self.cassette_context.__enter__()
def __exit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_val: Optional[BaseException] = None,
exc_tb: Optional[TracebackType] = None,
) -> Optional[bool]:
if self.exited:
return
self.exited = True
# Exit the VCR cassette context.
result = self.cassette_context.__exit__(exc_type, exc_val, exc_tb)
serializer = self.vcr.serializers[self.vcr.serializer]
# If a cassette was recorded (or updated), open and update its hash.
if os.path.exists(self.cassette_path):
with open(self.cassette_path, "r") as f:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception as e:
return result
# Update the cassette data with the expected hash.
if cassette_data.get("cassette_hash") != self.hash_value:
cassette_data["cassette_hash"] = self.hash_value
serialized_data = serializer.serialize(cassette_data)
with open(self.cassette_path, "w") as f:
f.write(serialized_data)
return result
+121 -198
View File
@@ -1,12 +1,9 @@
import ast
import importlib
from importlib.machinery import ModuleSpec
import importlib.util
import inspect
import logging
import re
from functools import lru_cache
import sys
from typing import List, Literal, Optional
from typing import List, Optional
from typing_extensions import TypedDict
@@ -42,7 +39,6 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.graph"], "langgraph.graph.message", "add_messages", "graphs"),
(["langgraph.graph"], "langgraph.graph.state", "StateGraph", "graphs"),
(["langgraph.graph"], "langgraph.graph.state", "CompiledStateGraph", "graphs"),
([], "langgraph.types", "StreamMode", "types"),
(["langgraph.graph"], "langgraph.constants", "START", "constants"),
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
(["langgraph.constants"], "langgraph.types", "Send", "types"),
@@ -51,7 +47,9 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.constants"], "langgraph.types", "Command", "types"),
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
(["langgraph.func"], "langgraph.func", "task", "func"),
([], "langgraph.types", "RetryPolicy", "types"),
(["langgraph.types"], "langgraph.types", "RetryPolicy", "types"),
(["langgraph.types"], "langgraph.types", "StreamMode", "types"),
(["langgraph.types"], "langgraph.types", "StreamWriter", "types"),
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
([], "langgraph.checkpoint.base", "BaseCheckpointSaver", "checkpoints"),
@@ -71,224 +69,149 @@ WELL_KNOWN_LANGGRAPH_OBJECTS = {
}
def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
if not pkg_prefix.isidentifier():
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
return re.compile(
r"from\s+(" + pkg_prefix + r"(?:_\w+)?(?:\.\w+)*?)\s+import\s+\(?"
r"((?:\w+(?:,\s*)?)*)\s*\)?", # Match zero or more words separated by a comma+optional ws
re.DOTALL, # Match newlines as well
)
# Regular expression to match langchain import lines
_IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
@lru_cache(maxsize=10_000)
def _get_full_module_name(
module_path: str, class_name: str | None, doc_title: str
) -> Optional[str]:
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
"""Get full module name using inspect, with LRU cache to memoize results."""
try:
if module_path in sys.modules:
module = sys.modules[module_path]
else:
spec: ModuleSpec | None = importlib.util.find_spec(module_path)
if spec is not None:
module = importlib.util.module_from_spec(spec)
sys.modules[module_path] = module
spec.loader.exec_module(module)
if class_name is not None:
class_ = getattr(module, class_name)
if re.match(r"\w+\s+as\s+\w+", class_name):
# Handle cases like "A as B"
class_name, _ = class_name.split(" as ")
module = inspect.getmodule(class_)
if module is None:
# For constants, inspect.getmodule() might return None
# In this case, we'll return the original module_path
return module_path
return module.__name__
module = importlib.import_module(module_path)
symbol = getattr(module, class_name)
# First check the __module__ attribute on the symbol.
mod_name = getattr(symbol, "__module__", None)
# If __module__ is not set or comes from typing,
# assume the definition is in module_path.
if mod_name is None or mod_name.startswith("typing"):
return module_path
return mod_name
except AttributeError as e:
if class_name is not None:
# the class_name might actually be a module
# e.g. from langchain import hub
# try to import it as a module, and if that doesn't work, throw
if class_name is not None:
module_name = _get_full_module_name(
f"{module_path}.{class_name}", None, doc_title
)
if module_name is not None:
# return the name of the parent module, rather than the name of the class as though it were a module
return module.__name__
logger.warning(
f"API Reference: Could not find module for {class_name} in {module_path}, imported in doc {doc_title}, {e}"
)
# don't log if we're trying to import the "hub" part as though it were a module
logger.warning(
f"API Reference: Could not find module for {module_path}, imported in doc {doc_title}, {e}"
)
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
return None
except ImportError as e:
logger.warning(
f"API Reference: Failed to import module {module_path} {doc_title}, {e}"
)
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
return None
def _get_doc_title(data: str, file_name: str) -> str:
try:
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
except IndexError:
pass
# Parse the rst-style titles
try:
return re.findall(r"^(.*)\n=+\n", data, re.MULTILINE)[0]
except IndexError:
return file_name
class ImportInformation(TypedDict):
imported: str # The name of the class that was imported.
source: str # The full module path from which the class was imported.
docs: str # The URL pointing to the class's documentation.
title: str # The title of the document where the import is used.
path: str # The path of the file where the markdown content originated.
def _get_imports(
code: str, doc_title: str, package_ecosystem: Literal["langchain", "langgraph"]
) -> List[ImportInformation]:
"""Get imports from the given code block.
Args:
code: Python code block from which to extract imports
doc_title: Title of the document
package_ecosystem: "langchain" or "langgraph". The two live in different
repositories and have separate documentation sites.
Returns:
List of import information for the given code block
"""
imports = []
if package_ecosystem == "langchain":
pattern = _IMPORT_LANGCHAIN_RE
elif package_ecosystem == "langgraph":
pattern = _IMPORT_LANGGRAPH_RE
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
for import_match in pattern.finditer(code):
module = import_match.group(1)
if "pydantic_v1" in module:
continue
imports_str = (
import_match.group(2).replace("(\n", "").replace("\n)", "")
) # Handle newlines within parentheses
# remove any newline and spaces, then split by comma
imported_classes = [
imp.strip()
for imp in re.split(r",\s*", imports_str.replace("\n", ""))
if imp.strip()
]
for class_name in imported_classes:
if module == "langchain_core.messages" and class_name == ")":
print("WARNING: ", file=sys.stderr)
print(
f"WARNING: Trying to import {class_name} from {module} in doc {doc_title}",
file=sys.stderr,
)
print("WARNING: ", file=sys.stderr)
print("WARNING:", import_match.group(0), file=sys.stderr)
print("WARNING: ", file=sys.stderr)
print(
"\n".join([f"WARNING: {line}" for line in code.splitlines()]),
file=sys.stderr,
)
print("WARNING: ", file=sys.stderr)
module_path = _get_full_module_name(module, class_name, doc_title)
if not module_path:
continue
if len(module_path.split(".")) < 2:
continue
if package_ecosystem == "langchain":
pkg = module_path.split(".")[0].replace("langchain_", "")
top_level_mod = module_path.split(".")[1]
url = (
_LANGCHAIN_API_REFERENCE
+ pkg
+ "/"
+ top_level_mod
+ "/"
+ module_path
+ "."
+ class_name
+ ".html"
)
elif package_ecosystem == "langgraph":
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
# Likely not documented yet
continue
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
(module, class_name)
]
url = (
_LANGGRAPH_API_REFERENCE
+ namespace
+ "/#"
+ source_module
+ "."
+ class_name
)
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
# Add the import information to our list
imports.append(
{
"imported": class_name,
"source": module,
"docs": url,
"title": doc_title,
}
)
return imports
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
def get_imports(code: str, path: str) -> List[ImportInformation]:
"""Retrieve all import references from the given code for specified ecosystems.
Args:
code: The source code from which to extract import references.
doc_title: The documentation title associated with the code.
path: The path of the file where the markdown content originated.
Returns:
A list of import information for each import found.
"""
ecosystems = ["langchain", "langgraph"]
all_imports = []
for package_ecosystem in ecosystems:
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
return all_imports
# Parse the code into an AST.
try:
tree = ast.parse(code)
except SyntaxError:
return []
found_imports = []
# Walk through the AST and process ImportFrom nodes.
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
# node.module is the source module.
if node.module is None:
continue
for alias in node.names:
if not (
node.module.startswith("langchain")
or node.module.startswith("langgraph")
):
continue
found_imports.append(
{
"source": node.module,
# alias.name is the original name even if an alias exists.
"imported": alias.name,
}
)
imports: list[ImportInformation] = []
for found_import in found_imports:
module = found_import["source"]
if module.startswith("langchain"):
# Handles things like `langchain` or `langchain_anthropic`
package_ecosystem = "langchain"
elif module.startswith("langgraph"):
package_ecosystem = "langgraph"
else:
continue
class_name = found_import["imported"]
module_path = _get_full_module_name(module, class_name)
if not module_path:
continue
if len(module_path.split(".")) < 2:
continue
if package_ecosystem == "langchain":
pkg = module_path.split(".")[0].replace("langchain_", "")
top_level_mod = module_path.split(".")[1]
url = (
_LANGCHAIN_API_REFERENCE
+ pkg
+ "/"
+ top_level_mod
+ "/"
+ module_path
+ "."
+ class_name
+ ".html"
)
elif package_ecosystem == "langgraph":
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
# Likely not documented yet
continue
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
(module, class_name)
]
url = (
_LANGGRAPH_API_REFERENCE
+ namespace
+ "/#"
+ source_module
+ "."
+ class_name
)
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
# Add the import information to our list
imports.append(
{
"imported": class_name,
"source": module,
"docs": url,
"path": path,
}
)
return imports
def update_markdown_with_imports(markdown: str, file_name: str) -> str:
def update_markdown_with_imports(markdown: str, path: str) -> str:
"""Update markdown to include API reference links for imports in Python code blocks.
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
This function scans the markdown content for Python code blocks, extracts any
imports, and appends links to their API documentation.
Args:
markdown: The markdown content to process.
path: The path of the file where the markdown content originated.
Returns:
Updated markdown with API reference links appended to Python code blocks.
@@ -299,7 +222,8 @@ def update_markdown_with_imports(markdown: str, file_name: str) -> str:
```python
from langchain.nlp import TextGenerator
```
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
This function will append an API reference link to the `TextGenerator` class
from the `langchain.nlp` module if it's recognized.
"""
code_block_pattern = re.compile(
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
@@ -317,9 +241,8 @@ def update_markdown_with_imports(markdown: str, file_name: str) -> str:
"""
indent = match.group("indent")
code_block = match.group("code")
language = match.group("language") # Preserve the language from the regex match
# Retrieve import information from the code block
imports = get_imports(code_block, file_name)
imports = get_imports(code_block, "__unused__")
original_code_block = match.group(0)
# If no imports are found, return the original code block
+2 -109
View File
@@ -1,10 +1,8 @@
import argparse
import ast
import glob
import os
import re
from pathlib import Path
from typing import Literal, Optional
from typing import Literal
import nbformat
from nbconvert.exporters import MarkdownExporter
@@ -352,17 +350,6 @@ exporter = MarkdownExporter(
],
)
md_executable = MarkdownExporter(
preprocessors=[
ExtractAttachmentsPreprocessor,
EscapePreprocessor(markdown_exec_migration=True),
],
template_name="md_executable",
extra_template_basedirs=[
os.path.join(os.path.dirname(__file__), "notebook_convert_templates")
],
)
def convert_notebook(
notebook_path: Path,
@@ -372,99 +359,5 @@ def convert_notebook(
nb = nbformat.read(f, as_version=4)
nb.metadata.mode = mode
if mode == "markdown":
body, _ = exporter.from_notebook_node(nb)
else:
body, _ = md_executable.from_notebook_node(nb)
body, _ = exporter.from_notebook_node(nb)
return body
HERE = Path(__file__).parent
DOCS = HERE.parent / "docs"
# Convert notebooks to markdown
def _convert_notebooks(
*,
output_dir: Optional[Path] = None,
replace: bool = False,
pattern: str = "*.ipynb",
) -> None:
"""Converting notebooks."""
if not output_dir and not replace:
raise ValueError("Either --output_dir or --replace must be specified")
output_dir_path = DOCS if replace else Path(output_dir)
# Get the directory where the script was executed
base_dir = os.getcwd()
# Build the full search pattern using the current working directory as the base
full_pattern = os.path.join(base_dir, args.pattern)
# Use glob with recursive search enabled
matching_files = glob.glob(full_pattern, recursive=True)
paths = [Path(file) for file in matching_files]
file_names = [notebook.name for notebook in paths]
for notebook in paths:
markdown = convert_notebook(notebook, mode="exec")
markdown_path = output_dir_path / notebook.relative_to(DOCS).with_suffix(".md")
markdown_path.parent.mkdir(parents=True, exist_ok=True)
with open(markdown_path, "w") as f:
f.write(markdown)
if replace:
notebook.unlink(missing_ok=False)
if replace:
# The regex will match markdown links that point to *.ipynb files.
# It captures:
# group(1): the link text (inside the square brackets)
# group(2): the file path (without the trailing .ipynb)
link_pattern = r"(?<!!)\[([^\]]+)\]\((?![^)]*//)([^)]+)\.ipynb\)"
def replace_link(match: re.Match) -> str:
link_text = match.group(1)
link_target = match.group(2)
# Reconstruct the file name with the .ipynb extension.
# For example, if link_target is "foo/bar", then linked_file becomes "bar.ipynb".
linked_file = Path(link_target).name + ".ipynb"
# Only update if the notebook was among those converted.
if linked_file in file_names:
# Change the extension from .ipynb to .md
return f"[{link_text}]({link_target}.md)"
# Otherwise, leave the original link intact.
return match.group(0)
# Process all markdown files in the output directory.
for path in output_dir_path.rglob("**/*.md"):
with open(path, "r", encoding="utf-8") as f:
content = f.read()
new_content = re.sub(link_pattern, replace_link, content)
with open(path, "w", encoding="utf-8") as f:
f.write(new_content)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert notebooks to markdown")
parser.add_argument(
"--output_dir",
default=None,
help="Directory to output markdown files",
)
parser.add_argument(
"--replace",
action="store_true",
help="Replace original notebooks with markdown files",
)
parser.add_argument(
"--pattern",
default="*.ipynb",
help="Glob pattern to match notebooks to convert",
)
args = parser.parse_args()
_convert_notebooks(
replace=args.replace,
output_dir=args.output_dir,
pattern=args.pattern,
)
@@ -1,5 +0,0 @@
{
"mimetypes": {
"text/markdown": true
}
}
@@ -1,38 +0,0 @@
{#https://github.com/rdbisme/nbconvert/blob/master/share/jupyter/nbconvert/templates/markdown/index.md.j2#}
{% extends 'markdown/index.md.j2' %}
{% block input %}
```
{%- if 'magics_language' in cell.metadata -%}
{{ cell.metadata.magics_language}}
{%- elif cell.metadata.get('language') == "shell" -%}
shell
{%- elif 'name' in nb.metadata.get('language_info', {}) -%}
{{ nb.metadata.language_info.name }}{% if cell.metadata.exec|default(false) %} exec="on" source="above" session="1"{% if cell.metadata.has_output|default(false) %} result="ansi"{% endif %}{% endif %}
{%- endif %}
{{ cell.source}}
```
{% endblock input %}
{%- block traceback_line -%}
{%- endblock traceback_line -%}
{%- block stream -%}
{%- endblock stream -%}
{%- block data_text scoped -%}
{%- endblock data_text -%}
{%- block data_html scoped -%}
```html
{{ output.data['text/html'] | safe }}
```
{%- endblock data_html -%}
{%- block data_jpg scoped -%}
![](data:image/jpg;base64,{{ output.data['image/jpeg'] }})
{%- endblock data_jpg -%}
{%- block data_png scoped -%}
![](data:image/png;base64,{{ output.data['image/png'] }})
{%- endblock data_png -%}
+2 -119
View File
@@ -2,18 +2,13 @@ import logging
import os
import posixpath
import re
import traceback
from typing import Any, Callable, Dict
from typing import Any, Dict
from markdown import Markdown
from markdown_exec.hooks import SessionHistoryEntry
from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from pymdownx.superfences import SuperFencesException
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.notebook_convert import convert_notebook
from _scripts.setup_vcr import load_postamble, load_preamble, _hash_string
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -163,118 +158,6 @@ def _highlight_code_blocks(markdown: str) -> str:
return markdown
def handle_vcr_setup(
*,
formatter: Callable,
language: str,
code: str,
session: str,
id: str,
md: Markdown,
**kwargs: Dict[str, Any],
) -> Dict[str, Any]:
"""Handle VCR setup in markdown content if necessary."""
try:
if kwargs.get("extra", None) is None:
raise SuperFencesException(
f"error while processing {language} block: extra dict is required"
)
if kwargs["extra"].get("path", None) is None:
raise SuperFencesException(
f"error while processing {language} block: path is required"
)
document_filename = kwargs["extra"]["path"]
if session is None or session == "" and id is None or id == "":
id = _hash_string(code)
if session is not None and session != "":
logger.info(f"new {language} session {session} on page {document_filename}")
cassette_prefix = document_filename.replace(".md", "").replace(os.path.sep, "_")
cassette_dir = os.path.abspath(
os.path.join(os.path.dirname(os.path.dirname(__file__)), "cassettes")
)
os.makedirs(cassette_dir, exist_ok=True)
# Build a unique cassette name.
cassette_name = os.path.join(
cassette_dir,
f"{cassette_prefix}_{session if session else id}_{language}.msgpack.zlib",
)
# Add context manager at start with explicit __enter__ and __exit__ calls
wrapped_lines = [
load_preamble(language, code, cassette_name),
code,
]
if session is None or session == "":
logger.info(
f"no session, adding postamble for {language} in {document_filename}"
)
wrapped_lines.append(load_postamble(language))
transformed_source = "\n".join(wrapped_lines)
# Propagate extras
keep_extras = {
key: value
for key, value in kwargs["extra"].items()
if key
in {
"hl_lines",
}
}
return dict(
transform_source=lambda code: (transformed_source, code),
id=id,
extra=keep_extras,
)
except Exception as e:
raise SuperFencesException(traceback.format_exc()) from e
def handle_vcr_teardown(
*,
formatter: Callable,
language: str,
session: str,
history: list[SessionHistoryEntry],
):
last_inputs = dict(history[-1].inputs)
code = load_postamble(language)
md = last_inputs["md"]
html = False
update_toc = False
document_filename = last_inputs.get("extra", {}).get("path", None)
if document_filename is None:
logger.warning(f"no document filename found while tearing down {session}!")
else:
logger.info(f"tearing down {language} {session} on {document_filename}")
kwargs = dict(
code=code,
session=session,
id=f"{id}_vcr_end",
md=md,
html=html,
update_toc=update_toc,
extra={},
)
# This doesn't actually render anything, we just call the formatter so it
# executes in the same context as the session of which we're disposing.
formatter(**kwargs)
def _on_page_markdown_with_config(
markdown: str,
page: Page,
@@ -292,7 +175,7 @@ def _on_page_markdown_with_config(
# Append API reference links to code blocks
if add_api_references:
markdown = update_markdown_with_imports(markdown, page.file.src_path)
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
# Apply highlight comments to code blocks
markdown = _highlight_code_blocks(markdown)
-77
View File
@@ -1,77 +0,0 @@
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
import hashlib
import os
from textwrap import dedent
preambles = {
"python": "vcr_setup_preamble.py",
"typescript": "nock_setup_preamble.ts",
}
def _get_python_cassette_init(cassette_name: str, hash_: str) -> str:
return dedent(
f"""
_cassette = HashedCassette('{cassette_name}', '{hash_}')
_cassette.__enter__()
"""
)
def _get_typescript_cassette_init(cassette_name: str, hash_: str) -> str:
return dedent(
f"""
const _cassette = new HashedCassette("{cassette_name}", "{hash_}");
await _cassette.enter();
"""
)
def _get_python_cassette_cleanup() -> str:
return "_cassette.__exit__()"
def _get_typescript_cassette_cleanup() -> str:
return "await _cassette.exit();"
preamble_inits = {
"python": _get_python_cassette_init,
"py": _get_python_cassette_init,
"typescript": _get_typescript_cassette_init,
"ts": _get_typescript_cassette_init,
}
preamble_cleanups = {
"python": _get_python_cassette_cleanup,
"py": _get_python_cassette_cleanup,
"typescript": _get_typescript_cassette_cleanup,
"ts": _get_typescript_cassette_cleanup,
}
def load_preamble(language: str, code: str, cassette_name: str) -> str:
"""Load the source code for the preamble for a given language."""
_assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
preamble_path = os.path.join(_assets_dir, preambles[language])
with open(preamble_path, "r") as f:
lines = f.readlines()
hash_ = _hash_string(code)
lines.append(preamble_inits[language](cassette_name, hash_))
return "\n".join(lines).strip()
def load_postamble(language: str) -> str:
"""Load the source code for the postamble for a given language."""
return preamble_cleanups[language]()
def _hash_string(input_string: str) -> str:
# Encode the input string to bytes
encoded_string = input_string.encode("utf-8")
# Create a SHA-256 hash object
sha256_hash = hashlib.sha256(encoded_string)
# Get the hexadecimal digest of the hash
return sha256_hash.hexdigest()
@@ -89,7 +89,7 @@ def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -
for package in sorted_packages:
name = f"**{package['name']}**"
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
downloads = package["weekly_downloads"] or 0
downloads = package["weekly_downloads"] or "-"
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
rows.append(row)
markdown_content = MARKDOWN.format(
@@ -35,20 +35,45 @@ def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
resolved_packages: list[ResolvedPackage] = []
for package in packages:
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
# First check if package exists on PyPI
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
try:
pypi_response = requests.get(pypi_url)
pypi_response.raise_for_status()
except requests.exceptions.HTTPError:
raise AssertionError(f"Package {package['name']} does not exist on PyPI")
response = requests.get(url)
response.raise_for_status()
data = response.json()
# Get first release date
pypi_data = pypi_response.json()
releases = pypi_data["releases"]
first_release_date = None
for version_releases in releases.values():
if version_releases: # Some versions may be empty lists
upload_time = datetime.fromisoformat(version_releases[0]["upload_time"])
if first_release_date is None or upload_time < first_release_date:
first_release_date = upload_time
sorted_data = sorted(
data["data"],
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
reverse=True,
)
if first_release_date is None:
raise AssertionError(f"Package {package['name']} has no releases yet")
# Sum the last 7 days of downloads
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
# If package was published in last 48 hours, skip download stats
if (datetime.now() - first_release_date).total_seconds() >= 48 * 3600:
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
response = requests.get(url)
response.raise_for_status()
data = response.json()
sorted_data = sorted(
data["data"],
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
reverse=True,
)
# Sum the last 7 days of downloads
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
else:
num_downloads = None
resolved_packages.append(
{
@@ -9,3 +9,12 @@ packages:
- name: "langgraph-supervisor"
repo: "langchain-ai/langgraph-supervisor"
description: "Build supervisor multi-agent systems with LangGraph"
- name: "langmem"
repo: "langchain-ai/langmem"
description: "Build agents that learn and adapt from interactions over time."
- name: "langchain-mcp-adapters"
repo: "langchain-ai/langchain-mcp-adapters"
description: "Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents."
- name: "open-deep-research"
repo: "langchain-ai/open_deep_research"
description: "Open source assistant for iterative web research and report writing."
+2 -1
View File
@@ -12,6 +12,7 @@ This list of companies using LangGraph and their success stories is compiled fro
| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) |
| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) |
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
| [Klarna](https://www.klarna.com/) | Fintech | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/customers-klarna/) |
| [Komodo Health](https://www.komodohealth.com/) | Healthcare | Copilot for domain-specific task | [Blog post](https://www.komodohealth.com/perspectives/new-gen-ai-assistant-empowers-the-enterprise/) |
| [LinkedIn](https://www.linkedin.com/) | Social Media | Code generation; Search & discovery | [Blog post, 2025](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics); [Blog post, 2024](https://www.linkedin.com/blog/engineering/generative-ai/behind-the-platform-the-journey-to-create-the-linkedin-genai-application-tech-stack) |
| [Minimal](https://gominimal.ai/) | E-commerce | Customer support | [Case study, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) |
@@ -22,4 +23,4 @@ This list of companies using LangGraph and their success stories is compiled fro
| [Tradestack](https://www.tradestack.uk/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-tradestack/) |
| [Uber](https://www.uber.com/) | Transportation | Developer productivity; Code generation | [Presentation, 2024](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/); [Video, 2024](https://www.youtube.com/watch?v=8rkA5vWUE4Y) |
| [Unify](https://www.unifygtm.com/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Blog post, 2024](https://blog.langchain.dev/unify-launches-agents-for-account-qualification-using-langgraph-and-langsmith/) |
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
+25
View File
@@ -92,3 +92,28 @@ Starting from the `LangGraph Platform` view...
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. 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
After installing and authorizing LangChain's `hosted-langserve` GitHub app, repository access for the app can be modified to add new repositories or remove existing repositories. If a new repository is created, it may need to be added explicitly.
1. From the GitHub profile, navigate to `Settings` > `Applications` > `hosted-langserve` > click `Configure`.
1. Under `Repository access`, select `All repositories` or `Only select repositories`. If `Only select repositories` is selected, new repositories must be explicitly added.
1. Click `Save`.
1. When creating a new deployment, the list of GitHub repositories in the dropdown menu will be updated to reflect the repository access changes.
## Whitelisting IP Addresses
All traffic from `LangGraph Platform` deployments created after January 6th 2025 will come through a NAT gateway.
This NAT gateway will have several static ip addresses depending on the region you are deploying in. Refer to the table below for the list of IP addresses to whitelist:
| US | EU |
|----------------|----------------|
| 35.197.29.146 | 34.13.192.67 |
| 34.145.102.123 | 34.147.105.64 |
| 34.169.45.153 | 34.90.22.166 |
| 34.82.222.17 | 34.147.36.213 |
| 35.227.171.135 | 34.32.137.113 |
| 34.169.88.30 | 34.91.238.184 |
| 34.19.93.202 | 35.204.101.241 |
| 34.19.34.50 | 35.204.48.32 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

@@ -0,0 +1,15 @@
# Prompt Engineering in LangGraph Studio
In LangGraph Studio you can iterate on the prompts used within your graph by utilizing the LangSmith Playground. To do so:
1. Open an existing thread or create a new one.
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
3. Select the LLM run you want to edit. This will open the LangSmith Playground with the selected LLM run.
![Playground in Studio](../img/studio_playground.png){width=1200}
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
For more information on how to use the LangSmith Playground, see the [LangSmith Playground documentation](https://docs.smith.langchain.com/prompt_engineering/how_to_guides#playground).
+94 -46
View File
@@ -9,13 +9,19 @@ The `useStream()` React hook provides a seamless way to integrate LangGraph into
Key features:
- Messages streaming: Handle a stream of message chunks to form a complete message
- Automatic state management for messages, loading states, and errors
- Automatic state management for messages, interrupts, loading states, and errors
- Conversation branching: Create alternate conversation paths from any point in the chat history
- UI-agnostic design - bring your own components and styling
- UI-agnostic design: bring your own components and styling
Let's explore how to use `useStream()` in your React application.
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://github.com/langchain-ai/assistant-ui).
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we also recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://www.assistant-ui.com/docs/runtimes/langgraph).
## Installation
```bash
npm install @langchain/langgraph-sdk @langchain/core
```
## Example
@@ -58,9 +64,7 @@ export default function App() {
Stop
</button>
) : (
<button key="submit" type="submit">
Send
</button>
<button keytype="submit">Send</button>
)}
</form>
</div>
@@ -74,6 +78,7 @@ The `useStream()` hook takes care of all the complex state management behind the
- Thread state management
- Loading and error states
- Interrupts
- Message handling and updates
- Branching support
@@ -127,9 +132,9 @@ We recommend storing the `threadId` in your URL's query parameters to let users
### Messages Handling
To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook.
The `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
By default, the `messagesKey` is set to `messages`, where it will append the new messages chunks to `values["messages"]`. If you store messages in a different key, you can change the value of `messagesKey`.
```tsx
import type { Message } from "@langchain/langgraph-sdk";
@@ -152,9 +157,49 @@ export default function HomePage() {
}
```
### Branching Support
Under the hood, the `useStream()` hook will use the `streamMode: "messages-key"` to receive a stream of messages (i.e. individual LLM tokens) from any LangChain chat model invocations inside your graph nodes. Learn more about messages streaming in the [How to stream messages from your graph](./stream_messages.md) guide.
To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
### Interrupts
The `useStream()` hook exposes the `interrupt` property, which will be filled with the last interrupt from the thread. You can use interrupts to:
- Render a confirmation UI before executing a node
- Wait for human input, allowing agent to ask the user with clarifying questions
Learn more about interrupts in the [How to handle interrupts](../../how-tos/human_in_the_loop/wait-user-input.ipynb) guide.
```tsx
const thread = useStream<
{ messages: Message[] },
{ InterruptType: string }
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
if (thread.interrupt) {
return (
<div>
Interrupted! {thread.interrupt.value}
<button
type="button"
onClick={() => {
// `resume` can be any value that the agent accepts
thread.submit(undefined, { command: { resume: true } });
}}
>
Resume
</button>
</div>
);
}
```
### Branching
For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
A branch can be created in following ways:
@@ -162,23 +207,12 @@ A branch can be created in following ways:
2. Request a regeneration of a previous assistant message.
```tsx
/* eslint-disable @typescript-eslint/no-floating-promises */
"use client";
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
import { useState } from "react";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
});
function BranchSwitcher({
branch,
branchOptions,
@@ -256,10 +290,7 @@ function EditMessage({
}
export default function App() {
const thread = useStream<
StateType<typeof AgentState.spec>,
UpdateType<typeof AgentState.spec>
>({
const thread = useStream({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
@@ -282,7 +313,7 @@ export default function App() {
onEdit={(message) =>
thread.submit(
{ messages: [message] },
{ checkpoint: parentCheckpoint }
{ checkpoint: parentCheckpoint },
)
}
/>
@@ -337,13 +368,11 @@ export default function App() {
}
```
For advanced use cases you can use the `experimental_branchTree` property to get the tree representation of the thread, which can be used to render branching controls for non-message based graphs.
### TypeScript
The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for:
- State shape
- Update format
- Custom events
The `useStream()` hook is friendly for apps written in TypeScript and you can specify types for the state to get better type safety and IDE support.
```tsx
// Define your types
@@ -352,25 +381,44 @@ type State = {
context?: Record<string, unknown>;
};
type Update = {
messages: Message[] | Message;
context?: Record<string, unknown>;
};
type CustomEvent = {
type: "progress" | "debug";
payload: unknown;
};
// Use them with the hook
const thread = useStream<State, Update, CustomEvent>({
const thread = useStream<State>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
```
If you're using LangGraph.js, you can reuse your graph's annotation types:
You can also optionally specify types for different scenarios, such as:
- `ConfigurableType`: Type for the `config.configurable` property (default: `Record<string, unknown>`)
- `InterruptType`: Type for the interrupt value - i.e. contents of `interrupt(...)` function (default: `unknown`)
- `CustomEventType`: Type for the custom events (default: `unknown`)
- `UpdateType`: Type for the submit function (default: `Partial<State>`)
```tsx
const thread = useStream<State, {
UpdateType: {
messages: Message[] | Message;
context?: Record<string, unknown>;
};
InterruptType: string;
CustomEventType: {
type: "progress" | "debug";
payload: unknown;
};
ConfigurableType: {
model: string;
};
}>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
```
If you're using LangGraph.js, you can also reuse your graph's annotation types. However, make sure to only import the types of the annotation schema in order to avoid importing the entire LangGraph.js runtime (i.e. via `import type { ... }` directive).
```tsx
import {
@@ -382,12 +430,12 @@ import {
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
context: Annotation.Optional(Annotation.Any()),
context: Annotation<string>(),
});
const thread = useStream<
StateType<typeof AgentState.spec>,
UpdateType<typeof AgentState.spec>
{ UpdateType: UpdateType<typeof AgentState.spec> }
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
@@ -403,7 +451,7 @@ The `useStream()` hook provides several callback options to help you respond to
- `onFinish`: Called when the stream is finished.
- `onUpdateEvent`: Called when an update event is received.
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
- `onMetadataEvent`: Called when a metadata event is received.
- `onMetadataEvent`: Called when a metadata event is received, which contains the Run ID and Thread ID.
## Learn More
+119 -114
View File
@@ -1,142 +1,147 @@
# Use Webhooks
# Using Webhooks
You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the "webhook" parameter.
When working with LangGraph Cloud, you may want to use webhooks to receive updates after an API call completes. Webhooks are useful for triggering actions in your service once a run has finished processing. To implement this, you need to expose an endpoint that can accept `POST` requests and pass this endpoint as a `webhook` parameter in your API request.
Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows.
Currently, the SDK does not provide built-in support for defining webhook endpoints, but you can specify them manually using API requests.
The following endpoints accept `webhook` as a parameter:
## Supported Endpoints
- Create Run -> POST /thread/{thread_id}/runs
- Create Thread Cron -> POST /thread/{thread_id}/runs/crons
- Stream Run -> POST /thread/{thread_id}/runs/stream
- Wait Run -> POST /thread/{thread_id}/runs/wait
- Create Cron -> POST /runs/crons
- Stream Run Stateless -> POST /runs/stream
- Wait Run Stateless -> POST /runs/wait
The following API endpoints accept a `webhook` parameter:
In this example, we will show calling a webhook after streaming a run.
| Operation | HTTP Method | Endpoint |
|-----------|------------|----------|
| Create Run | `POST` | `/thread/{thread_id}/runs` |
| Create Thread Cron | `POST` | `/thread/{thread_id}/runs/crons` |
| Stream Run | `POST` | `/thread/{thread_id}/runs/stream` |
| Wait Run | `POST` | `/thread/{thread_id}/runs/wait` |
| Create Cron | `POST` | `/runs/crons` |
| Stream Run Stateless | `POST` | `/runs/stream` |
| Wait Run Stateless | `POST` | `/runs/wait` |
## Setup
In this guide, well show how to trigger a webhook after streaming a run.
First, let's setup our assistant and thread:
## Setting Up Your Assistant and Thread
Before making API calls, set up your assistant and thread.
=== "Python"
```python
from langgraph_sdk import get_client
```python
from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
print(thread)
```
client = get_client(url=<DEPLOYMENT_URL>)
# Using the graph deployed with the name "agent"
assistant_id = "agent"
# create thread
thread = await client.threads.create()
print(thread)
```
=== "JavaScript"
```js
import { Client } from "@langchain/langgraph-sdk";
=== "Javascript"
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// Using the graph deployed with the name "agent"
const assistantID = "agent";
// create thread
const thread = await client.threads.create();
console.log(thread);
```
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantID = "agent";
const thread = await client.threads.create();
console.log(thread);
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/assistants/search \
--header 'Content-Type: application/json' \
--data '{ "limit": 10, "offset": 0 }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json' \
--data '{}'
```
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/assistants/search \
--header 'Content-Type: application/json' \
--data '{
"limit": 10,
"offset": 0
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json' \
--data '{}'
```
### Example Response
```json
{
"thread_id": "9dde5490-2b67-47c8-aa14-4bfec88af217",
"created_at": "2024-08-30T23:07:38.242730+00:00",
"updated_at": "2024-08-30T23:07:38.242730+00:00",
"metadata": {},
"status": "idle",
"config": {},
"values": null
}
```
Output:
## Using a Webhook with a Graph Run
{
'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217',
'created_at': '2024-08-30T23:07:38.242730+00:00',
'updated_at': '2024-08-30T23:07:38.242730+00:00',
'metadata': {},
'status': 'idle',
'config': {},
'values': None
}
To use a webhook, specify the `webhook` parameter in your API request. When the run completes, LangGraph Cloud sends a `POST` request to the specified webhook URL.
## Use graph with a webhook
To invoke a run with a webhook, we specify the `webhook` parameter with the desired endpoint when creating a run. Webhook requests are triggered by the end of a run.
For example, if we can receive requests at `https://my-server.app/my-webhook-endpoint`, we can pass this to `stream`:
For example, if your server listens for webhook events at `https://my-server.app/my-webhook-endpoint`, include this in your request:
=== "Python"
```python
input = { "messages": [{ "role": "user", "content": "Hello!" }] }
```python
# create input
input = { "messages": [{ "role": "user", "content": "Hello!" }] }
async for chunk in client.runs.stream(
thread_id=thread["thread_id"],
assistant_id=assistant_id,
input=input,
stream_mode="events",
webhook="https://my-server.app/my-webhook-endpoint"
):
pass
```
async for chunk in client.runs.stream(
thread_id=thread["thread_id"],
assistant_id=assistant_id,
input=input,
stream_mode="events",
webhook="https://my-server.app/my-webhook-endpoint"
):
# Do something with the stream output
pass
```
=== "JavaScript"
```js
const input = { messages: [{ role: "human", content: "Hello!" }] };
=== "Javascript"
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantID,
{
input: input,
webhook: "https://my-server.app/my-webhook-endpoint"
}
);
```js
// create input
const input = { messages: [{ role: "human", content: "Hello!" }] };
// stream events
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantID,
{
input: input,
webhook: "https://my-server.app/my-webhook-endpoint"
}
);
for await (const chunk of streamResponse) {
// Do something with the stream output
}
```
for await (const chunk of streamResponse) {
// Handle stream output
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data '{
"assistant_id": <ASSISTANT_ID>,
"input" : {"messages":[{"role": "user", "content": "Hello!"}]},
"webhook": "https://my-server.app/my-webhook-endpoint"
}'
```
The schema for the payload sent to `my-webhook-endpoint` is that of a [run](../../concepts/langgraph_server.md/#runs). See [API Reference](https://langchain-ai.github.io/langgraph/cloud/reference/api/api_ref.html#model/run) for more detail. Note that the run input, configuration, etc. are included in the `kwargs` field.
### Signing webhook requests
To sign the webhook requests, we can specify a token parameter in the webhook URL, e.g.,
```
https://my-server.app/my-webhook-endpoint?token=...
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data '{
"assistant_id": <ASSISTANT_ID>,
"input": {"messages": [{"role": "user", "content": "Hello!"}]},
"webhook": "https://my-server.app/my-webhook-endpoint"
}'
```
The server should then extract the token from the request's parameters and validate it before processing the payload.
## Webhook Payload
LangGraph Cloud sends webhook notifications in the format of a [Run](../../concepts/langgraph_server.md/#runs). See the [API Reference](https://langchain-ai.github.io/langgraph/cloud/reference/api/api_ref.html#model/run) for details. The request payload includes run input, configuration, and other metadata in the `kwargs` field.
## Securing Webhooks
To ensure only authorized requests hit your webhook endpoint, consider adding a security token as a query parameter:
```
https://my-server.app/my-webhook-endpoint?token=YOUR_SECRET_TOKEN
```
Your server should extract and validate this token before processing requests.
## Testing Webhooks
You can test your webhook using online services like:
- **[Beeceptor](https://beeceptor.com/)** Quickly create a test endpoint and inspect incoming webhook payloads.
- **[Webhook.site](https://webhook.site/)** View, debug, and log incoming webhook requests in real time.
These tools help you verify that LangGraph Cloud is correctly triggering and sending webhooks to your service.
---
By following these steps, you can integrate webhooks into your LangGraph Cloud workflow, automating actions based on completed runs.
+1
View File
@@ -51,6 +51,7 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
| <span style="white-space: nowrap;">`pip_config_file`</span> | Path to `pip` config file. |
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
| <span style="white-space: nowrap;">`http`</span> | HTTP server configuration with the following fields: <ul><li>`app`: Path to custom Starlette/FastAPI app (e.g., `"./src/agent/webapp.py:app"`). See [custom routes guide](../../how-tos/http/custom_routes.md).</li><li>`disable_assistants`: Disable `/assistants` routes</li><li>`disable_threads`: Disable `/threads` routes</li><li>`disable_runs`: Disable `/runs` routes</li><li>`disable_store`: Disable `/store` routes</li><li>`disable_meta`: Disable `/ok`, `/info`, `/metrics`, and `/docs` routes</li><li>`cors`: CORS configuration with fields for `allow_origins`, `allow_methods`, `allow_headers`, etc.</li></ul> |
=== "JS"
+1 -1
View File
@@ -83,7 +83,7 @@ node at a time or if you want to pause the graph execution at specific nodes.
### `NodeInterrupt` exception
We recommend that you [**use the `interrupt` function instead**](#the-interrupt-function) of the `NodeInterrupt` exception if you're trying to implement
We recommend that you [**use the `interrupt` function instead**][langgraph.types.interrupt] of the `NodeInterrupt` exception if you're trying to implement
[human-in-the-loop](./human_in_the_loop.md) workflows. The `interrupt` function is easier to use and more flexible.
??? node "`NodeInterrupt` exception"
+2 -2
View File
@@ -30,7 +30,7 @@ The guide below will explain the differences between the deployment options.
!!! warning "Note"
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
The LangGraph Platform Deployments view is optionally available for Self-Hosted Enterprise LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
With a Self-Hosted Enterprise deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
@@ -49,7 +49,7 @@ For more information, please see:
!!! warning "Note"
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
The LangGraph Platform Deployments view is optionally available for Self-Hosted Lite LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
+152
View File
@@ -0,0 +1,152 @@
# Durable Execution
**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later).
LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state.
!!! tip
If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures.
To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](./functional_api.md#task). You can use [tasks](./functional_api.md#task) from both the [StateGraph (Graph API)](./low_level.md) and the [Functional API](./functional_api.md).
## Requirements
To leverage durable execution in LangGraph, you need to:
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
## Determinism and Consistent Replay
When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped.
As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](./functional_api.md#task) or [nodes](./low_level.md#nodes).
To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines:
- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer.
- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes.
- **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution.
For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows
how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph].
## Using tasks in nodes
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
=== "Original"
```python
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
import requests
# Define a TypedDict to represent the state
class State(TypedDict):
url: str
result: NotRequired[str]
def call_api(state: State):
"""Example node that makes an API request."""
# highlight-next-line
result = requests.get(state['url']).text[:100] # Side-effect
return {
"result": result
}
# Create a StateGraph builder and add a node for the call_api function
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# Connect the start and end nodes to the call_api node
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
# Define a config with a thread ID.
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# Invoke the graph
graph.invoke({"url": "https://www.example.com"}, config)
```
=== "With task"
```python
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import task
from langgraph.graph import StateGraph, START, END
import requests
# Define a TypedDict to represent the state
class State(TypedDict):
urls: list[str]
result: NotRequired[list[str]]
@task
def _make_request(url: str):
"""Make a request."""
# highlight-next-line
return requests.get(url).text[:100]
def call_api(state: State):
"""Example node that makes an API request."""
# highlight-next-line
requests = [_make_request(url) for url in state['urls']]
results = [request.result() for request in requests]
return {
"results": results
}
# Create a StateGraph builder and add a node for the call_api function
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# Connect the start and end nodes to the call_api node
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
# Define a config with a thread ID.
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# Invoke the graph
graph.invoke({"urls": ["https://www.example.com"]}, config)
```
## Resuming Workflows
Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios:
- **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](./functional_api.md#resuming-after-an-error) with the functional API).
## Starting Points for Resuming Workflows
* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped.
* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
+2 -4
View File
@@ -1,8 +1,5 @@
# Functional API
!!! warning "Beta"
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
## Overview
The **Functional API** allows you to add LangGraph's key features -- [persistence](./persistence.md), [memory](./memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code.
@@ -832,7 +829,8 @@ from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
# Global variable to track the number of attempts
# This variable is just used for demonstration purposes to simulate a network failure.
# It's not something you will have in your actual code.
attempts = 0
@task()
+2 -6
View File
@@ -647,19 +647,15 @@ def node_in_parent_graph(state: State):
This will print out
```pycon
--- First invocation ---
In parent node: {'foo': 'bar'}
Entered `parent_node` a total of 1 times
Entered `node_in_subgraph` a total of 1 times
Entered human_node in sub-graph a total of 1 times
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:0b23d72f-aaba-0329-1a59-ca4f3c8bad3b', 'human_node:25df717c-cb80-57b0-7410-44e20aac8f3c'], when='during'),)}
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:4c3a0248-21f0-1287-eacf-3002bc304db4', 'human_node:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when='during'),)}
--- Resuming ---
In parent node: {'foo': 'bar'}
Entered `parent_node` a total of 2 times
Entered human_node in sub-graph a total of 2 times
Got an answer of 35
{'parent_node': None}
{'parent_node': {'state_counter': 1}}
```
+5 -2
View File
@@ -7,7 +7,7 @@ description: Conceptual Guide for LangGraph
This guide provides explanations of the key concepts behind the LangGraph framework and AI applications more broadly.
We recommend that you go through at least the [Quick Start](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
We recommend that you go through at least the [Quickstart](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
The conceptual guide does not cover step-by-step instructions or specific implementation examples — those are found in the [Tutorials](../tutorials/index.md) and [How-to guides](../how-tos/index.md). For detailed reference material, please see the [API reference](../reference/index.md).
@@ -28,7 +28,9 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
- [Functional API](functional_api.md): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
- [Durable Execution](durable_execution.md): LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
- [Pregel](pregel.md): Pregel is LangGraph's runtime, which is responsible for managing the execution of LangGraph applications.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
## LangGraph Platform
@@ -46,6 +48,7 @@ The LangGraph Platform offers a few different deployment options described in th
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
- [Platform Architecture](./platform_architecture.md): A high-level overview of the architecture of the LangGraph Platform.
- [Scalability and Resilience](./scalability_and_resilience.md): LangGraph Platform is designed to be scalable and resilient. This document explains how the platform achieves this.
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
+16
View File
@@ -80,6 +80,22 @@ A high-level diagram of a Cloud SaaS deployment.
![diagram](img/langgraph_cloud_architecture.png)
## Whitelisting IP Addresses
All traffic from `LangGraph Platform` deployments created after January 6th 2025 will come through a NAT gateway.
This NAT gateway will have several static ip addresses depending on the region you are deploying in. Refer to the table below for the list of IP addresses to whitelist:
| US | EU |
|----------------|----------------|
| 35.197.29.146 | 34.13.192.67 |
| 34.145.102.123 | 34.147.105.64 |
| 34.169.45.153 | 34.90.22.166 |
| 34.82.222.17 | 34.147.36.213 |
| 35.227.171.135 | 34.32.137.113 |
| 34.169.88.30 | 34.91.238.184 |
| 34.19.93.202 | 35.204.101.241 |
| 34.19.34.50 | 35.204.48.32 |
## Related
- [Deployment Options](./deployment_options.md)
+2
View File
@@ -112,6 +112,7 @@ In this architecture, agents are defined as graph nodes. Each agent can communic
```python
from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from langgraph.graph import StateGraph, MessagesState, START, END
model = ChatOpenAI()
@@ -158,6 +159,7 @@ In this architecture, we define agents as nodes and add a supervisor node (LLM)
```python
from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from langgraph.graph import StateGraph, MessagesState, START, END
model = ChatOpenAI()
+347
View File
@@ -0,0 +1,347 @@
# LangGraph's Runtime (Pregel)
[Pregel][langgraph.pregel.Pregel] implements LangGraph's runtime, managing the execution of LangGraph applications.
Compiling a [StateGraph][langgraph.graph.StateGraph] or creating an [entrypoint][langgraph.func.entrypoint] produces a [Pregel][langgraph.pregel.Pregel] instance that can be invoked with input.
This guide explains the runtime at a high level and provides instructions for directly implementing applications with Pregel.
> **Note:** The [Pregel][langgraph.pregel.Pregel] runtime is named after [Google's Pregel algorithm](https://research.google/pubs/pub37252/), which describes an efficient method for large-scale parallel computation using graphs.
## Overview
In LangGraph, Pregel combines [**actors**](https://en.wikipedia.org/wiki/Actor_model) and **channels** into a single application. **Actors** read data from channels and write data to channels. Pregel organizes the execution of the application into multiple steps, following the **Pregel Algorithm**/**Bulk Synchronous Parallel** model.
Each step consists of three phases:
- **Plan**: Determine which **actors** to execute in this step. For example, in the first step, select the **actors** that subscribe to the special **input** channels; in subsequent steps, select the **actors** that subscribe to channels updated in the previous step.
- **Execution**: Execute all selected **actors** in parallel, until all complete, or one fails, or a timeout is reached. During this phase, channel updates are invisible to actors until the next step.
- **Update**: Update the channels with the values written by the **actors** in this step.
Repeat until no **actors** are selected for execution, or a maximum number of steps is reached.
## Actors
An **actor** is a [PregelNode][langgraph.pregel.read.PregelNode]. It subscribes to channels, reads data from them, and writes data to them. It can be thought of as an **actor** in the Pregel algorithm. [PregelNodes][langgraph.pregel.read.PregelNode] implement LangChain's Runnable interface.
## Channels
Channels are used to communicate between actors (PregelNodes). Each channel has a value type, an update type, and an update function which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. LangGraph provides a number of built-in channels:
### Basic channels: LastValue and Topic
- [LastValue][langgraph.channels.LastValue]: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next.
- [Topic][langgraph.channels.Topic]: A configurable PubSub Topic, useful for sending multiple values between **actors**, or for accumulating output. Can be configured to deduplicate values or to accumulate values over the course of multiple steps.
### Advanced channels: Context and BinaryOperatorAggregate
- `Context`: exposes the value of a context manager, managing its lifecycle. Useful for accessing external resources that require setup and/or teardown; e.g., `client = Context(httpx.Client)`.
- [BinaryOperatorAggregate][langgraph.channels.BinaryOperatorAggregate]: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps; e.g.,`total = BinaryOperatorAggregate(int, operator.add)`
## Examples
While most users will interact with Pregel through the [StateGraph][langgraph.graph.StateGraph] API or
the [entrypoint][langgraph.func.entrypoint] decorator, it is possible to interact with Pregel directly.
Below are a few different examples to give you a sense of the Pregel API.
=== "Single node"
```python
from langgraph.channels import EphemeralValue
from langgraph.pregel import Pregel, Channel
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| Channel.write_to("b")
)
app = Pregel(
nodes={"node1": node1},
channels={
"a": EphemeralValue(str),
"b": EphemeralValue(str),
},
input_channels=["a"],
output_channels=["b"],
)
app.invoke({"a": "foo"})
```
```con
{'b': 'foofoo'}
```
=== "Multiple nodes"
```python
from langgraph.channels import LastValue, EphemeralValue
from langgraph.pregel import Pregel, Channel
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| Channel.write_to("b")
)
node2 = (
Channel.subscribe_to("b")
| (lambda x: x + x)
| Channel.write_to("c")
)
app = Pregel(
nodes={"node1": node1, "node2": node2},
channels={
"a": EphemeralValue(str),
"b": LastValue(str),
"c": EphemeralValue(str),
},
input_channels=["a"],
output_channels=["b", "c"],
)
app.invoke({"a": "foo"})
```
```con
{'b': 'foofoo', 'c': 'foofoofoofoo'}
```
=== "Topic"
```python
from langgraph.channels import EphemeralValue, Topic
from langgraph.pregel import Pregel, Channel
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| {
"b": Channel.write_to("b"),
"c": Channel.write_to("c")
}
)
node2 = (
Channel.subscribe_to("b")
| (lambda x: x + x)
| {
"c": Channel.write_to("c"),
}
)
app = Pregel(
nodes={"node1": node1, "node2": node2},
channels={
"a": EphemeralValue(str),
"b": EphemeralValue(str),
"c": Topic(str, accumulate=True),
},
input_channels=["a"],
output_channels=["c"],
)
app.invoke({"a": "foo"})
```
```pycon
{'c': ['foofoo', 'foofoofoofoo']}
```
=== "BinaryOperatorAggregate"
This examples demonstrates how to use the BinaryOperatorAggregate channel to implement a reducer.
```python
from langgraph.channels import EphemeralValue, BinaryOperatorAggregate
from langgraph.pregel import Pregel, Channel
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| {
"b": Channel.write_to("b"),
"c": Channel.write_to("c")
}
)
node2 = (
Channel.subscribe_to("b")
| (lambda x: x + x)
| {
"c": Channel.write_to("c"),
}
)
def reducer(current, update):
if current:
return current + " | " + "update"
else:
return update
app = Pregel(
nodes={"node1": node1, "node2": node2},
channels={
"a": EphemeralValue(str),
"b": EphemeralValue(str),
"c": BinaryOperatorAggregate(str, operator=reducer),
},
input_channels=["a"],
output_channels=["c"],
)
app.invoke({"a": "foo"})
```
=== "Cycle"
This example demonstrates how to introduce a cycle in the graph, by having
a chain write to a channel it subscribes to. Execution will continue
until a None value is written to the channel.
```python
from langgraph.channels import EphemeralValue
from langgraph.pregel import Pregel, Channel, ChannelWrite, ChannelWriteEntry
example_node = (
Channel.subscribe_to("value")
| (lambda x: x + x if len(x) < 10 else None)
| ChannelWrite(writes=[ChannelWriteEntry(channel="value", skip_none=True)])
)
app = Pregel(
nodes={"example_node": example_node},
channels={
"value": EphemeralValue(str),
},
input_channels=["value"],
output_channels=["value"],
)
app.invoke({"value": "a"})
```
```pycon
{'value': 'aaaaaaaaaaaaaaaa'}
```
## High-level API
LangGraph provides two high-level APIs for creating a Pregel application: the [StateGraph (Graph API)](./low_level.md) and the [Functional API](functional_api.md).
=== "StateGraph (Graph API)"
The [StateGraph (Graph API)][langgraph.graph.StateGraph] is a higher-level abstraction that simplifies the creation of Pregel applications. It allows you to define a graph of nodes and edges. When you compile the graph, the StateGraph API automatically creates the Pregel application for you.
```python
from typing import TypedDict, Optional
from langgraph.constants import START
from langgraph.graph import StateGraph
class Essay(TypedDict):
topic: str
content: Optional[str]
score: Optional[float]
def write_essay(essay: Essay):
return {
"content": f"Essay about {essay['topic']}",
}
def score_essay(essay: Essay):
return {
"score": 10
}
builder = StateGraph(Essay)
builder.add_node(write_essay)
builder.add_node(score_essay)
builder.add_edge(START, "write_essay")
# Compile the graph.
# This will return a Pregel instance.
graph = builder.compile()
```
The compiled Pregel instance will be associated with a list of nodes and channels. You can inspect the nodes and channels by printing them.
```python
print(graph.nodes)
```
You will see something like this:
```pycon
{'__start__': <langgraph.pregel.read.PregelNode at 0x7d05e3ba1810>,
'write_essay': <langgraph.pregel.read.PregelNode at 0x7d05e3ba14d0>,
'score_essay': <langgraph.pregel.read.PregelNode at 0x7d05e3ba1710>}
```
```python
print(graph.channels)
```
You should see something like this
```pycon
{'topic': <langgraph.channels.last_value.LastValue at 0x7d05e3294d80>,
'content': <langgraph.channels.last_value.LastValue at 0x7d05e3295040>,
'score': <langgraph.channels.last_value.LastValue at 0x7d05e3295980>,
'__start__': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e3297e00>,
'write_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e32960c0>,
'score_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e2d8ab80>,
'branch:__start__:__self__:write_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e32941c0>,
'branch:__start__:__self__:score_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e2d88800>,
'branch:write_essay:__self__:write_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e3295ec0>,
'branch:write_essay:__self__:score_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e2d8ac00>,
'branch:score_essay:__self__:write_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e2d89700>,
'branch:score_essay:__self__:score_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e2d8b400>,
'start:write_essay': <langgraph.channels.ephemeral_value.EphemeralValue at 0x7d05e2d8b280>}
```
=== "Functional API"
In the [Functional API](functional_api.md), you can use an [`entrypoint`][langgraph.func.entrypoint] to create
a Pregel application. The `entrypoint` decorator allows you to define a function that takes input and returns output.
```python
from typing import TypedDict, Optional
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.func import entrypoint
class Essay(TypedDict):
topic: str
content: Optional[str]
score: Optional[float]
checkpointer = InMemorySaver()
@entrypoint(checkpointer=checkpointer)
def write_essay(essay: Essay):
return {
"content": f"Essay about {essay['topic']}",
}
print("Nodes: ")
print(write_essay.nodes)
print("Channels: ")
print(write_essay.channels)
```
```pycon
Nodes:
{'write_essay': <langgraph.pregel.read.PregelNode object at 0x7d05e2f9aad0>}
Channels:
{'__start__': <langgraph.channels.ephemeral_value.EphemeralValue object at 0x7d05e2c906c0>, '__end__': <langgraph.channels.last_value.LastValue object at 0x7d05e2c90c40>, '__previous__': <langgraph.channels.last_value.LastValue object at 0x7d05e1007280>}
```
@@ -0,0 +1,35 @@
# LangGraph Platform: Scalability & Resilience
LangGraph Platform is designed to scale horizontally with your workload. Each instance of the service is stateless, and keeps no resources in memory. The service is designed to gracefully handle new instances being added or removed, including hard shutdown cases.
## Server scalability
As you add more instances to a service, they will share the HTTP load as long as an appropriate load balancer mechanism is placed in front of them. In most deployment modalities we configure a load balancer for the service automatically. In the “self-hosted without control plane” modality its your responsibility to add a load balancer. Since the instances are stateless any load balancing strategy will work, no session stickiness is needed, or recommended. Any instance of the server can communicate with any queue instance (through Redis PubSub), meaning that requests to cancel or stream an in-progress run can be handled by any arbitrary instance.
## Queue scalability
As you add more instances to a service, they will increase run throughput linearly, as each instance is configured to handle a set number of concurrent runs (by default 10). Each attempt for each run will be handled by a single instance, with exactly-once semantics enforced through Postgress MVCC model (refer to section below for crash resilience details). Attempts that fail due to transient database errors are retried up to 3 times. We do not make use of long-lived transactions or locks, this enables us to make more efficient use of Postgres resources.
## Resilience
While a run is being handled by a queue instance, a periodic heartbeat timestamp will be recorded in Redis by that queue worker.
When a graceful shutdown request is received (SIGINT) an instance enters shutdown mode, which
- stops accepting new HTTP requests
- gives any in-progress runs a limited number of seconds to finish (if not finished it will be put back in the queue)
- stops the instance from picking up more runs from the queue
If a hard shutdown occurs, eg. due to a server crash, or an infra failure, any runs that were in progress will be picked up by a periodic sweeper task that looks for in-progress runs that have breached their heartbeat window, which will put them back in the queue for another instance to pick them up.
## Postgres resilience
For deployment modalities where we manage the Postgres database we have periodic backups, continuously replicated standby replicas for automatic failover. Optionally, on request, we can also setup read replicas as well as other advanced failover capabilities.
All communication with Postgres implements retries for retry-able errors. If Postgres is momentarily unavailable, such as during a database restart, most/all traffic should continue to succeed. Prolonged failure of the Postgres instance will switch traffic to the failover replica. If the failover replica also fails before the primary is brought back online the service would become unavailable.
## Redis resilience
All data that requires durable storage is stored in Postgres, not Redis. Redis is used only for ephemeral metadata, and communication between instances. Refer to the [architecture](./platform_architecture.md) page for more details on how we use Redis. Therefore we place no durability requirements on Redis.
All communication with Redis implements retries for retry-able errors. If Redis is momentarily unavailable, such as during a database restart, most/all traffic should continue to succeed. Prolonged failure of Redis will render the LGP service unavailable.
+1 -1
View File
@@ -34,7 +34,7 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
!!! warning "Note"
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite or Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
The LangGraph Platform Deployments view is optionally available for Self-Hosted LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
@@ -170,8 +170,6 @@
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal, TypedDict\n",
"\n",
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.graph import add_messages\n",
@@ -224,12 +222,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Find numbers between 10 and 30 in fibonacci sequence\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
"\n",
@@ -255,9 +253,9 @@
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"exitcode: 0 (execution succeeded)\n",
"Code output: \n",
@@ -266,7 +264,7 @@
"\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
"\n",
@@ -320,7 +318,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Multiply the last number by 3\n",
"Context: \n",
@@ -336,7 +334,7 @@
"TERMINATE\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
"\n",
+8 -10
View File
@@ -168,8 +168,6 @@
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal, TypedDict\n",
"\n",
"from langchain_core.messages import convert_to_openai_messages\n",
"from langgraph.graph import StateGraph, MessagesState, START\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
@@ -241,12 +239,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Find numbers between 10 and 30 in fibonacci sequence\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
"\n",
@@ -272,9 +270,9 @@
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"exitcode: 0 (execution succeeded)\n",
"Code output: \n",
@@ -283,7 +281,7 @@
"\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
"\n",
@@ -338,7 +336,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Multiply the last number by 3\n",
"Context: \n",
@@ -354,7 +352,7 @@
"TERMINATE\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
"\n",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+82
View File
@@ -0,0 +1,82 @@
# How to add custom lifespan events
When deploying agents on the LangGraph platform, you often need to initialize resources like database connections when your server starts up, and ensure they're properly closed when it shuts down. Lifespan events let you hook into your server's startup and shutdown sequence to handle these critical setup and teardown tasks.
This works the same way as [adding custom routes](./custom_routes.md) - you just need to provide your own [`Starlette`](https://www.starlette.io/applications/) app (including [`FastAPI`](https://fastapi.tiangolo.com/), [`FastHTML`](https://fastht.ml/) and other compatible apps).
Below is an example using FastAPI.
???+ note "Python only"
We currently only support custom lifespan events in Python deployments with `langgraph-api>=0.0.26`.
## Create app
Starting from an **existing** LangGraph Platform application, add the following lifespan code to your `webapp.py` file. If you are starting from scratch, you can create a new app from a template using the CLI.
```bash
langgraph new --template=new-langgraph-project-python my_new_project
```
Once you have a LangGraph project, add the following app code:
```python
# ./src/agent/webapp.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
@asynccontextmanager
async def lifespan(app: FastAPI):
# for example...
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
# Create reusable session factory
async_session = sessionmaker(engine, class_=AsyncSession)
# Store in app state
app.state.db_session = async_session
yield
# Clean up connections
await engine.dispose()
# highlight-next-line
app = FastAPI(lifespan=lifespan)
# ... can add custom routes if needed.
```
## Configure `langgraph.json`
Add the following to your `langgraph.json` file. Make sure the path points to the `webapp.py` file you created above.
```json
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent/graph.py:graph"
},
"env": ".env",
"http": {
"app": "./src/agent/webapp.py:app"
}
// Other configuration options like auth, store, etc.
}
```
## Start server
Test the server out locally:
```bash
langgraph dev --no-browser
```
You should see your startup message printed when the server starts, and your cleanup message when you stop it with Ctrl+C.
## Deploying
You can deploy your app as-is to the managed langgraph cloud or to your self-hosted platform.
## Next steps
Now that you've added lifespan events to your deployment, you can use similar techniques to add [custom routes](./custom_routes.md) or [custom middleware](./custom_middleware.md) to further customize your server's behavior.
@@ -0,0 +1,75 @@
# How to add custom middleware
When deploying agents on the LangGraph platform, you can add custom middleware to your server to handle cross-cutting concerns like logging request metrics, injecting or checking headers, and enforcing security policies without modifying core server logic. This works the same way as [adding custom routes](./custom_routes.md) - you just need to provide your own [`Starlette`](https://www.starlette.io/applications/) app (including [`FastAPI`](https://fastapi.tiangolo.com/), [`FastHTML`](https://fastht.ml/) and other compatible apps).
Adding middleware lets you intercept and modify requests and responses globally across your deployment, whether they're hitting your custom endpoints or the built-in LangGraph Platform APIs.
Below is an example using FastAPI.
???+ note "Python only"
We currently only support custom middleware in Python deployments with `langgraph-api>=0.0.26`.
## Create app
Starting from an **existing** LangGraph Platform application, add the following middleware code to your `webapp.py` file. If you are starting from scratch, you can create a new app from a template using the CLI.
```bash
langgraph new --template=new-langgraph-project-python my_new_project
```
Once you have a LangGraph project, add the following app code:
```python
# ./src/agent/webapp.py
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
# highlight-next-line
app = FastAPI()
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers['X-Custom-Header'] = 'Hello from middleware!'
return response
# Add the middleware to the app
app.add_middleware(CustomHeaderMiddleware)
```
## Configure `langgraph.json`
Add the following to your `langgraph.json` file. Make sure the path points to the `webapp.py` file you created above.
```json
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent/graph.py:graph"
},
"env": ".env",
"http": {
"app": "./src/agent/webapp.py:app"
}
// Other configuration options like auth, store, etc.
}
```
## Start server
Test the server out locally:
```bash
langgraph dev --no-browser
```
Now any request to your server will include the custom header `X-Custom-Header` in its response.
## Deploying
You can deploy this app as-is to the managed langgraph cloud or to your self-hosted platform.
## Next steps
Now that you've added custom middleware to your deployment, you can use similar techniques to add [custom routes](./custom_routes.md) or define [custom lifespan events](./custom_lifespan.md) to further customize your server's behavior.
+78
View File
@@ -0,0 +1,78 @@
# How to add custom routes
When deploying agents on the LangGraph platform, your server automatically exposes routes for creating runs and threads, interacting with the long-term memory store, managing configurable assistants, and other core functionality ([see all default API endpoints](../../cloud/reference/api/api_ref.md)).
You can add custom routes by providing your own [`Starlette`](https://www.starlette.io/applications/) app (including [`FastAPI`](https://fastapi.tiangolo.com/), [`FastHTML`](https://fastht.ml/) and other compatible apps). You make LangGraph Platform aware of this by providing a path to the app in your `langgraph.json` configuration file. (`"http": {"app": "path/to/app.py:app"}`).
Defining a custom app object lets you add any routes you'd like, so you can do anything from adding a `/login` endpoint to writing an entire full-stack web-app, all deployed in a single LangGraph deployment.
Below is an example using FastAPI.
???+ note "Python only"
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.26`.
## Create app
Starting from an **existing** LangGraph Platform application, add the following custom route code to your `webapp.py` file. If you are starting from scratch, you can create a new app from a template using the CLI.
```bash
langgraph new --template=new-langgraph-project-python my_new_project
```
Once you have a LangGraph project, add the following app code:
```python
# ./src/agent/webapp.py
from fastapi import FastAPI
# highlight-next-line
app = FastAPI()
@app.get("/hello")
def read_root():
return {"Hello": "World"}
```
## Configure `langgraph.json`
Add the following to your `langgraph.json` file. Make sure the path points to the `app.py` file you created above.
```json
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent/graph.py:graph"
},
"env": ".env",
"http": {
"app": "./src/agent/webapp.py:app"
}
// Other configuration options like auth, store, etc.
}
```
## Start server
Test the server out locally:
```bash
langgraph dev --no-browser
```
If you navigate to `localhost:2024/hello` in your browser (2024 is the default development port), you should see the `hello` endpoint returning `{"Hello": "World"}`.
!!! note "Shadowing default endpoints"
The routes you create in the app are given priority over the system defaults, meaning you can shadow and redefine the behavior of any default endpoint.
## Deploying
You can deploy this app as-is to the managed langgraph cloud or to your self-hsoted platform.
## Next steps
Now that you've added a custom route to your deployment, you can use this same technique to further customize how your server behaves, such as defining custom [custom middleware](./custom_middleware.md) and [custom lifespan events](./custom_lifespan.md).
+16 -13
View File
@@ -11,9 +11,9 @@ Here youll find answers to “How do I...?” types of questions. These guide
### Graph API Basics
- [How to update graph state from nodes](state-reducers.md)
- [How to create a sequence of steps](sequence.md)
- [How to create branches for parallel execution](branching.md)
- [How to update graph state from nodes](state-reducers.ipynb)
- [How to create a sequence of steps](sequence.ipynb)
- [How to create branches for parallel execution](branching.ipynb)
- [How to create and control loops with recursion limits](recursion-limit.ipynb)
- [How to visualize your graph](visualization.ipynb)
@@ -39,8 +39,7 @@ execution of your graph.
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
See the below guides for how-to add persistence to your workflow using the (beta)
[Functional API](../concepts/functional_api.md):
See the below guides for how-to add persistence to your workflow using the [Functional API](../concepts/functional_api.md):
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
@@ -73,7 +72,7 @@ Other methods:
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
See the below guides for how-to implement human-in-the-loop workflows with the
[Functional API](../concepts/functional_api.md):
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
@@ -130,8 +129,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
See the below guides for how to implement multi-agent workflows with the (beta)
[Functional API](../concepts/functional_api.md):
See the below guides for how to implement multi-agent workflows with the [Functional API](../concepts/functional_api.md):
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
@@ -149,8 +147,7 @@ See the below guides for how to implement multi-agent workflows with the (beta)
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
See the below guide for how to integrate with other frameworks using the (beta)
[Functional API](../concepts/functional_api.md):
See the below guide for how to integrate with other frameworks using the [Functional API](../concepts/functional_api.md):
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
@@ -162,7 +159,7 @@ One of the big benefits of LangGraph is that you can easily create your own agen
These guides show how to use the prebuilt ReAct agent:
- [How to use the pre-built ReAct agent](create-react-agent.md)
- [How to use the pre-built ReAct agent](create-react-agent.ipynb)
- [How to add thread-level memory to a ReAct Agent](create-react-agent-memory.ipynb)
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
@@ -174,8 +171,7 @@ overview of its underlying implementation to help you customize for your own nee
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
See the below guide for how-to build ReAct agents with the (beta)
[Functional API](../concepts/functional_api.md):
See the below guide for how-to build ReAct agents with the [Functional API](../concepts/functional_api.md):
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
@@ -219,6 +215,12 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
- [How to add custom authentication](./auth/custom_auth.md)
- [How to update the security schema of your OpenAPI spec](./auth/openapi_security.md)
### Modifying the API
- [How to add custom routes](./http/custom_routes.md)
- [How to add custom middleware](./http/custom_middleware.md)
- [How to add custom lifespan events](./http/custom_lifespan.md)
### Assistants
[Assistants](../concepts/assistants.md) is a configured instance of a template.
@@ -294,6 +296,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
- [How to engineer prompts in LangGraph Studio](../cloud/how-tos/iterate_graph_studio.md)
## Troubleshooting
+1 -1
View File
@@ -207,7 +207,7 @@
"\n",
"\n",
"# Here we define the logic to map out over the generated subjects\n",
"# We will use this an edge in the graph\n",
"# We will use this as an edge in the graph\n",
"def continue_to_jokes(state: OverallState):\n",
" # We will return a list of `Send` objects\n",
" # Each `Send` object consists of the name of a node in the graph\n",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -33
View File
@@ -1,37 +1,7 @@
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
# 🚀 Prebuilt Agents
LangGraph includes a prebuilt React agent. For more information on how to use it,
check out our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent).
If youre looking for other prebuilt libraries, explore the community-built options
below. These libraries can extend LangGraph's functionality in various ways.
## 📚 Available Libraries
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
| Name | GitHub URL | Description | Weekly Downloads |
| --- | --- | --- | --- |
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 8803 |
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 636 |
| **breeze-agent** | [andrestorres123/breeze-agent](https://github.com/andrestorres123/breeze-agent) | A streamlined research system built inspired on STORM and built on LangGraph | 184 |
## ✨ Contributing Your Library
Have you built an awesome open-source library using LangGraph? We'd love to feature
your project on the official LangGraph documentation pages! 🏆
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) file.
**Guidelines**
- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm
for JavaScript/TypeScript, etc.) 📦
- The repo should either use the Graph API (exposing a `StateGraph` instance) or
the Functional API (exposing an `entrypoint`).
- The package must include documentation (e.g., a `README.md` or docs site)
explaining how to use it.
We'll review your contribution and merge it in!
Thanks for contributing! 🚀
[//]: # (This file is stub. Do not edit this file directly!)
[//]: # (1. Update the `packages.yml` file in the `docs/_scripts/third_party_page` directory.)
[//]: # (2. From the /docs directory, run `make build-prebuilt` to generate an updated version of this file for testing locally.)
+5 -7
View File
@@ -1,9 +1,7 @@
::: langgraph.pregel.Pregel
# Pregel
::: langgraph.pregel
options:
members:
- stream
- astream
- invoke
- ainvoke
- update_state
- aupdate_state
- Pregel
- PregelNode
@@ -22,7 +22,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain langsmith langchain_openai"
"%pip install -U langgraph langchain langsmith langchain_openai langchain_community"
]
},
{
@@ -496,16 +496,16 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[1massistant\u001b[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
"\u001b[1muser\u001b[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
"\u001b[1massistant\u001b[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
"\u001b[1muser\u001b[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
"\u001b[1massistant\u001b[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
"\u001b[1muser\u001b[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
"\u001b[1massistant\u001b[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
"\u001b[1muser\u001b[0m: I'm sorry, I cannot help with that.\n",
"\u001b[1massistant\u001b[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
"\u001b[1muser\u001b[0m: FINISHED\n"
"\u001B[1massistant\u001B[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
"\u001B[1muser\u001B[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
"\u001B[1massistant\u001B[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
"\u001B[1muser\u001B[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
"\u001B[1massistant\u001B[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
"\u001B[1muser\u001B[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
"\u001B[1massistant\u001B[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
"\u001B[1muser\u001B[0m: I'm sorry, I cannot help with that.\n",
"\u001B[1massistant\u001B[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
"\u001B[1muser\u001B[0m: FINISHED\n"
]
}
],
@@ -555,7 +555,6 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain.smith import RunEvalConfig\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
@@ -614,12 +613,10 @@
}
],
"source": [
"evaluation = RunEvalConfig(evaluators=[did_resist])\n",
"\n",
"result = client.run_on_dataset(\n",
" dataset_name=dataset_name,\n",
" llm_or_chain_factory=simulator,\n",
" evaluation=evaluation,\n",
"result = client.evaluate(\n",
" simulator,\n",
" data=dataset_name,\n",
" evaluators=[did_resist],\n",
")"
]
}
@@ -0,0 +1,203 @@
import functools
from typing import Annotated, Any, Callable, Dict, List, Optional, Union
from langchain_community.adapters.openai import convert_message_to_dict
from langchain_core.messages import AIMessage, AnyMessage, BaseMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import Runnable, RunnableLambda
from langchain_core.runnables import chain as as_runnable
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
def langchain_to_openai_messages(messages: List[BaseMessage]):
"""
Convert a list of langchain base messages to a list of openai messages.
Parameters:
messages (List[BaseMessage]): A list of langchain base messages.
Returns:
List[dict]: A list of openai messages.
"""
return [
convert_message_to_dict(m) if isinstance(m, BaseMessage) else m
for m in messages
]
def create_simulated_user(
system_prompt: str, llm: Runnable | None = None
) -> Runnable[Dict, AIMessage]:
"""
Creates a simulated user for chatbot simulation.
Args:
system_prompt (str): The system prompt to be used by the simulated user.
llm (Runnable | None, optional): The language model to be used for the simulation.
Defaults to gpt-3.5-turbo.
Returns:
Runnable[Dict, AIMessage]: The simulated user for chatbot simulation.
"""
return ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder(variable_name="messages"),
]
) | (llm or ChatOpenAI(model="gpt-3.5-turbo")).with_config(
run_name="simulated_user"
)
Messages = Union[list[AnyMessage], AnyMessage]
def add_messages(left: Messages, right: Messages) -> Messages:
if not isinstance(left, list):
left = [left]
if not isinstance(right, list):
right = [right]
return left + right
class SimulationState(TypedDict):
"""
Represents the state of a simulation.
Attributes:
messages (List[AnyMessage]): A list of messages in the simulation.
inputs (Optional[dict[str, Any]]): Optional inputs for the simulation.
"""
messages: Annotated[List[AnyMessage], add_messages]
inputs: Optional[dict[str, Any]]
def create_chat_simulator(
assistant: (
Callable[[List[AnyMessage]], str | AIMessage]
| Runnable[List[AnyMessage], str | AIMessage]
),
simulated_user: Runnable[Dict, AIMessage],
*,
input_key: str,
max_turns: int = 6,
should_continue: Optional[Callable[[SimulationState], str]] = None,
):
"""Creates a chat simulator for evaluating a chatbot.
Args:
assistant: The chatbot assistant function or runnable object.
simulated_user: The simulated user object.
input_key: The key for the input to the chat simulation.
max_turns: The maximum number of turns in the chat simulation. Default is 6.
should_continue: Optional function to determine if the simulation should continue.
If not provided, a default function will be used.
Returns:
The compiled chat simulation graph.
"""
graph_builder = StateGraph(SimulationState)
graph_builder.add_node(
"user",
_create_simulated_user_node(simulated_user),
)
graph_builder.add_node(
"assistant", _fetch_messages | assistant | _coerce_to_message
)
graph_builder.add_edge("assistant", "user")
graph_builder.add_conditional_edges(
"user",
should_continue or functools.partial(_should_continue, max_turns=max_turns),
)
# If your dataset has a 'leading question/input', then we route first to the assistant, otherwise, we let the user take the lead.
graph_builder.add_edge(START, "assistant" if input_key is not None else "user")
return (
RunnableLambda(_prepare_example).bind(input_key=input_key)
| graph_builder.compile()
)
## Private methods
def _prepare_example(inputs: dict[str, Any], input_key: Optional[str] = None):
if input_key is not None:
if input_key not in inputs:
raise ValueError(
f"Dataset's example input must contain the provided input key: '{input_key}'.\nFound: {list(inputs.keys())}"
)
messages = [HumanMessage(content=inputs[input_key])]
return {
"inputs": {k: v for k, v in inputs.items() if k != input_key},
"messages": messages,
}
return {"inputs": inputs, "messages": []}
def _invoke_simulated_user(state: SimulationState, simulated_user: Runnable):
"""Invoke the simulated user node."""
runnable = (
simulated_user
if isinstance(simulated_user, Runnable)
else RunnableLambda(simulated_user)
)
inputs = state.get("inputs", {})
inputs["messages"] = state["messages"]
return runnable.invoke(inputs)
def _swap_roles(state: SimulationState):
new_messages = []
for m in state["messages"]:
if isinstance(m, AIMessage):
new_messages.append(HumanMessage(content=m.content))
else:
new_messages.append(AIMessage(content=m.content))
return {
"inputs": state.get("inputs", {}),
"messages": new_messages,
}
@as_runnable
def _fetch_messages(state: SimulationState):
"""Invoke the simulated user node."""
return state["messages"]
def _convert_to_human_message(message: BaseMessage):
return {"messages": [HumanMessage(content=message.content)]}
def _create_simulated_user_node(simulated_user: Runnable):
"""Simulated user accepts a {"messages": [...]} argument and returns a single message."""
return (
_swap_roles
| RunnableLambda(_invoke_simulated_user).bind(simulated_user=simulated_user)
| _convert_to_human_message
)
def _coerce_to_message(assistant_output: str | BaseMessage):
if isinstance(assistant_output, str):
return {"messages": [AIMessage(content=assistant_output)]}
else:
return {"messages": [assistant_output]}
def _should_continue(state: SimulationState, max_turns: int = 6):
messages = state["messages"]
# TODO support other stop criteria
if len(messages) > max_turns:
return END
elif messages[-1].content.strip() == "FINISHED":
return END
else:
return "assistant"
+8 -28
View File
@@ -56,29 +56,6 @@ plugins:
- search:
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
- autorefs
- markdown-exec:
ansi: required
hooks:
python:
pre_session:
- _scripts.notebook_hooks:handle_vcr_setup
post_session:
- _scripts.notebook_hooks:handle_vcr_teardown
py:
pre_session:
- _scripts.notebook_hooks:handle_vcr_setup
post_session:
- _scripts.notebook_hooks:handle_vcr_teardown
typescript:
pre_session:
- _scripts.notebook_hooks:handle_vcr_setup
post_session:
- _scripts.notebook_hooks:handle_vcr_teardown
ts:
pre_session:
- _scripts.notebook_hooks:handle_vcr_setup
post_session:
- _scripts.notebook_hooks:handle_vcr_teardown
- mkdocstrings:
handlers:
python:
@@ -123,9 +100,9 @@ nav:
- LangGraph: how-tos#langgraph
- Graph API Basics:
- Graph API Basics: how-tos#graph-api-basics
- how-tos/state-reducers.md
- how-tos/sequence.md
- how-tos/branching.md
- how-tos/state-reducers.ipynb
- how-tos/sequence.ipynb
- how-tos/branching.ipynb
- how-tos/recursion-limit.ipynb
- how-tos/visualization.ipynb
- Controllability:
@@ -203,7 +180,7 @@ nav:
- how-tos/autogen-integration-functional.ipynb
- Prebuilt ReAct Agent:
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
- how-tos/create-react-agent.md
- how-tos/create-react-agent.ipynb
- how-tos/create-react-agent-memory.ipynb
- how-tos/create-react-agent-system-prompt.ipynb
- how-tos/create-react-agent-hitl.ipynb
@@ -293,6 +270,8 @@ nav:
- concepts/memory.md
- concepts/streaming.md
- concepts/functional_api.md
- concepts/durable_execution.md
- concepts/pregel.md
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- High Level:
@@ -378,6 +357,7 @@ nav:
- tutorials/auth/resource_auth.md
- tutorials/auth/add_auth_server.md
- Resources:
# NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
- Prebuilt Agents: prebuilt.md
- Adopters: adopters.md
- FAQ: concepts/faq.md
@@ -487,7 +467,7 @@ extra:
link: https://twitter.com/LangChainAI
analytics:
provider: google
property: G-WR87FQLG9F
property: G-G8X6ELZYE0
feedback:
title: Was this page helpful?
ratings:
+212
View File
@@ -0,0 +1,212 @@
"""Test generation of links into the API reference."""
import pytest
from _scripts.generate_api_reference_links import (
update_markdown_with_imports,
get_imports,
)
MARKDOWN_IMPORTS = """\
```python
from langgraph.types import interrupt
```
"""
EXPECTED_MARKDOWN = """\
```python
from langgraph.types import interrupt
```
API Reference: <a href="https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt">interrupt</a>
"""
def test_update_markdown_with_imports() -> None:
"""Light weight end-to-end test."""
assert (
update_markdown_with_imports(MARKDOWN_IMPORTS, "some_path") == EXPECTED_MARKDOWN
)
@pytest.mark.parametrize(
"code_block, expected_imports",
[
(
"from langgraph.types import interrupt",
[
{
"docs": "https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt",
"imported": "interrupt",
"path": "some_path",
"source": "langgraph.types",
}
],
),
(
"from langgraph.types import ( interrupt )",
[
{
"docs": "https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt",
"imported": "interrupt",
"path": "some_path",
"source": "langgraph.types",
}
],
),
(
"from langgraph.types import interrupt as foo",
[
{
"docs": "https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt",
"imported": "interrupt",
"path": "some_path",
"source": "langgraph.types",
}
],
),
],
)
def test_get_imports(code_block: str, expected_imports: list) -> None:
"""Get imports from a code block."""
assert (
get_imports(code_block, "some_path") == expected_imports
), f"Failed for code_block=`{code_block}`"
@pytest.mark.parametrize(
"code, expected_imports",
[
# Single import without parenthesis
(
"from langgraph.types import interrupt",
[
{
"source": "langgraph.types",
"imported": "interrupt",
}
],
),
# Multiple imports
(
(
"from langgraph.types import interrupt\n"
"from langgraph.func import task"
),
[
{
"source": "langgraph.types",
"imported": "interrupt",
},
{
"source": "langgraph.func",
"imported": "task",
},
],
),
# Single import with parenthesis and extra whitespace
(
"from langgraph.types import ( interrupt )",
[
{
"source": "langgraph.types",
"imported": "interrupt",
}
],
),
# Single import with an alias
(
"from langgraph.types import interrupt as foo",
[
{
"source": "langgraph.types",
"imported": "interrupt",
}
],
),
# Multiple imports on one line with an alias
(
"from langgraph.types import interrupt, StreamWriter as bar",
[
{
"source": "langgraph.types",
"imported": "interrupt",
},
{
"source": "langgraph.types",
"imported": "StreamWriter",
},
],
),
# Multiple imports without aliases
(
"from langgraph.types import interrupt, StreamWriter",
[
{
"source": "langgraph.types",
"imported": "interrupt",
},
{
"source": "langgraph.types",
"imported": "StreamWriter",
},
],
),
# Multiline import with parenthesis and trailing comma
(
"""from langgraph.types import (
interrupt,
StreamWriter as foo,
Command,
)""",
[
{
"source": "langgraph.types",
"imported": "interrupt",
},
{
"source": "langgraph.types",
"imported": "StreamWriter",
},
{
"source": "langgraph.types",
"imported": "Command",
},
],
),
# Multiline import with parenthesis and trailing comma
(
(
"from langgraph.types import (\n"
" interrupt,\n"
" StreamWriter as foo\n,"
" Command,\n"
")\n"
"def foo():\n"
" pass\n"
""
),
[
{
"source": "langgraph.types",
"imported": "interrupt",
},
{
"source": "langgraph.types",
"imported": "StreamWriter",
},
{
"source": "langgraph.types",
"imported": "Command",
},
],
),
],
)
def test_regexp_matching(code: str, expected_imports: list) -> None:
results = get_imports(code, "some_path")
for result in results:
del result["docs"]
del result["path"]
assert results == expected_imports
@@ -1,81 +1,10 @@
import nbformat
import pytest
from _scripts.notebook_convert import (
_convert_links_in_markdown,
md_executable,
_has_output,
)
EXPECTED_OUTPUT = """\
```python exec="on" source="above" session="1" result="ansi"
print("Hello, world!")
```
"""
def test_convert_normal_code_block() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell('print("Hello, world!")'))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == EXPECTED_OUTPUT
# We treat cell magic as a non-executable code block.
CELL_MAGIC_INPUT = """\
%%capture
%pip install numpy
"""
CELL_MAGIC_OUTPUT = """\
```shell
pip install numpy
```
"""
def test_convert_cell_magic() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell(CELL_MAGIC_INPUT))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == CELL_MAGIC_OUTPUT
STDIN_INPUT = """\
input("Enter your name: ")\
"""
STDIN_OUTPUT = """\
```python
input("Enter your name: ")
```
"""
def test_convert_input_cell() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell(STDIN_INPUT))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == STDIN_OUTPUT
NO_STDOUT_EXPECTED = """\
```python exec="on" source="above" session="1"
display(x)
```
"""
def test_convert_block_without_output() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell("display(x)"))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == NO_STDOUT_EXPECTED
def test_has_output() -> None:
"""Test if a given code block is expected to have output."""
+2 -2
View File
@@ -348,7 +348,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.10"
version = "2.0.15"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1255,4 +1255,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9.0,<4.0"
content-hash = "ca18ea480430714e47685ff0f25da9990dbbdecd38e9c1a3ca41d42be7069067"
content-hash = "369bfffecb9489835b43b8255932e043176a11d2f639aad2d055ffd89263ca1e"
+1 -1
View File
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^2.0.10"
langgraph-checkpoint = "^2.0.15"
orjson = ">=3.10.1"
psycopg = "^3.2.0"
psycopg-pool = "^3.2.0"
+2 -2
View File
@@ -350,7 +350,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.10"
version = "2.0.15"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1043,4 +1043,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9.0"
content-hash = "03c697eae6f550f3c7e29f1d61f4c409dabe04ae8d43281728e549174d2fc670"
content-hash = "e6d3ca9bce723c05f4c5ae9dc4bee872f7581b7763680b34112f1d280f5a9b0a"
+1 -1
View File
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0"
langgraph-checkpoint = "^2.0.10"
langgraph-checkpoint = "^2.0.15"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev.dependencies]
+3 -3
View File
@@ -1,6 +1,6 @@
# LangGraph Checkpoint
This library defines the base interface for LangGraph checkpointers. Checkpointers provide persistence layer for LangGraph. They allow you to interact with and manage the graph's state. When you use a graph with a checkpointer, the checkpointer saves a _checkpoint_ of the graph state at every superstep, enabling several powerful capabilities like human-in-the-loop, "memory" between interactions and more.
This library defines the base interface for LangGraph checkpointers. Checkpointers provide a persistence layer for LangGraph. They allow you to interact with and manage the graph's state. When you use a graph with a checkpointer, the checkpointer saves a _checkpoint_ of the graph state at every superstep, enabling several powerful capabilities like human-in-the-loop, "memory" between interactions and more.
## Key concepts
@@ -12,8 +12,8 @@ Checkpoint is a snapshot of the graph state at a given point in time. Checkpoint
Threads enable the checkpointing of multiple different runs, making them essential for multi-tenant chat applications and other scenarios where maintaining separate states is necessary. A thread is a unique ID assigned to a series of checkpoints saved by a checkpointer. When using a checkpointer, you must specify a `thread_id` and optionally `checkpoint_id` when running the graph.
- `thread_id` is simply the ID of a thread. This is always required
- `checkpoint_id` can optionally be passed. This identifier refers to a specific checkpoint within a thread. This can be used to kick of a run of a graph from some point halfway through a thread.
- `thread_id` is simply the ID of a thread. This is always required.
- `checkpoint_id` can optionally be passed. This identifier refers to a specific checkpoint within a thread. This can be used to kick off a run of a graph from some point halfway through a thread.
You must pass these when invoking the graph as part of the configurable part of the config, e.g.
@@ -5,6 +5,7 @@ import json
import pathlib
import re
from collections import deque
from collections.abc import Sequence
from datetime import date, datetime, time, timedelta, timezone
from enum import Enum
from inspect import isclass
@@ -16,7 +17,7 @@ from ipaddress import (
IPv6Interface,
IPv6Network,
)
from typing import Any, Callable, Optional, Sequence, Union, cast
from typing import Any, Callable, Optional, Union, cast
from uuid import UUID
import msgpack # type: ignore[import-untyped]
@@ -502,15 +503,5 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
return
ENC_POOL: deque[msgpack.Packer] = deque(maxlen=32)
def _msgpack_enc(data: Any) -> bytes:
try:
enc = ENC_POOL.popleft()
except IndexError:
enc = msgpack.Packer(default=_msgpack_default)
try:
return enc.pack(data)
finally:
ENC_POOL.append(enc)
return msgpack.packb(data, default=_msgpack_default)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.15"
version = "2.0.16"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+62
View File
@@ -0,0 +1,62 @@
from contextlib import asynccontextmanager
from contextvars import ContextVar
from typing import Any
from starlette.applications import Starlette
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from starlette.routing import Route
my_context_var: ContextVar[str] = ContextVar("my_context_var", default="")
LIFESPAN_VAL = ""
other_context_var = ContextVar("other_context_var", default="")
@asynccontextmanager
async def my_lifespan(app):
global LIFESPAN_VAL
LIFESPAN_VAL = "foobar-lifespan"
yield
assert LIFESPAN_VAL == "foobar-lifespan"
LIFESPAN_VAL = ""
class MyContextMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Any, call_next: Any) -> Any:
token = my_context_var.set("Foobar")
try:
response = await call_next(request)
return response
finally:
my_context_var.reset(token)
async def custom_my_route(request):
"""A great route."""
assert my_context_var.get() == "Foobar"
assert LIFESPAN_VAL == "foobar-lifespan"
return JSONResponse({"foo": "bar"})
async def runs_afakeroute(request):
"""Another great route."""
assert my_context_var.get() == "Foobar"
assert LIFESPAN_VAL == "foobar-lifespan"
return JSONResponse({"foo": "afakeroute"})
async def other_middleware(request: Any, call_next: Any) -> Any:
other_context_var.set("foobar")
response = await call_next(request)
other_context_var.reset()
return response
app = Starlette(
middleware=[(MyContextMiddleware, {}, {})],
routes=[
Route("/custom/my-route", custom_my_route),
Route("/runs/afakeroute", runs_afakeroute),
],
lifespan=my_lifespan,
)
+31 -13
View File
@@ -303,7 +303,15 @@ def _build(
tag,
]
# apply config
stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image)
stdin, additional_contexts = langgraph_cli.config.config_to_docker(
config, config_json, base_image
)
# add additional_contexts
if additional_contexts:
additional_contexts_str = ",".join(
f"{k}={v}" for k, v in additional_contexts.items()
)
args.extend(["--build-context", additional_contexts_str])
# run docker build
runner.run(
subp_exec(
@@ -439,20 +447,28 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
secho("✅ Configuration validated!", fg="green")
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
dockerfile, additional_contexts = langgraph_cli.config.config_to_docker(
config,
config_json,
(
"langchain/langgraphjs-api"
if config_json.get("node_version")
else "langchain/langgraph-api"
),
)
with open(str(save_path), "w", encoding="utf-8") as f:
f.write(
langgraph_cli.config.config_to_docker(
config,
config_json,
(
"langchain/langgraphjs-api"
if config_json.get("node_version")
else "langchain/langgraph-api"
),
)
)
f.write(dockerfile)
secho("✅ Created: Dockerfile", fg="green")
if additional_contexts:
additional_contexts_str = ",".join(
f"{k}={v}" for k, v in additional_contexts.items()
)
secho(
f"""📝 Run docker build with these additional build contexts `--build-context {additional_contexts_str}`""",
fg="yellow",
)
if add_docker_compose:
# Add docker compose and related files
# Add .dockerignore file in the same directory as the Dockerfile
@@ -575,7 +591,7 @@ def dev(
):
"""CLI entrypoint for running the LangGraph API server."""
try:
from langgraph_api.cli import run_server
from langgraph_api.cli import run_server # type: ignore
except ImportError:
py_version_msg = ""
if sys.version_info < (3, 11):
@@ -634,6 +650,7 @@ def dev(
store=config_json.get("store"),
wait_for_client=wait_for_client,
auth=config_json.get("auth"),
http=config_json.get("http"),
)
@@ -662,6 +679,7 @@ def prepare_args_and_stdin(
debugger_base_url: Optional[str] = None,
postgres_uri: Optional[str] = None,
) -> Tuple[List[str], str]:
assert config_path.exists(), f"Config file not found: {config_path}"
# prepare args
stdin = langgraph_cli.docker.compose(
capabilities,
+201 -36
View File
@@ -2,6 +2,7 @@ import json
import os
import pathlib
import textwrap
from collections import Counter
from typing import NamedTuple, Optional, TypedDict, Union
import click
@@ -85,6 +86,33 @@ class AuthConfig(TypedDict, total=False):
"""
class CorsConfig(TypedDict, total=False):
allow_origins: list[str]
allow_methods: list[str]
allow_headers: list[str]
allow_credentials: bool
allow_origin_regex: str
expose_headers: list[str]
max_age: int
class HttpConfig(TypedDict, total=False):
app: str
"""Import path for a custom Starlette/FastAPI app to mount"""
disable_assistants: bool
"""Disable /assistants routes"""
disable_threads: bool
"""Disable /threads routes"""
disable_runs: bool
"""Disable /runs routes"""
disable_store: bool
"""Disable /store routes"""
disable_meta: bool
"""Disable /ok, /info, /metrics, and /docs routes"""
cors: Optional[CorsConfig]
"""Cross-Origin Resource Sharing (CORS) configuration"""
class Config(TypedDict, total=False):
"""Configuration for langgraph-cli."""
@@ -123,6 +151,9 @@ class Config(TypedDict, total=False):
auth: Optional[AuthConfig]
"""Configuration for authentication."""
http: Optional[HttpConfig]
"""Configuration for HTTP server."""
def _parse_version(version_str: str) -> tuple[int, int]:
"""Parse a version string into a tuple of (major, minor)."""
@@ -157,6 +188,7 @@ def validate_config(config: Config) -> Config:
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
}
if config.get("node_version")
else {
@@ -168,6 +200,7 @@ def validate_config(config: Config) -> Config:
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
}
)
@@ -220,7 +253,13 @@ def validate_config(config: Config) -> Config:
f"Invalid auth.path format: '{auth_conf['path']}'. "
"Must be in format './path/to/file.py:attribute_name'"
)
if http_conf := config.get("http"):
if "app" in http_conf:
if ":" not in http_conf["app"]:
raise ValueError(
f"Invalid http.app format: '{http_conf['app']}'. "
"Must be in format './path/to/file.py:attribute_name'"
)
return config
@@ -294,10 +333,10 @@ class LocalDeps(NamedTuple):
tuples. Each entry points to a local `requirements.txt` file and where
it should be placed inside the Docker container before running `pip install`.
real_pkgs: A dictionary mapping a local directory path (host side) to the
same dependency string from the config. These directories contain the
necessary files (e.g., `pyproject.toml` or `setup.py`) to be installed
as a standard Python package with pip.
real_pkgs: A dictionary mapping a local directory path (host side) to a
tuple of (dependency_string, container_package_path). These directories
contain the necessary files (e.g., `pyproject.toml` or `setup.py`) to be
installed as a standard Python package with pip.
faux_pkgs: A dictionary mapping a local directory path (host side) to a
tuple of (dependency_string, container_package_path). For these
@@ -310,16 +349,23 @@ class LocalDeps(NamedTuple):
directory. If the local dependency `"."` is present in the config, this
field captures the path where that dependency will appear in the
container (e.g., `/deps/<name>` or similar). Otherwise, it may be `None`.
additional_contexts: A list of paths to directories that contain local
dependencies in parent directories. These directories are added to the
Docker build context to ensure that the Dockerfile can access them.
"""
pip_reqs: list[tuple[str, str]]
real_pkgs: dict[pathlib.Path, str]
pip_reqs: list[tuple[pathlib.Path, str]]
real_pkgs: dict[pathlib.Path, tuple[str, str]]
faux_pkgs: dict[pathlib.Path, tuple[str, str]]
# if . is in dependencies, use it as working_dir
working_dir: Optional[str] = None
# if there are local dependencies in parent directories, use additional_contexts
additional_contexts: list[pathlib.Path] = None
def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps:
config_path = config_path.resolve()
# ensure reserved package names are not used
reserved = {
"src",
@@ -336,6 +382,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
"httpx",
"langsmith",
}
counter = Counter()
def check_reserved(name: str, ref: str):
if name in reserved:
@@ -348,7 +395,8 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
pip_reqs = []
real_pkgs = {}
faux_pkgs = {}
working_dir = None
working_dir: Optional[str] = None
additional_contexts: list[pathlib.Path] = []
for local_dep in config["dependencies"]:
if not local_dep.startswith("."):
@@ -357,7 +405,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
# Verify that the local dependency can be resolved
# (e.g., this would raise an informative error if a user mistyped a path).
resolved = config_path.parent / local_dep
resolved = (config_path.parent / local_dep).resolve()
# validate local dependency
if not resolved.exists():
@@ -366,25 +414,28 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
raise NotADirectoryError(
f"Local dependency must be a directory: {resolved}"
)
elif not resolved.is_relative_to(config_path.parent):
raise ValueError(
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
)
elif resolved == config_path.parent:
pass
elif config_path.parent not in resolved.parents:
additional_contexts.append(resolved)
# Check for pyproject.toml or setup.py
# If found, treat as a real package, if not treat as a faux package.
# For faux packages, we'll also check for presence of requirements.txt.
files = os.listdir(resolved)
if "pyproject.toml" in files:
if "pyproject.toml" in files or "setup.py" in files:
# real package
real_pkgs[resolved] = local_dep
# assign a unique folder name
container_name = resolved.name
if counter[container_name] > 0:
container_name += f"_{counter[container_name]}"
counter[container_name] += 1
# add to deps
real_pkgs[resolved] = (local_dep, container_name)
# set working_dir
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
elif "setup.py" in files:
# real package
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
working_dir = f"/deps/{container_name}"
else:
# We could not find a pyproject.toml or setup.py, so treat as a faux package
if any(file == "__init__.py" for file in files):
@@ -423,12 +474,12 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
rfile = resolved / "requirements.txt"
pip_reqs.append(
(
rfile.relative_to(config_path.parent).as_posix(),
rfile,
f"{container_path}/requirements.txt",
)
)
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts)
def _update_graph_paths(
@@ -554,9 +605,62 @@ def _update_auth_path(
)
def _update_http_app_path(
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
) -> None:
"""Update the HTTP app path to point to the correct location in the Docker container.
Similar to _update_graph_paths, this ensures that if a custom app is specified via
a local file path, that file is included in the Docker build context and its path
is updated to point to the correct location in the container.
"""
if not (http_config := config.get("http")) or not (
app_str := http_config.get("app")
):
return
module_str, _, attr_str = app_str.partition(":")
if not module_str or not attr_str:
message = (
'Import string "{import_str}" must be in format "<module>:<attribute>".'
)
raise ValueError(message.format(import_str=app_str))
# Check if it's a file path
if "/" in module_str or "\\" in module_str:
# Resolve the local path properly on the current OS
resolved = (config_path.parent / module_str).resolve()
if not resolved.exists():
raise FileNotFoundError(f"Could not find HTTP app module: {resolved}")
elif not resolved.is_file():
raise IsADirectoryError(f"HTTP app module must be a file: {resolved}")
else:
for path in local_deps.real_pkgs:
if resolved.is_relative_to(path):
container_path = (
pathlib.Path("/deps") / path.name / resolved.relative_to(path)
)
module_str = container_path.as_posix()
break
else:
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
if resolved.is_relative_to(faux_pkg):
container_subpath = resolved.relative_to(faux_pkg)
# Construct the final path, ensuring POSIX style
module_str = f"{destpath}/{container_subpath.as_posix()}"
break
else:
raise ValueError(
f"HTTP app module '{app_str}' not found in 'dependencies' list. "
"Add its containing package to 'dependencies' list."
)
# update the config
http_config["app"] = f"{module_str}:{attr_str}"
def python_config_to_docker(
config_path: pathlib.Path, config: Config, base_image: str
) -> str:
) -> tuple[str, dict[str, str]]:
"""Generate a Dockerfile from the configuration."""
# configure pip
pip_install = (
@@ -577,13 +681,21 @@ def python_config_to_docker(
_update_graph_paths(config_path, config, local_deps)
# Rewrite auth path, so it points to the correct location in the Docker container
_update_auth_path(config_path, config, local_deps)
# Rewrite HTTP app path, so it points to the correct location in the Docker container
_update_http_app_path(config_path, config, local_deps)
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
if local_deps.pip_reqs:
pip_reqs_str = os.linesep.join(
f"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
f"COPY --from=__outer_{reqpath.name} requirements.txt {destpath}"
if reqpath.parent in local_deps.additional_contexts
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
for reqpath, destpath in local_deps.pip_reqs
)
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
pip_reqs_str = f"""# -- Installing local requirements --
{pip_reqs_str}
# -- End of local requirements install --"""
else:
pip_reqs_str = ""
@@ -591,7 +703,14 @@ def python_config_to_docker(
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
# https://til.simonwillison.net/python/pyproject
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
f"""ADD {relpath} {destpath}
(
f"""# -- Adding non-package dependency {fullpath.name} --
COPY --from=__outer_{fullpath.name} . {destpath}"""
if fullpath in local_deps.additional_contexts
else f"""# -- Adding non-package dependency {fullpath.name} --
ADD {relpath} {destpath}"""
)
+ f"""
RUN set -ex && \\
for line in '[project]' \\
'name = "{fullpath.name}"' \\
@@ -599,12 +718,20 @@ RUN set -ex && \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
done"""
done
# -- End of non-package dependency {fullpath.name} --"""
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
)
local_pkgs_str = os.linesep.join(
f"ADD {relpath} /deps/{fullpath.name}"
for fullpath, relpath in local_deps.real_pkgs.items()
f"""# -- Adding local package {relpath} --
COPY --from={name} . /deps/{name}
# -- End of local package {relpath} --"""
if fullpath in local_deps.additional_contexts
else f"""# -- Adding local package {relpath} --
ADD {relpath} /deps/{name}
# -- End of local package {relpath} --"""
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
)
installs = f"{os.linesep}{os.linesep}".join(
@@ -628,6 +755,9 @@ RUN set -ex && \\
if (auth_config := config.get("auth")) is not None:
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
graphs = config["graphs"]
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(graphs)}'")
@@ -638,15 +768,30 @@ RUN set -ex && \\
"",
installs,
"",
"# -- Installing all local dependencies --",
f"RUN {pip_install} -e /deps/*",
"# -- End of local dependencies install --",
os.linesep.join(env_vars),
"",
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
]
return os.linesep.join(docker_file_contents)
additional_contexts: dict[str, str] = {}
for p in local_deps.additional_contexts:
if p in local_deps.real_pkgs:
name = local_deps.real_pkgs[p][1]
elif p in local_deps.faux_pkgs:
name = f"__outer_{p.name}"
else:
raise RuntimeError(f"Unknown additional context: {p}")
additional_contexts[name] = str(p)
return os.linesep.join(docker_file_contents), additional_contexts
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
def node_config_to_docker(
config_path: pathlib.Path, config: Config, base_image: str
) -> tuple[str, dict[str, str]]:
faux_path = f"/deps/{config_path.parent.name}"
def test_file(file_name):
@@ -684,9 +829,14 @@ ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
if (auth_config := config.get("auth")) is not None:
env_additional_config += f"""
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
"""
if (http_config := config.get("http")) is not None:
env_additional_config += f"""
ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'
"""
return f"""FROM {base_image}:{config['node_version']}
return (
f"""FROM {base_image}:{config['node_version']}
{os.linesep.join(config["dockerfile_lines"])}
@@ -698,10 +848,14 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
WORKDIR {faux_path}
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""",
{},
)
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
def config_to_docker(
config_path: pathlib.Path, config: Config, base_image: str
) -> tuple[str, dict[str, str]]:
if config.get("node_version"):
return node_config_to_docker(config_path, config, base_image)
@@ -737,13 +891,24 @@ def config_to_compose(
else:
watch_str = ""
dockerfile, additional_contexts = config_to_docker(config_path, config, base_image)
additional_contexts_str = "\n".join(
f" - {name}: {path}"
for name, path in additional_contexts.items()
)
if additional_contexts_str:
additional_contexts_str = f"""
additional_contexts:
{additional_contexts_str}"""
return f"""
{textwrap.indent(env_vars_str, " ")}
{env_file_str}
pull_policy: build
build:
context: .
context: .{additional_contexts_str}
dockerfile_inline: |
{textwrap.indent(config_to_docker(config_path, config, base_image), " ")}
{textwrap.indent(dockerfile, " ")}
{watch_str}
"""
+3 -1
View File
@@ -49,7 +49,9 @@ def check_capabilities(runner) -> DockerCapabilities:
raise click.UsageError("Docker not installed") from None
try:
stdout, _ = runner.run(subp_exec("docker", "info", "-f", "json", collect=True))
stdout, _ = runner.run(
subp_exec("docker", "info", "-f", "{{json .}}", collect=True)
)
info = json.loads(stdout)
except (click.exceptions.Exit, json.JSONDecodeError):
raise click.UsageError("Docker not installed or not running") from None
+451 -346
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.71"
version = "0.1.73"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
langgraph-api = { version = ">=0.0.12,<0.1.0", optional = true, python = ">=3.11,<4.0" }
langgraph-api = { version = ">=0.0.26,<0.1.0", optional = true, python = ">=3.11,<4.0" }
python-dotenv = { version = ">=0.8.0", optional = true }
[tool.poetry.group.dev.dependencies]
+16 -5
View File
@@ -40,9 +40,9 @@ def temporary_config_folder(config_content: dict):
def test_prepare_args_and_stdin() -> None:
# this basically serves as an end-to-end test for using config and docker helpers
config_path = pathlib.Path("./langgraph.json")
config_path = pathlib.Path(__file__).parent / "langgraph.json"
config = validate_config(
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
)
port = 8000
debugger_port = 8001
@@ -61,7 +61,7 @@ def test_prepare_args_and_stdin() -> None:
expected_args = [
"--project-directory",
".",
str(pathlib.Path(__file__).parent.absolute()),
"-f",
"custom-docker-compose.yml",
"-f",
@@ -129,18 +129,29 @@ services:
pull_policy: build
build:
context: .
additional_contexts:
- cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/
# -- Adding local package . --
ADD . /deps/cli
# -- End of local package . --
# -- Adding local package ../../.. --
COPY --from=cli_1 . /deps/cli_1
# -- End of local package ../../.. --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
WORKDIR /deps/
WORKDIR /deps/cli
develop:
watch:
- path: langgraph.json
action: rebuild
- path: .
action: rebuild
- path: ../../..
action: rebuild\
"""
assert actual_args == expected_args
@@ -0,0 +1,6 @@
from langgraph.func import entrypoint
@entrypoint()
def graph(state):
return None
+5 -1
View File
@@ -6,10 +6,14 @@
],
"dependencies": [
"langchain_openai",
"starlette",
"."
],
"graphs": {
"agent": "graphs/agent.py:graph"
},
"env": ".env"
"env": ".env",
"http": {
"app": "../../examples/my_app.py:app"
}
}
+141 -7
View File
@@ -32,6 +32,7 @@ def test_validate_config():
"env": {},
"store": None,
"auth": None,
"http": None,
**expected_config,
}
actual_config = validate_config(expected_config)
@@ -50,6 +51,7 @@ def test_validate_config():
"env": env,
"store": None,
"auth": None,
"http": None,
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
@@ -108,6 +110,18 @@ def test_validate_config():
}
)
assert config["python_version"] == "3.12-slim"
with pytest.raises(
ValueError,
match="Invalid http.app format",
):
validate_config(
{
"python_version": "3.12",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
"http": {"app": "../../examples/my_app.py"},
}
)
def test_validate_config_file():
@@ -177,13 +191,27 @@ def test_validate_config_file():
# config_to_docker
def test_config_to_docker_simple():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin = config_to_docker(
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
validate_config(
{
"dependencies": [".", "../../examples/graphs_reqs_a", "../../examples"],
"graphs": graphs,
"http": {"app": "../../examples/my_app.py:app"},
}
),
"langchain/langgraph-api",
)
expected_docker_stdin = """\
FROM langchain/langgraph-api:3.11
# -- Installing local requirements --
COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -r /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
# -- End of local requirements install --
# -- Adding local package ../../examples --
COPY --from=examples . /deps/examples
# -- End of local package ../../examples --
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -193,16 +221,81 @@ RUN set -ex && \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Adding non-package dependency graphs_reqs_a --
COPY --from=__outer_graphs_reqs_a . /deps/__outer_graphs_reqs_a/graphs_reqs_a
RUN set -ex && \\
for line in '[project]' \\
'name = "graphs_reqs_a"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs_reqs_a/pyproject.toml; \\
done
# -- End of non-package dependency graphs_reqs_a --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGGRAPH_HTTP='{"app": "/deps/examples/my_app.py:app"}'
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests\
"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {
"__outer_graphs_reqs_a": str(
(pathlib.Path(__file__).parent / "../../examples/graphs_reqs_a").resolve()
),
"examples": str((pathlib.Path(__file__).parent / "../../examples").resolve()),
}
def test_config_to_docker_outside_path():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config({"dependencies": [".", ".."], "graphs": graphs}),
"langchain/langgraph-api",
)
expected_docker_stdin = """\
FROM langchain/langgraph-api:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Adding non-package dependency tests --
COPY --from=__outer_tests . /deps/__outer_tests/tests
RUN set -ex && \\
for line in '[project]' \\
'name = "tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_tests/pyproject.toml; \\
done
# -- End of non-package dependency tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests\
"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {
"__outer_tests": str(pathlib.Path(__file__).parent.parent.absolute()),
}
def test_config_to_docker_pipconfig():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin = config_to_docker(
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
@@ -216,6 +309,7 @@ def test_config_to_docker_pipconfig():
expected_docker_stdin = """\
FROM langchain/langgraph-api:3.11
ADD pipconfig.txt /pipconfig.txt
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -225,11 +319,15 @@ RUN set -ex && \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests\
"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
def test_config_to_docker_invalid_inputs():
@@ -254,7 +352,7 @@ def test_config_to_docker_invalid_inputs():
def test_config_to_docker_local_deps():
graphs = {"agent": "./graphs/agent.py:graph"}
actual_docker_stdin = config_to_docker(
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
@@ -266,6 +364,7 @@ def test_config_to_docker_local_deps():
)
expected_docker_stdin = """\
FROM langchain/langgraph-api-custom:3.11
# -- Adding non-package dependency graphs --
ADD ./graphs /deps/__outer_graphs/src
RUN set -ex && \\
for line in '[project]' \\
@@ -275,10 +374,14 @@ RUN set -ex && \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
done
# -- End of non-package dependency graphs --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\
"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
def test_config_to_docker_pyproject():
@@ -291,7 +394,7 @@ dependencies = ["langchain"]"""
f.write(pyproject_str)
graphs = {"agent": "./graphs/agent.py:graph"}
actual_docker_stdin = config_to_docker(
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
@@ -303,16 +406,21 @@ dependencies = ["langchain"]"""
)
os.remove(pyproject_path)
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
# -- Adding local package . --
ADD . /deps/unit_tests
# -- End of local package . --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
WORKDIR /deps/unit_tests"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
def test_config_to_docker_end_to_end():
graphs = {"agent": "./graphs/agent.py:graph"}
actual_docker_stdin = config_to_docker(
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
@@ -330,6 +438,7 @@ ARG meow
ARG foo
ADD pipconfig.txt /pipconfig.txt
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain langchain_openai
# -- Adding non-package dependency graphs --
ADD ./graphs/ /deps/__outer_graphs/src
RUN set -ex && \\
for line in '[project]' \\
@@ -339,15 +448,19 @@ RUN set -ex && \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
done
# -- End of non-package dependency graphs --
# -- Installing all local dependencies --
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
# node.js build used for LangGraph Cloud
def test_config_to_docker_nodejs():
graphs = {"agent": "./graphs/agent.js:graph"}
actual_docker_stdin = config_to_docker(
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
@@ -368,6 +481,7 @@ WORKDIR /deps/unit_tests
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
# config_to_compose
@@ -380,6 +494,7 @@ def test_config_to_compose_simple_config():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -389,7 +504,10 @@ def test_config_to_compose_simple_config():
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
@@ -410,6 +528,7 @@ def test_config_to_compose_env_vars():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api-custom:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -419,7 +538,10 @@ def test_config_to_compose_env_vars():
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
@@ -447,6 +569,7 @@ def test_config_to_compose_env_file():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -456,7 +579,10 @@ def test_config_to_compose_env_file():
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
@@ -477,6 +603,7 @@ def test_config_to_compose_watch():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -486,7 +613,10 @@ def test_config_to_compose_watch():
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
@@ -516,6 +646,7 @@ def test_config_to_compose_end_to_end():
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/__outer_unit_tests/unit_tests
RUN set -ex && \\
for line in '[project]' \\
@@ -525,7 +656,10 @@ def test_config_to_compose_end_to_end():
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
+6 -1
View File
@@ -1,5 +1,6 @@
import asyncio
import sys
from typing import Any
from langchain_core.runnables import RunnableConfig
from langchain_core.runnables.config import var_child_runnable_config
@@ -9,6 +10,10 @@ from langgraph.store.base import BaseStore
from langgraph.types import StreamWriter
def _no_op_stream_writer(c: Any) -> None:
pass
def get_config() -> RunnableConfig:
if sys.version_info < (3, 11):
try:
@@ -177,4 +182,4 @@ def get_stream_writer() -> StreamWriter:
```
"""
config = get_config()
return config[CONF][CONFIG_KEY_STREAM_WRITER]
return config[CONF].get(CONFIG_KEY_STREAM_WRITER, _no_op_stream_writer)
+2
View File
@@ -81,6 +81,8 @@ CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
# holds a mutable dict for temporary storage scoped to the current task
CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous")
# holds the previous return value from a stateful Pregel graph.
CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit")
# holds a function that receives tasks from runner, executes them and returns results
# --- Other constants ---
PUSH = sys.intern("__pregel_push")
@@ -57,9 +57,6 @@ def task(
]:
"""Define a LangGraph task using the `task` decorator.
!!! warning "Beta"
The Functional API is currently in beta and is subject to change.
!!! important "Requires python 3.11 or higher for async functions"
The `task` decorator supports both sync and async functions. To use async
functions, ensure that you are using Python 3.11 or higher.
@@ -153,10 +150,6 @@ S = TypeVar("S")
class entrypoint:
"""Define a LangGraph workflow using the `entrypoint` decorator.
!!! warning "Beta"
The Functional API is currently in beta and is subject to change.
### Function signature
The decorated function must accept a **single parameter**, which serves as the input
+7 -3
View File
@@ -286,7 +286,7 @@ class StateGraph(Graph):
Will take the name of the function/runnable as the node name.
Args:
node (Union[str, RunnableLike)]: The function or runnable this node will run.
node (Union[str, RunnableLike]): The function or runnable this node will run.
action (Optional[RunnableLike]): The action associated with the node. (default: None)
metadata (Optional[dict[str, Any]]): The metadata associated with the node. (default: None)
input (Optional[Type[Any]]): The input schema for the node. (default: the graph's input schema)
@@ -675,7 +675,9 @@ class CompiledStateGraph(CompiledGraph):
elif isinstance(input, Command):
if input.graph == Command.PARENT:
return None
return input._update_as_tuples()
return [
(k, v) for k, v in input._update_as_tuples() if k in output_keys
]
elif (
isinstance(input, (list, tuple))
and input
@@ -686,7 +688,9 @@ class CompiledStateGraph(CompiledGraph):
if isinstance(i, Command):
if i.graph == Command.PARENT:
continue
updates.extend(i._update_as_tuples())
updates.extend(
(k, v) for k, v in i._update_as_tuples() if k in output_keys
)
else:
updates.extend(_get_updates(i) or ())
return updates
@@ -12,7 +12,11 @@ from typing import (
cast,
)
from langchain_core.language_models import BaseChatModel, LanguageModelLike
from langchain_core.language_models import (
BaseChatModel,
LanguageModelInput,
LanguageModelLike,
)
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
from langchain_core.runnables import (
Runnable,
@@ -52,6 +56,10 @@ class AgentState(TypedDict):
remaining_steps: RemainingSteps
class AgentStateWithStructuredResponse(AgentState):
"""The state of the agent with a structured response."""
structured_response: StructuredResponse
@@ -63,15 +71,15 @@ PROMPT_RUNNABLE_NAME = "Prompt"
MessagesModifier = Union[
SystemMessage,
str,
Callable[[Sequence[BaseMessage]], Sequence[BaseMessage]],
Runnable[Sequence[BaseMessage], Sequence[BaseMessage]],
Callable[[Sequence[BaseMessage]], LanguageModelInput],
Runnable[Sequence[BaseMessage], LanguageModelInput],
]
Prompt = Union[
SystemMessage,
str,
Callable[[StateSchema], Sequence[BaseMessage]],
Runnable[StateSchema, Sequence[BaseMessage]],
Callable[[StateSchema], LanguageModelInput],
Runnable[StateSchema, LanguageModelInput],
]
@@ -238,11 +246,12 @@ def create_react_agent(
model: Union[str, LanguageModelLike],
tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode],
*,
state_schema: Optional[StateSchemaType] = None,
prompt: Optional[Prompt] = None,
response_format: Optional[
Union[StructuredResponseSchema, tuple[str, StructuredResponseSchema]]
] = None,
state_schema: Optional[StateSchemaType] = None,
config_schema: Optional[Type[Any]] = None,
checkpointer: Optional[Checkpointer] = None,
store: Optional[BaseStore] = None,
interrupt_before: Optional[list[str]] = None,
@@ -257,9 +266,6 @@ def create_react_agent(
model: The `LangChain` chat model that supports tool calling.
tools: A list of tools, a ToolExecutor, or a ToolNode instance.
If an empty list is provided, the agent will consist of a single LLM node without tool calling.
state_schema: An optional state schema that defines graph state.
Must have `messages` and `is_last_step` keys.
Defaults to `AgentState` that defines those two keys.
prompt: An optional prompt for the LLM. Can take a few different forms:
- str: This is converted to a SystemMessage and added to the beginning of the list of messages in state["messages"].
@@ -288,6 +294,11 @@ def create_react_agent(
!!! Note
The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.
This is not the only strategy to get structured responses, see more options in [this guide](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/).
state_schema: An optional state schema that defines graph state.
Must have `messages` and `is_last_step` keys.
Defaults to `AgentState` that defines those two keys.
config_schema: An optional schema for configuration.
Use this to expose configurable parameters via agent.config_specs.
checkpointer: An optional checkpoint saver object. This is used for persisting
the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation).
store: An optional store object. This is used for persisting data
@@ -599,6 +610,13 @@ def create_react_agent(
if missing_keys := required_keys - set(state_schema.__annotations__):
raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
if state_schema is None:
state_schema = (
AgentStateWithStructuredResponse
if response_format is not None
else AgentState
)
if isinstance(tools, ToolExecutor):
tool_classes: Sequence[BaseTool] = tools.tools
tool_node = ToolNode(tool_classes)
@@ -748,7 +766,7 @@ def create_react_agent(
if not tool_calling_enabled:
# Define a new graph
workflow = StateGraph(state_schema or AgentState)
workflow = StateGraph(state_schema, config_schema=config_schema)
workflow.add_node("agent", RunnableCallable(call_model, acall_model))
workflow.set_entry_point("agent")
if response_format is not None:
@@ -788,7 +806,7 @@ def create_react_agent(
return [Send("tools", [tool_call]) for tool_call in tool_calls]
# Define a new graph
workflow = StateGraph(state_schema or AgentState)
workflow = StateGraph(state_schema or AgentState, config_schema=config_schema)
# Define the two nodes we will cycle between
workflow.add_node("agent", RunnableCallable(call_model, acall_model))
+221 -35
View File
@@ -59,6 +59,7 @@ from langgraph.constants import (
CONFIG_KEY_NODE_FINISHED,
CONFIG_KEY_READ,
CONFIG_KEY_RESUMING,
CONFIG_KEY_RUNNER_SUBMIT,
CONFIG_KEY_SEND,
CONFIG_KEY_STORE,
CONFIG_KEY_STREAM,
@@ -199,10 +200,42 @@ class Channel:
class Pregel(PregelProtocol):
"""Pregel manages the runtime behavior for LangGraph applications.
## Overview
Pregel combines [**actors**](https://en.wikipedia.org/wiki/Actor_model)
and **channels** into a single application.
**Actors** read data from channels and write data to channels.
Pregel organizes the execution of the application into multiple steps,
following the **Pregel Algorithm**/**Bulk Synchronous Parallel** model.
Each step consists of three phases:
- **Plan**: Determine which **actors** to execute in this step. For example,
in the first step, select the **actors** that subscribe to the special
**input** channels; in subsequent steps,
select the **actors** that subscribe to channels updated in the previous step.
- **Execution**: Execute all selected **actors** in parallel,
until all complete, or one fails, or a timeout is reached. During this
phase, channel updates are invisible to actors until the next step.
- **Update**: Update the channels with the values written by the **actors**
in this step.
Repeat until no **actors** are selected for execution, or a maximum number of
steps is reached.
## Actors
An **actor** is a [PregelNode][langgraph.pregel.read.PregelNode].
It subscribes to channels, reads data from them, and writes data to them.
It can be thought of as an **actor** in the Pregel algorithm.
[PregelNodes][langgraph.pregel.read.PregelNode] implement LangChain's
Runnable interface.
## Channels
Channels are used to communicate between chains. Each channel has a value type,
an update type, and an update function which takes a sequence of updates and
Channels are used to communicate between actors (PregelNodes).
Each channel has a value type, an update type, and an update function which
takes a sequence of updates and
modifies the stored value. Channels can be used to send data from one chain to
another, or to send data from a chain to itself in a future step. LangGraph
provides a number of built-in channels:
@@ -212,7 +245,7 @@ class Pregel(PregelProtocol):
- `LastValue`: The default channel, stores the last value sent to the channel,
useful for input and output values, or for sending data from one step to the next
- `Topic`: A configurable PubSub Topic, useful for sending multiple values
between chains, or for accumulating output. Can be configured to deduplicate
between *actors*, or for accumulating output. Can be configured to deduplicate
values, and/or to accumulate values over the course of multiple steps.
### Advanced channels: Context and BinaryOperatorAggregate
@@ -225,48 +258,201 @@ class Pregel(PregelProtocol):
sent to the channel, useful for computing aggregates over multiple steps. eg.
`total = BinaryOperatorAggregate(int, operator.add)`
## Chains
## Examples
Chains are LCEL Runnables which subscribe to one or more channels, and write to
one or more channels. Any valid LCEL expression can be used as a chain. Chains
can be combined into a Pregel application, which coordinates the execution of the
chains across multiple steps.
Most users will interact with Pregel via a
[StateGraph (Graph API)][langgraph.graph.StateGraph] or via an
[entrypoint (Functional API)][langgraph.func.entrypoint].
## Pregel
However, for **advanced** use cases, Pregel can be used directly. If you're
not sure whether you need to use Pregel directly, then the answer is probably no
you should use the Graph API or Functional API instead. These are higher-level
interfaces that will compile down to Pregel under the hood.
Pregel combines multiple chains (or actors) into a single application. It
coordinates the execution of the chains across multiple steps, following the
Pregel/Bulk Synchronous Parallel model. Each step consists of three phases:
Here are some examples to give you a sense of how it works:
- **Plan**: Determine which chains to execute in this step, ie. the chains that
subscribe to channels updated in the previous step (or, in the first step,
chains that subscribe to input channels)
- **Execution**: Execute those chains in parallel, until all complete, or one fails,
or a timeout is reached. Any channel updates are invisible to other
chains until the next step.
- **Update**: Update the channels with the values written by the
chains in this step.
Repeat until no chains are planned for execution, or a maximum number of steps
is reached.
Example:
Example: Single node application
```python
from langgraph import Channel, Pregel
from langgraph.channels import EphemeralValue
from langgraph.pregel import Pregel, Channel, ChannelWriteEntry
grow_value = (
Channel.subscribe_to("value")
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| Channel.write_to(value=lambda x: x if len(x) < 10 else None)
| Channel.write_to("b")
)
app = Pregel(
chains={"grow_value": grow_value},
input="value",
output="value",
nodes={"node1": node1},
channels={
"a": EphemeralValue(str),
"b": EphemeralValue(str),
},
input_channels=["a"],
output_channels=["b"],
)
assert app.invoke("a") == "aaaaaaaa"
app.invoke({"a": "foo"})
```
```con
{'b': 'foofoo'}
```
Example: Using multiple nodes and multiple output channels
```python
from langgraph.channels import LastValue, EphemeralValue
from langgraph.pregel import Pregel, Channel, ChannelWriteEntry
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| Channel.write_to("b")
)
node2 = (
Channel.subscribe_to("b")
| (lambda x: x + x)
| Channel.write_to("c")
)
app = Pregel(
nodes={"node1": node1, "node2": node2},
channels={
"a": EphemeralValue(str),
"b": LastValue(str),
"c": EphemeralValue(str),
},
input_channels=["a"],
output_channels=["b", "c"],
)
app.invoke({"a": "foo"})
```
```con
{'b': 'foofoo', 'c': 'foofoofoofoo'}
```
Example: Using a Topic channel
```python
from langgraph.channels import LastValue, EphemeralValue, Topic
from langgraph.pregel import Pregel, Channel, ChannelWriteEntry
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| {
"b": Channel.write_to("b"),
"c": Channel.write_to("c")
}
)
node2 = (
Channel.subscribe_to("b")
| (lambda x: x + x)
| {
"c": Channel.write_to("c"),
}
)
app = Pregel(
nodes={"node1": node1, "node2": node2},
channels={
"a": EphemeralValue(str),
"b": EphemeralValue(str),
"c": Topic(str, accumulate=True),
},
input_channels=["a"],
output_channels=["c"],
)
app.invoke({"a": "foo"})
```
```pycon
{'c': ['foofoo', 'foofoofoofoo']}
```
Example: Using a BinaryOperatorAggregate channel
```python
from langgraph.channels import EphemeralValue, BinaryOperatorAggregate
from langgraph.pregel import Pregel, Channel
node1 = (
Channel.subscribe_to("a")
| (lambda x: x + x)
| {
"b": Channel.write_to("b"),
"c": Channel.write_to("c")
}
)
node2 = (
Channel.subscribe_to("b")
| (lambda x: x + x)
| {
"c": Channel.write_to("c"),
}
)
def reducer(current, update):
if current:
return current + " | " + "update"
else:
return update
app = Pregel(
nodes={"node1": node1, "node2": node2},
channels={
"a": EphemeralValue(str),
"b": EphemeralValue(str),
"c": BinaryOperatorAggregate(str, operator=reducer),
},
input_channels=["a"],
output_channels=["c"]
)
app.invoke({"a": "foo"})
```
```con
{'c': 'foofoo | foofoofoofoo'}
```
Example: Introducing a cycle
This example demonstrates how to introduce a cycle in the graph, by having
a chain write to a channel it subscribes to. Execution will continue
until a None value is written to the channel.
```python
from langgraph.channels import EphemeralValue
from langgraph.pregel import Pregel, Channel, ChannelWrite, ChannelWriteEntry
example_node = (
Channel.subscribe_to("value")
| (lambda x: x + x if len(x) < 10 else None)
| ChannelWrite(writes=[ChannelWriteEntry(channel="value", skip_none=True)])
)
app = Pregel(
nodes={"example_node": example_node},
channels={
"value": EphemeralValue(str),
},
input_channels=["value"],
output_channels=["value"]
)
app.invoke({"value": "a"})
```
```con
{'value': 'aaaaaaaaaaaaaaaa'}
```
"""
@@ -1755,7 +1941,7 @@ class Pregel(PregelProtocol):
) as loop:
# create runner
runner = PregelRunner(
submit=loop.submit,
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
put_writes=loop.put_writes,
schedule_task=loop.accept_push,
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
@@ -2047,7 +2233,7 @@ class Pregel(PregelProtocol):
) as loop:
# create runner
runner = PregelRunner(
submit=loop.submit,
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
put_writes=loop.put_writes,
schedule_task=loop.accept_push,
use_astream=do_stream is not None,
+10 -4
View File
@@ -888,7 +888,11 @@ class SyncPregelLoop(PregelLoop, ContextManager):
)
def _update_mv(self, key: str, values: Sequence[Any]) -> None:
return self.submit(cast(WritableManagedValue, self.managed[key]).update, values)
managed_value = self.managed.get(key)
if managed_value is None:
return
return self.submit(cast(WritableManagedValue, managed_value).update, values)
# context manager
@@ -1023,9 +1027,11 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
)
def _update_mv(self, key: str, values: Sequence[Any]) -> None:
return self.submit(
cast(WritableManagedValue, self.managed[key]).aupdate, values
)
managed_value = self.managed.get(key)
if managed_value is None:
return
return self.submit(cast(WritableManagedValue, managed_value).aupdate, values)
# context manager
+5 -1
View File
@@ -323,11 +323,15 @@ class RemoteGraph(PregelProtocol):
if k not in reserved_configurable_keys and not k.startswith("__pregel_")
}
return {
sanitized: RunnableConfig = {
"tags": config.get("tags") or [],
"metadata": config.get("metadata") or {},
"configurable": new_configurable,
}
if "recursion_limit" in config:
sanitized["recursion_limit"] = config["recursion_limit"]
return sanitized
def get_state(
self, config: RunnableConfig, *, subgraphs: bool = False
+44 -4
View File
@@ -13,6 +13,7 @@ from typing import (
Coroutine,
Iterator,
Optional,
Protocol,
Sequence,
Tuple,
Union,
@@ -35,7 +36,7 @@ from langchain_core.runnables.config import (
)
from langchain_core.runnables.utils import Input, Output
from langchain_core.tracers._streaming import _StreamingCallbackHandler
from typing_extensions import Concatenate, ParamSpec, TypeGuard
from typing_extensions import TypeGuard
from langgraph.constants import (
CONF,
@@ -132,12 +133,51 @@ Each tuple contains:
VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
P = ParamSpec("P") # to handle injected kwargs like `writer` / `store`
class _RunnableWithWriter(Protocol[Input, Output]):
def __call__(self, state: Input, *, writer: StreamWriter) -> Output: ...
class _RunnableWithStore(Protocol[Input, Output]):
def __call__(self, state: Input, *, store: BaseStore) -> Output: ...
class _RunnableWithWriterStore(Protocol[Input, Output]):
def __call__(
self, state: Input, *, writer: StreamWriter, store: BaseStore
) -> Output: ...
class _RunnableWithConfigWriter(Protocol[Input, Output]):
def __call__(
self, state: Input, *, config: RunnableConfig, writer: StreamWriter
) -> Output: ...
class _RunnableWithConfigStore(Protocol[Input, Output]):
def __call__(
self, state: Input, *, config: RunnableConfig, store: BaseStore
) -> Output: ...
class _RunnableWithConfigWriterStore(Protocol[Input, Output]):
def __call__(
self,
state: Input,
*,
config: RunnableConfig,
writer: StreamWriter,
store: BaseStore,
) -> Output: ...
RunnableLike = Union[
LCRunnableLike,
Callable[Concatenate[Input, P], Output],
Callable[Concatenate[Input, P], Awaitable[Output]],
_RunnableWithWriter[Input, Output],
_RunnableWithStore[Input, Output],
_RunnableWithWriterStore[Input, Output],
_RunnableWithConfigWriter[Input, Output],
_RunnableWithConfigStore[Input, Output],
_RunnableWithConfigWriterStore[Input, Output],
]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.72"
version = "0.2.74"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -2832,10 +2832,10 @@
'''
# ---
# name: test_prebuilt_tool_chat
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}, "BaseModel": {"properties": {}, "title": "BaseModel", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "structured_response": {"anyOf": [{"type": "object"}, {"$ref": "#/$defs/BaseModel"}], "title": "Structured Response"}}, "required": ["messages", "structured_response"], "title": "LangGraphInput", "type": "object"}'
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphInput", "type": "object"}'
# ---
# name: test_prebuilt_tool_chat.1
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}, "BaseModel": {"properties": {}, "title": "BaseModel", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "structured_response": {"anyOf": [{"type": "object"}, {"$ref": "#/$defs/BaseModel"}], "title": "Structured Response"}}, "required": ["messages", "structured_response"], "title": "LangGraphOutput", "type": "object"}'
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphOutput", "type": "object"}'
# ---
# name: test_prebuilt_tool_chat.2
'''
+93
View File
@@ -54,6 +54,7 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
)
from langgraph.checkpoint.memory import InMemorySaver, MemorySaver
from langgraph.config import get_stream_writer
from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, START
from langgraph.errors import InvalidUpdateError
from langgraph.func import entrypoint, task
@@ -6259,6 +6260,67 @@ def test_merging_updates_command_parent():
]
def test_merging_non_overlapping_updates_command_parent():
# simple reducer
def append_unique(left, right):
combined = list(left)
for item in right:
if item in combined:
continue
else:
combined.append(item)
return combined
class State(TypedDict):
foo: Annotated[list, append_unique]
# Define subgraph
def subgraph_node_1(state: State):
return Command(
goto="subgraph_node_2",
update={
"foo": ["bar"],
"bar": ["subgraph_node_1"],
},
)
def subgraph_node_2(state: State):
return Command(
goto="node_3",
update={"bar": ["subgraph_node_2"]},
graph=Command.PARENT,
)
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
# Define main graph
def node_1(state: State):
return Command(
goto="node_2",
update={"foo": ["foo"]},
)
def node_3(state: State, store):
return Command(
update={"foo": ["baz"]},
)
main_builder = StateGraph(State)
main_builder.add_node("node_1", node_1)
main_builder.add_node("node_2", subgraph_builder.compile())
main_builder.add_node("node_3", node_3)
main_builder.add_edge(START, "node_1")
main_builder.add_edge("node_2", "node_3")
main_graph = main_builder.compile()
assert main_graph.invoke({"foo": []}) == {
"foo": ["foo", "bar", "baz"],
}
def test_entrypoint_output_schema_with_return_and_save() -> None:
"""Test output schema inference with entrypoint.final."""
@@ -6495,3 +6557,34 @@ def test_pydantic_none_state_update() -> None:
graph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile()
assert graph.invoke({"foo": ""}) == {"foo": None}
def test_get_stream_writer() -> None:
class State(TypedDict):
foo: str
def my_node(state):
writer = get_stream_writer()
writer("custom!")
return state
graph = StateGraph(State).add_node(my_node).add_edge(START, "my_node").compile()
assert list(graph.stream({"foo": "bar"}, stream_mode="custom")) == ["custom!"]
assert list(graph.stream({"foo": "bar"}, stream_mode="values")) == [
{"foo": "bar"},
{"foo": "bar"},
]
assert list(graph.stream({"foo": "bar"}, stream_mode=["custom", "updates"])) == [
(
"custom",
"custom!",
),
(
"updates",
{
"my_node": {
"foo": "bar",
},
},
),
]
+1 -1
View File
@@ -13,4 +13,4 @@ react.d.cts
node_modules
dist
.yarn
docs
docs
+1
View File
@@ -15,5 +15,6 @@ export const config = {
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
cjsDestination: "./dist",
additionalGitignorePaths: ["docs"],
abs,
};
+9 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.42",
"version": "0.0.46",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
@@ -43,6 +43,14 @@
"react": "^18 || ^19",
"@langchain/core": ">=0.2.31 <0.4.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"@langchain/core": {
"optional": true
}
},
"exports": {
".": {
"types": {
+132 -22
View File
@@ -9,7 +9,13 @@ import type {
OnCompletionBehavior,
} from "../types.js";
import type { Message } from "../types.messages.js";
import type { Checkpoint, Config, Metadata, ThreadState } from "../schema.js";
import type {
Checkpoint,
Config,
Interrupt,
Metadata,
ThreadState,
} from "../schema.js";
import type {
CustomStreamEvent,
DebugStreamEvent,
@@ -37,6 +43,7 @@ import {
type BaseMessage,
coerceMessageLikeToMessage,
convertToChunk,
isBaseMessageChunk,
} from "@langchain/core/messages";
class StreamError extends Error {
@@ -54,21 +61,53 @@ class StreamError extends Error {
}
}
function tryConvertToChunk(message: BaseMessage): BaseMessageChunk | null {
try {
return convertToChunk(message);
} catch {
return null;
}
}
class MessageTupleManager {
chunks: Record<string, { chunk?: BaseMessageChunk; index?: number }> = {};
chunks: Record<
string,
{ chunk?: BaseMessageChunk | BaseMessage; index?: number }
> = {};
constructor() {
this.chunks = {};
}
add(serialized: Message): string | null {
const chunk = convertToChunk(coerceMessageLikeToMessage(serialized));
// TODO: this is sometimes sent from the API
// figure out how to prevent this or move this to LC.js
if (serialized.type.endsWith("MessageChunk")) {
serialized.type = serialized.type
.slice(0, -"MessageChunk".length)
.toLowerCase() as Message["type"];
}
const id = chunk.id;
if (!id) return null;
const message = coerceMessageLikeToMessage(serialized);
const chunk = tryConvertToChunk(message);
const id = (chunk ?? message).id;
if (!id) {
console.warn(
"No message ID found for chunk, ignoring in state",
serialized,
);
return null;
}
this.chunks[id] ??= {};
this.chunks[id].chunk = this.chunks[id]?.chunk?.concat(chunk) ?? chunk;
if (chunk) {
const prev = this.chunks[id].chunk;
this.chunks[id].chunk =
(isBaseMessageChunk(prev) ? prev : null)?.concat(chunk) ?? chunk;
} else {
this.chunks[id].chunk = message;
}
return id;
}
@@ -334,10 +373,41 @@ const useControllableThreadId = (options?: {
return [options.threadId, onThreadId];
};
type BagTemplate = {
ConfigurableType?: Record<string, unknown>;
InterruptType?: unknown;
CustomEventType?: unknown;
UpdateType?: unknown;
};
type GetUpdateType<
Bag extends BagTemplate,
StateType extends Record<string, unknown>,
> = Bag extends { UpdateType: unknown }
? Bag["UpdateType"]
: Partial<StateType>;
type GetConfigurableType<Bag extends BagTemplate> = Bag extends {
ConfigurableType: Record<string, unknown>;
}
? Bag["ConfigurableType"]
: Record<string, unknown>;
type GetInterruptType<Bag extends BagTemplate> = Bag extends {
InterruptType: unknown;
}
? Bag["InterruptType"]
: unknown;
type GetCustomEventType<Bag extends BagTemplate> = Bag extends {
CustomEventType: unknown;
}
? Bag["CustomEventType"]
: unknown;
interface UseStreamOptions<
StateType extends Record<string, unknown> = Record<string, unknown>,
UpdateType extends Record<string, unknown> = Partial<StateType>,
CustomType = unknown,
Bag extends BagTemplate = BagTemplate,
> {
/**
* The ID of the assistant to use.
@@ -375,12 +445,16 @@ interface UseStreamOptions<
/**
* Callback that is called when an update event is received.
*/
onUpdateEvent?: (data: UpdatesStreamEvent<UpdateType>["data"]) => void;
onUpdateEvent?: (
data: UpdatesStreamEvent<GetUpdateType<Bag, StateType>>["data"],
) => void;
/**
* Callback that is called when a custom event is received.
*/
onCustomEvent?: (data: CustomStreamEvent<CustomType>["data"]) => void;
onCustomEvent?: (
data: CustomStreamEvent<GetCustomEventType<Bag>>["data"],
) => void;
/**
* Callback that is called when a metadata event is received.
@@ -400,7 +474,7 @@ interface UseStreamOptions<
interface UseStream<
StateType extends Record<string, unknown> = Record<string, unknown>,
UpdateType extends Record<string, unknown> = Partial<StateType>,
Bag extends BagTemplate = BagTemplate,
> {
/**
* The current values of the thread.
@@ -425,7 +499,10 @@ interface UseStream<
/**
* Create and stream a run to the thread.
*/
submit: (values: UpdateType, options?: SubmitOptions<StateType>) => void;
submit: (
values: GetUpdateType<Bag, StateType> | null | undefined,
options?: SubmitOptions<StateType, GetConfigurableType<Bag>>,
) => void;
/**
* The current branch of the thread.
@@ -448,6 +525,11 @@ interface UseStream<
*/
experimental_branchTree: Sequence<StateType>;
/**
* Get the interrupt value for the stream if interrupted.
*/
interrupt: Interrupt<GetInterruptType<Bag>> | undefined;
/**
* Messages inferred from the thread.
* Will automatically update with incoming message chunks.
@@ -468,10 +550,14 @@ interface UseStream<
) => MessageMetadata<StateType> | undefined;
}
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
Config & { configurable?: ConfigurableType };
interface SubmitOptions<
StateType extends Record<string, unknown> = Record<string, unknown>,
ConfigurableType extends Record<string, unknown> = Record<string, unknown>,
> {
config?: Config;
config?: ConfigWithConfigurable<ConfigurableType>;
checkpoint?: Omit<Checkpoint, "thread_id"> | null;
command?: Command;
interruptBefore?: "*" | string[];
@@ -489,11 +575,18 @@ interface SubmitOptions<
export function useStream<
StateType extends Record<string, unknown> = Record<string, unknown>,
UpdateType extends Record<string, unknown> = Partial<StateType>,
CustomType = unknown,
>(
options: UseStreamOptions<StateType, UpdateType, CustomType>,
): UseStream<StateType, UpdateType> {
Bag extends {
ConfigurableType?: Record<string, unknown>;
InterruptType?: unknown;
CustomEventType?: unknown;
UpdateType?: unknown;
} = BagTemplate,
>(options: UseStreamOptions<StateType, Bag>): UseStream<StateType, Bag> {
type UpdateType = GetUpdateType<Bag, StateType>;
type CustomType = GetCustomEventType<Bag>;
type InterruptType = GetInterruptType<Bag>;
type ConfigurableType = GetConfigurableType<Bag>;
type EventStreamEvent =
| ValuesStreamEvent<StateType>
| UpdatesStreamEvent<UpdateType>
@@ -527,7 +620,7 @@ export function useStream<
const trackStreamModeRef = useRef<
Array<"values" | "updates" | "events" | "custom" | "messages-tuple">
>(["values", "messages-tuple"]);
>([]);
const trackStreamMode = useCallback(
(mode: Exclude<StreamMode, "debug" | "messages">) => {
@@ -639,8 +732,8 @@ export function useStream<
}, []);
const submit = async (
values: UpdateType | undefined,
submitOptions?: SubmitOptions<StateType>,
values: UpdateType | null | undefined,
submitOptions?: SubmitOptions<StateType, ConfigurableType>,
) => {
try {
setIsLoading(true);
@@ -763,6 +856,7 @@ export function useStream<
(error.name === "AbortError" || error.name === "TimeoutError")
)
) {
console.error(error);
setStreamError(error);
onError?.(error);
}
@@ -776,7 +870,7 @@ export function useStream<
}
};
const error = isLoading ? streamError : historyError;
const error = streamError ?? historyError;
const values = streamValues ?? historyValues;
return {
@@ -797,6 +891,22 @@ export function useStream<
history: flatHistory,
experimental_branchTree: rootSequence,
get interrupt() {
// Don't show the interrupt if the stream is loading
if (isLoading) return undefined;
const interrupts = threadHead?.tasks?.at(-1)?.interrupts;
if (interrupts == null || interrupts.length === 0) {
// check if there's a next task present
const next = threadHead?.next ?? [];
if (!next.length || error != null) return undefined;
return { when: "breakpoint" };
}
// Return only the current interrupt
return interrupts.at(-1) as Interrupt<InterruptType> | undefined;
},
get messages() {
trackStreamMode("messages-tuple");
return getMessages(values);
+4 -4
View File
@@ -141,10 +141,10 @@ export interface AssistantGraph {
/**
* An interrupt thrown inside a thread.
*/
export interface Interrupt {
value: unknown;
when: "during";
resumable: boolean;
export interface Interrupt<TValue = unknown> {
value?: TValue;
when: "during" | (string & {});
resumable?: boolean;
ns?: string[];
}
+39 -24
View File
@@ -147,15 +147,21 @@ def get_client(
# example usage: client.<model>.<method_name>()
assistants = await client.assistants.get(assistant_id="some_uuid")
"""
transport: Optional[httpx.AsyncBaseTransport] = None
if url is None:
try:
from langgraph_api.server import app # type: ignore
if os.environ.get("__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT") == "true":
transport = httpx.ASGITransport(app=None, root_path="/noauth")
_registered_transports.append(transport)
url = "http://api"
transport = httpx.ASGITransport(app, root_path="/noauth")
except Exception:
url = "http://localhost:8123"
else:
try:
from langgraph_api.server import app # type: ignore
url = "http://api"
transport = httpx.ASGITransport(app, root_path="/noauth")
except Exception:
url = "http://localhost:8123"
if transport is None:
transport = httpx.AsyncHTTPTransport(retries=5)
@@ -1318,9 +1324,9 @@ class RunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": {k: v for k, v in command.items() if v is not None}
if command
else None,
"command": (
{k: v for k, v in command.items() if v is not None} if command else None
),
"config": config,
"metadata": metadata,
"stream_mode": stream_mode,
@@ -1505,9 +1511,9 @@ class RunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": {k: v for k, v in command.items() if v is not None}
if command
else None,
"command": (
{k: v for k, v in command.items() if v is not None} if command else None
),
"stream_mode": stream_mode,
"stream_subgraphs": stream_subgraphs,
"config": config,
@@ -1676,9 +1682,9 @@ class RunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": {k: v for k, v in command.items() if v is not None}
if command
else None,
"command": (
{k: v for k, v in command.items() if v is not None} if command else None
),
"config": config,
"metadata": metadata,
"assistant_id": assistant_id,
@@ -3483,9 +3489,9 @@ class SyncRunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": {k: v for k, v in command.items() if v is not None}
if command
else None,
"command": (
{k: v for k, v in command.items() if v is not None} if command else None
),
"config": config,
"metadata": metadata,
"stream_mode": stream_mode,
@@ -3670,9 +3676,9 @@ class SyncRunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": {k: v for k, v in command.items() if v is not None}
if command
else None,
"command": (
{k: v for k, v in command.items() if v is not None} if command else None
),
"stream_mode": stream_mode,
"stream_subgraphs": stream_subgraphs,
"config": config,
@@ -3838,9 +3844,9 @@ class SyncRunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": {k: v for k, v in command.items() if v is not None}
if command
else None,
"command": (
{k: v for k, v in command.items() if v is not None} if command else None
),
"config": config,
"metadata": metadata,
"assistant_id": assistant_id,
@@ -4444,3 +4450,12 @@ class SyncStoreClient:
def _provided_vals(d: dict):
return {k: v for k, v in d.items() if v is not None}
_registered_transports: list[httpx.ASGITransport] = []
# Do not move; this is used in the server.
def configure_loopback_transports(app: Any) -> None:
for transport in _registered_transports:
transport.app = app
+31
View File
@@ -378,11 +378,42 @@ class StreamPart(NamedTuple):
class Send(TypedDict):
"""Represents a message to be sent to a specific node in the graph.
This type is used to explicitly send messages to nodes in the graph, typically
used within Command objects to control graph execution flow.
"""
node: str
"""The name of the target node to send the message to."""
input: Optional[dict[str, Any]]
"""Optional dictionary containing the input data to be passed to the node.
If None, the node will be called with no input."""
class Command(TypedDict, total=False):
"""Represents one or more commands to control graph execution flow and state.
This type defines the control commands that can be returned by nodes to influence
graph execution. It lets you navigate to other nodes, update graph state,
and resume from interruptions.
"""
goto: Union[Send, str, Sequence[Union[Send, str]]]
"""Specifies where execution should continue. Can be:
- A string node name to navigate to
- A Send object to execute a node with specific input
- A sequence of node names or Send objects to execute in order
"""
update: Union[dict[str, Any], Sequence[Tuple[str, Any]]]
"""Updates to apply to the graph's state. Can be:
- A dictionary of state updates to merge
- A sequence of (key, value) tuples for ordered updates
"""
resume: Any
"""Value to resume execution with after an interruption.
Used in conjunction with interrupt() to implement control flow.
"""
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.51"
version = "0.1.53"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"