langgraph-api 0.2.26__tar.gz → 0.2.28__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- langgraph_api-0.2.28/.dockerignore +14 -0
- langgraph_api-0.2.28/.gitignore +5 -0
- langgraph_api-0.2.28/Makefile +114 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/PKG-INFO +24 -30
- langgraph_api-0.2.28/benchmark/weather.js +75 -0
- langgraph_api-0.2.28/constraints.txt +19 -0
- langgraph_api-0.2.28/forbidden.txt +2 -0
- langgraph_api-0.2.28/healthcheck.py +18 -0
- langgraph_api-0.2.28/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/assistants.py +4 -4
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/store.py +10 -6
- langgraph_api-0.2.28/langgraph_api/asgi_transport.py +171 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/asyncio.py +17 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/config.py +1 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/graph.py +28 -5
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/remote.py +16 -11
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/metadata.py +28 -16
- langgraph_api-0.2.28/langgraph_api/store.py +127 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/stream.py +17 -7
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/worker.py +1 -1
- langgraph_api-0.2.28/pyproject.toml +102 -0
- langgraph_api-0.2.28/scripts/create_license.py +60 -0
- langgraph_api-0.2.28/uv.lock +1340 -0
- langgraph_api-0.2.26/langgraph_api/__init__.py +0 -1
- langgraph_api-0.2.26/langgraph_api/js/tests/api.test.mts +0 -2194
- langgraph_api-0.2.26/langgraph_api/js/tests/auth.test.mts +0 -648
- langgraph_api-0.2.26/langgraph_api/js/tests/compose-postgres.auth.yml +0 -59
- langgraph_api-0.2.26/langgraph_api/js/tests/compose-postgres.yml +0 -59
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/.gitignore +0 -1
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/agent.css +0 -1
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/agent.mts +0 -187
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/agent.ui.tsx +0 -10
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/agent_simple.mts +0 -105
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/auth.mts +0 -106
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/command.mts +0 -48
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/delay.mts +0 -30
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/dynamic.mts +0 -24
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/error.mts +0 -17
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/http.mts +0 -76
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/langgraph.json +0 -11
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/nested.mts +0 -44
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/package.json +0 -13
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/weather.mts +0 -57
- langgraph_api-0.2.26/langgraph_api/js/tests/graphs/yarn.lock +0 -242
- langgraph_api-0.2.26/langgraph_api/js/tests/utils.mts +0 -17
- langgraph_api-0.2.26/pyproject.toml +0 -98
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/LICENSE +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/README.md +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/cli.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/command.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/http.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/route.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/server.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/state.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/utils.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/logging.json +0 -0
- {langgraph_api-0.2.26 → langgraph_api-0.2.28}/openapi.json +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
.PHONY: build release lint format test test_watch start start-inmem start-inmem-license-oss start check-version
|
|
2
|
+
|
|
3
|
+
# lint commands
|
|
4
|
+
|
|
5
|
+
lint:
|
|
6
|
+
uv run ruff check .
|
|
7
|
+
uv run ruff format . --diff
|
|
8
|
+
|
|
9
|
+
format:
|
|
10
|
+
uv run ruff check --fix .
|
|
11
|
+
uv run ruff format .
|
|
12
|
+
|
|
13
|
+
# test commands
|
|
14
|
+
|
|
15
|
+
TEST ?= "tests/integration_tests/"
|
|
16
|
+
AUTH_TEST ?= "tests/integration_tests/test_custom_auth.py"
|
|
17
|
+
LANGGRAPH_HTTP ?= {"disable_mcp": false}
|
|
18
|
+
LANGGRAPH_AES_KEY ?= '1234567890123456'
|
|
19
|
+
|
|
20
|
+
ifeq ($(LANGGRAPH_HTTP),fastapi)
|
|
21
|
+
HTTP_CONFIG := {"app": "./tests/graphs/my_router.py:app", "disable_mcp": false, "mount_prefix": "/my-cool/api"}
|
|
22
|
+
else
|
|
23
|
+
HTTP_CONFIG := $(LANGGRAPH_HTTP)
|
|
24
|
+
endif
|
|
25
|
+
|
|
26
|
+
LANGGRAPH_STORE ?= ""
|
|
27
|
+
ifeq ($(LANGGRAPH_STORE),custom)
|
|
28
|
+
STORE_CONFIG := {"path": "./tests/graphs/custom_store.py:generate_store"}
|
|
29
|
+
else
|
|
30
|
+
STORE_CONFIG := {"index": {"dims": 500, "embed": "./tests/graphs/test_utils/embeddings.py:embeddings"}}
|
|
31
|
+
endif
|
|
32
|
+
|
|
33
|
+
REVISION ?= $(shell git rev-parse --short HEAD)
|
|
34
|
+
|
|
35
|
+
test-license-oss:
|
|
36
|
+
LANGGRAPH_RUNTIME_EDITION=inmem LANGGRAPH_HTTP='$(HTTP_CONFIG)' LANGGRAPH_STORE='$(STORE_CONFIG)' REDIS_URI=_FAKE DATABASE_URI=:memory: MIGRATIONS_PATH=__inmem__ uv run pytest -v $(TEST)
|
|
37
|
+
|
|
38
|
+
test-watch-oss:
|
|
39
|
+
LANGGRAPH_RUNTIME_EDITION=inmem LANGGRAPH_HTTP='$(HTTP_CONFIG)' REDIS_URI=_FAKE DATABASE_URI=:memory: MIGRATIONS_PATH=__inmem__ uv run ptw . -- -x -vv --ff --capture=no $(TEST)
|
|
40
|
+
|
|
41
|
+
test: test-license-oss
|
|
42
|
+
test-watch: test-watch-oss
|
|
43
|
+
|
|
44
|
+
test-auth:
|
|
45
|
+
LANGGRAPH_RUNTIME_EDITION=inmem LANGGRAPH_AUTH='{"path": "./tests/graphs/fastapi_jwt_auth.py:get_current_active_user"}' REDIS_URI=_FAKE DATABASE_URI=:memory: MIGRATIONS_PATH=__inmem__ uv run pytest -v $(AUTH_TEST)
|
|
46
|
+
|
|
47
|
+
test-auth-watch:
|
|
48
|
+
LANGGRAPH_RUNTIME_EDITION=inmem LANGGRAPH_AUTH='{"path": "./tests/graphs/fastapi_jwt_auth.py:get_current_active_user"}' REDIS_URI=_FAKE DATABASE_URI=:memory: MIGRATIONS_PATH=__inmem__ uv run ptw . -- -x -vv --ff --capture=no $(AUTH_TEST)
|
|
49
|
+
|
|
50
|
+
# dev commands
|
|
51
|
+
|
|
52
|
+
start:
|
|
53
|
+
LANGGRAPH_HTTP='$(HTTP_CONFIG)' \
|
|
54
|
+
LANGGRAPH_RUNTIME_EDITION=inmem \
|
|
55
|
+
LANGGRAPH_AES_KEY='$(LANGGRAPH_AES_KEY)' \
|
|
56
|
+
N_JOBS_PER_WORKER=2 \
|
|
57
|
+
LANGGRAPH_CONFIG='{"agent": {"configurable": {"model_name": "openai"}}}' \
|
|
58
|
+
LANGSERVE_GRAPHS='{"agent": "./tests/graphs/agent.py:graph", "single_node": "./tests/graphs/single_node.py:graph", "other": "./tests/graphs/other.py:make_graph", "weather": "./tests/graphs/weather.py:mk_weather_graph", "searchy": "./tests/graphs/searchy.py:graph", "agent_simple": "./tests/graphs/agent_simple.py:graph"}' \
|
|
59
|
+
LANGGRAPH_STORE='$(STORE_CONFIG)' \
|
|
60
|
+
LANGGRAPH_CONFIG='{"agent": {"configurable": {"model_name": "openai"}}}' \
|
|
61
|
+
LANGSMITH_LANGGRAPH_API_VARIANT=test \
|
|
62
|
+
REDIS_URI=fake \
|
|
63
|
+
DATABASE_URI=:memory: \
|
|
64
|
+
MIGRATIONS_PATH=__inmem \
|
|
65
|
+
uv run uvicorn \
|
|
66
|
+
"langgraph_api.server:app" \
|
|
67
|
+
--reload \
|
|
68
|
+
--port 9123 \
|
|
69
|
+
--reload-dir langgraph_api \
|
|
70
|
+
--no-access-log
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
start-license-oss: start
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
start-auth-jwt:
|
|
77
|
+
LANGGRAPH_RUNTIME_EDITION=inmem LANGGRAPH_HTTP='$(HTTP_CONFIG)' \
|
|
78
|
+
LANGGRAPH_AES_KEY='$(LANGGRAPH_AES_KEY)' \
|
|
79
|
+
N_JOBS_PER_WORKER=2 \
|
|
80
|
+
LANGSERVE_GRAPHS='{"agent": "./tests/graphs/agent.py:graph", "other": "./tests/graphs/other.py:make_graph", "weather": "./tests/graphs/weather.py:mk_weather_graph", "searchy": "./tests/graphs/searchy.py:graph", "agent_simple": "./tests/graphs/agent_simple.py:graph"}' \
|
|
81
|
+
LANGGRAPH_STORE='$(STORE_CONFIG)' \
|
|
82
|
+
LANGGRAPH_AUTH='{"path": "tests/graphs/jwt_auth.py:auth"}' \
|
|
83
|
+
LANGSMITH_LANGGRAPH_API_VARIANT=test \
|
|
84
|
+
REDIS_URI=fake \
|
|
85
|
+
DATABASE_URI=:memory: \
|
|
86
|
+
MIGRATIONS_PATH=__inmem \
|
|
87
|
+
uv run uvicorn \
|
|
88
|
+
"langgraph_api.server:app" \
|
|
89
|
+
--reload \
|
|
90
|
+
--port 9123 \
|
|
91
|
+
--reload-dir langgraph_api \
|
|
92
|
+
--no-access-log
|
|
93
|
+
|
|
94
|
+
start-auth-fastapi-jwt:
|
|
95
|
+
LANGGRAPH_RUNTIME_EDITION=inmem LANGGRAPH_HTTP='$(HTTP_CONFIG)' \
|
|
96
|
+
N_JOBS_PER_WORKER=2 \
|
|
97
|
+
LANGSERVE_GRAPHS='{"agent": "./tests/graphs/agent.py:graph", "other": "./tests/graphs/other.py:make_graph", "weather": "./tests/graphs/weather.py:mk_weather_graph", "searchy": "./tests/graphs/searchy.py:graph", "agent_simple": "./tests/graphs/agent_simple.py:graph"}' \
|
|
98
|
+
LANGGRAPH_STORE='$(STORE_CONFIG)' \
|
|
99
|
+
LANGGRAPH_AUTH='{"path": "./tests/graphs/fastapi_jwt_auth.py:auth"}' \
|
|
100
|
+
LANGSMITH_LANGGRAPH_API_VARIANT=test \
|
|
101
|
+
REDIS_URI=fake \
|
|
102
|
+
DATABASE_URI=:memory: \
|
|
103
|
+
MIGRATIONS_PATH=__inmem \
|
|
104
|
+
uv run uvicorn \
|
|
105
|
+
"langgraph_api.server:app" \
|
|
106
|
+
--reload \
|
|
107
|
+
--port 9123 \
|
|
108
|
+
--reload-dir langgraph_api \
|
|
109
|
+
--no-access-log
|
|
110
|
+
|
|
111
|
+
VERSION_KIND ?= patch
|
|
112
|
+
|
|
113
|
+
bump-version:
|
|
114
|
+
uv run --with hatch hatch version $(VERSION_KIND)
|
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
|
|
3
|
+
Version: 0.2.28
|
|
4
|
+
Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
|
|
5
5
|
License: Elastic-2.0
|
|
6
|
-
|
|
7
|
-
Author-email: nuno@langchain.dev
|
|
6
|
+
License-File: LICENSE
|
|
8
7
|
Requires-Python: >=3.11
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: structlog (>=24.1.0,<26)
|
|
29
|
-
Requires-Dist: tenacity (>=8.0.0)
|
|
30
|
-
Requires-Dist: truststore (>=0.1)
|
|
31
|
-
Requires-Dist: uvicorn (>=0.26.0)
|
|
32
|
-
Requires-Dist: watchfiles (>=0.13)
|
|
8
|
+
Requires-Dist: cloudpickle>=3.0.0
|
|
9
|
+
Requires-Dist: cryptography<45.0,>=42.0.0
|
|
10
|
+
Requires-Dist: httpx>=0.25.0
|
|
11
|
+
Requires-Dist: jsonschema-rs<0.30,>=0.20.0
|
|
12
|
+
Requires-Dist: langchain-core>=0.2.38
|
|
13
|
+
Requires-Dist: langgraph-checkpoint>=2.0.23
|
|
14
|
+
Requires-Dist: langgraph-runtime-inmem<0.2,>=0.1.0
|
|
15
|
+
Requires-Dist: langgraph-sdk>=0.1.66
|
|
16
|
+
Requires-Dist: langgraph>=0.3.27
|
|
17
|
+
Requires-Dist: langsmith>=0.1.112
|
|
18
|
+
Requires-Dist: orjson>=3.9.7
|
|
19
|
+
Requires-Dist: pyjwt>=2.9.0
|
|
20
|
+
Requires-Dist: sse-starlette<2.2.0,>=2.1.0
|
|
21
|
+
Requires-Dist: starlette>=0.38.6
|
|
22
|
+
Requires-Dist: structlog<26,>=24.1.0
|
|
23
|
+
Requires-Dist: tenacity>=8.0.0
|
|
24
|
+
Requires-Dist: truststore>=0.1
|
|
25
|
+
Requires-Dist: uvicorn>=0.26.0
|
|
26
|
+
Requires-Dist: watchfiles>=0.13
|
|
33
27
|
Description-Content-Type: text/markdown
|
|
34
28
|
|
|
35
29
|
# LangGraph API
|
|
@@ -132,4 +126,4 @@ Options:
|
|
|
132
126
|
|
|
133
127
|
## License
|
|
134
128
|
|
|
135
|
-
This project is licensed under the Elastic License 2.0 - see the [LICENSE](./LICENSE) file for details.
|
|
129
|
+
This project is licensed under the Elastic License 2.0 - see the [LICENSE](./LICENSE) file for details.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import http from "k6/http";
|
|
2
|
+
import { check } from "k6";
|
|
3
|
+
|
|
4
|
+
export const options = {
|
|
5
|
+
vus: 100,
|
|
6
|
+
duration: "30s",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const ORIGIN = "http://localhost:9123";
|
|
10
|
+
|
|
11
|
+
export function setup() {
|
|
12
|
+
const assistant = http.post(
|
|
13
|
+
`${ORIGIN}/assistants`,
|
|
14
|
+
JSON.stringify({ graph_id: "weather" }),
|
|
15
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
check(assistant, { "assistant status is 200": (r) => r.status === 200 });
|
|
19
|
+
|
|
20
|
+
return assistant.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function main(assistant) {
|
|
24
|
+
const thread = http.post(`${ORIGIN}/threads`, JSON.stringify({}), {
|
|
25
|
+
params: { headers: { "Content-Type": "application/json" } },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
check(thread, { "thread status is 200": (r) => r.status === 200 });
|
|
29
|
+
|
|
30
|
+
const wait = http.post(
|
|
31
|
+
`${ORIGIN}/threads/${thread.json().thread_id}/runs/wait`,
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
input: {
|
|
34
|
+
messages: [{ role: "human", content: "SF", id: "initial-message" }],
|
|
35
|
+
},
|
|
36
|
+
assistant_id: assistant.assistant_id,
|
|
37
|
+
}),
|
|
38
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
check(wait, {
|
|
42
|
+
"interrupted run status is 200": (r) => r.status === 200,
|
|
43
|
+
"interrupted run has values": (r) => {
|
|
44
|
+
const json = r.json();
|
|
45
|
+
if (json.route !== "weather") return false;
|
|
46
|
+
if (json.messages.length !== 1) return false;
|
|
47
|
+
if (json.messages[0].id !== "initial-message") return false;
|
|
48
|
+
if (json.messages[0].content !== "SF") return false;
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const continueWait = http.post(
|
|
54
|
+
`${ORIGIN}/threads/${thread.json().thread_id}/runs/wait`,
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
input: null,
|
|
57
|
+
assistant_id: assistant.assistant_id,
|
|
58
|
+
}),
|
|
59
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
check(continueWait, {
|
|
63
|
+
"run status is 200": (r) => r.status === 200,
|
|
64
|
+
"run has values": (r) => {
|
|
65
|
+
const json = r.json();
|
|
66
|
+
if (json.route !== "weather") return false;
|
|
67
|
+
if (json.messages.length !== 2) return false;
|
|
68
|
+
if (json.messages[0].id !== "initial-message") return false;
|
|
69
|
+
if (json.messages[0].content !== "SF") return false;
|
|
70
|
+
if (json.messages[1].content !== "It's sunny in San Francisco!")
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# When editing this file, also update docs pages:
|
|
2
|
+
# https://github.com/langchain-ai/langgraph/blob/main/docs/docs/cloud/deployment/setup.md
|
|
3
|
+
# https://github.com/langchain-ai/langgraph/blob/main/docs/docs/cloud/deployment/setup_pyproject.md
|
|
4
|
+
langgraph>=0.3.27
|
|
5
|
+
langgraph-sdk>=0.1.66
|
|
6
|
+
langgraph-checkpoint>=2.0.23
|
|
7
|
+
langchain-core>=0.2.43
|
|
8
|
+
langsmith>=0.1.112
|
|
9
|
+
orjson>=3.9.7,<3.10.17
|
|
10
|
+
httpx>=0.25.0
|
|
11
|
+
tenacity>=8.0.0
|
|
12
|
+
uvicorn>=0.26.0
|
|
13
|
+
sse-starlette>=2.1.0,<2.2.0
|
|
14
|
+
uvloop>=0.18.0
|
|
15
|
+
httptools>=0.5.0
|
|
16
|
+
jsonschema-rs>=0.20.0
|
|
17
|
+
structlog>=24.1.0
|
|
18
|
+
cloudpickle>=3.0.0
|
|
19
|
+
truststore>=0.1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import urllib.request
|
|
4
|
+
|
|
5
|
+
prefix = ""
|
|
6
|
+
# Override prefix if it's set in the http config
|
|
7
|
+
if (http := os.environ.get("LANGGRAPH_HTTP")) and (
|
|
8
|
+
mount_prefix := json.loads(http).get("mount_prefix")
|
|
9
|
+
):
|
|
10
|
+
prefix = mount_prefix
|
|
11
|
+
# Override that
|
|
12
|
+
if os.environ.get("MOUNT_PREFIX"):
|
|
13
|
+
prefix = os.environ["MOUNT_PREFIX"]
|
|
14
|
+
|
|
15
|
+
with urllib.request.urlopen(
|
|
16
|
+
f"http://localhost:{os.environ['PORT']}{prefix}/ok"
|
|
17
|
+
) as response:
|
|
18
|
+
assert response.status == 200
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.28"
|
|
@@ -9,6 +9,7 @@ from starlette.exceptions import HTTPException
|
|
|
9
9
|
from starlette.responses import Response
|
|
10
10
|
from starlette.routing import BaseRoute
|
|
11
11
|
|
|
12
|
+
from langgraph_api import store as api_store
|
|
12
13
|
from langgraph_api.graph import get_assistant_id, get_graph
|
|
13
14
|
from langgraph_api.js.base import BaseRemotePregel
|
|
14
15
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
@@ -25,7 +26,6 @@ from langgraph_runtime.checkpoint import Checkpointer
|
|
|
25
26
|
from langgraph_runtime.database import connect
|
|
26
27
|
from langgraph_runtime.ops import Assistants
|
|
27
28
|
from langgraph_runtime.retry import retry_db
|
|
28
|
-
from langgraph_runtime.store import Store
|
|
29
29
|
|
|
30
30
|
logger = structlog.stdlib.get_logger(__name__)
|
|
31
31
|
|
|
@@ -194,7 +194,7 @@ async def get_assistant_graph(
|
|
|
194
194
|
assistant["graph_id"],
|
|
195
195
|
config,
|
|
196
196
|
checkpointer=Checkpointer(conn),
|
|
197
|
-
store=
|
|
197
|
+
store=(await api_store.get_store()),
|
|
198
198
|
) as graph:
|
|
199
199
|
xray: bool | int = False
|
|
200
200
|
xray_query = request.query_params.get("xray")
|
|
@@ -240,7 +240,7 @@ async def get_assistant_subgraphs(
|
|
|
240
240
|
assistant["graph_id"],
|
|
241
241
|
config,
|
|
242
242
|
checkpointer=Checkpointer(conn),
|
|
243
|
-
store=
|
|
243
|
+
store=(await api_store.get_store()),
|
|
244
244
|
) as graph:
|
|
245
245
|
namespace = request.path_params.get("namespace")
|
|
246
246
|
|
|
@@ -286,7 +286,7 @@ async def get_assistant_schemas(
|
|
|
286
286
|
assistant["graph_id"],
|
|
287
287
|
config,
|
|
288
288
|
checkpointer=Checkpointer(conn),
|
|
289
|
-
store=
|
|
289
|
+
store=(await api_store.get_store()),
|
|
290
290
|
) as graph:
|
|
291
291
|
if isinstance(graph, BaseRemotePregel):
|
|
292
292
|
schemas = await graph.fetch_state_schema()
|
|
@@ -6,6 +6,7 @@ from starlette.routing import BaseRoute
|
|
|
6
6
|
|
|
7
7
|
from langgraph_api.auth.custom import handle_event as _handle_event
|
|
8
8
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
9
|
+
from langgraph_api.store import get_store
|
|
9
10
|
from langgraph_api.utils import get_auth_ctx
|
|
10
11
|
from langgraph_api.validation import (
|
|
11
12
|
StoreDeleteRequest,
|
|
@@ -14,7 +15,6 @@ from langgraph_api.validation import (
|
|
|
14
15
|
StoreSearchRequest,
|
|
15
16
|
)
|
|
16
17
|
from langgraph_runtime.retry import retry_db
|
|
17
|
-
from langgraph_runtime.store import Store
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _validate_namespace(namespace: tuple[str, ...]) -> Response | None:
|
|
@@ -57,7 +57,9 @@ async def put_item(request: ApiRequest):
|
|
|
57
57
|
"value": payload["value"],
|
|
58
58
|
}
|
|
59
59
|
await handle_event("put", handler_payload)
|
|
60
|
-
await
|
|
60
|
+
await (await get_store()).aput(
|
|
61
|
+
namespace, handler_payload["key"], handler_payload["value"]
|
|
62
|
+
)
|
|
61
63
|
return Response(status_code=204)
|
|
62
64
|
|
|
63
65
|
|
|
@@ -75,7 +77,7 @@ async def get_item(request: ApiRequest):
|
|
|
75
77
|
"key": key,
|
|
76
78
|
}
|
|
77
79
|
await handle_event("get", handler_payload)
|
|
78
|
-
result = await
|
|
80
|
+
result = await (await get_store()).aget(namespace, key)
|
|
79
81
|
return ApiResponse(result.dict() if result is not None else None)
|
|
80
82
|
|
|
81
83
|
|
|
@@ -91,7 +93,9 @@ async def delete_item(request: ApiRequest):
|
|
|
91
93
|
"key": payload["key"],
|
|
92
94
|
}
|
|
93
95
|
await handle_event("delete", handler_payload)
|
|
94
|
-
await
|
|
96
|
+
await (await get_store()).adelete(
|
|
97
|
+
handler_payload["namespace"], handler_payload["key"]
|
|
98
|
+
)
|
|
95
99
|
return Response(status_code=204)
|
|
96
100
|
|
|
97
101
|
|
|
@@ -114,7 +118,7 @@ async def search_items(request: ApiRequest):
|
|
|
114
118
|
"query": query,
|
|
115
119
|
}
|
|
116
120
|
await handle_event("search", handler_payload)
|
|
117
|
-
items = await
|
|
121
|
+
items = await (await get_store()).asearch(
|
|
118
122
|
handler_payload["namespace"],
|
|
119
123
|
filter=handler_payload["filter"],
|
|
120
124
|
limit=handler_payload["limit"],
|
|
@@ -145,7 +149,7 @@ async def list_namespaces(request: ApiRequest):
|
|
|
145
149
|
"offset": offset,
|
|
146
150
|
}
|
|
147
151
|
await handle_event("list_namespaces", handler_payload)
|
|
148
|
-
result = await
|
|
152
|
+
result = await (await get_store()).alist_namespaces(
|
|
149
153
|
prefix=handler_payload["namespace"],
|
|
150
154
|
suffix=handler_payload["suffix"],
|
|
151
155
|
max_depth=handler_payload["max_depth"],
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""ASGI transport that lets you schedule to the main loop.
|
|
2
|
+
|
|
3
|
+
Adapted from: https://github.com/encode/httpx/blob/6c7af967734bafd011164f2a1653abc87905a62b/httpx/_transports/asgi.py#L1
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
from httpx import ASGITransport as ASGITransportBase
|
|
11
|
+
from httpx import AsyncByteStream, Request, Response
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING: # pragma: no cover
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
import trio
|
|
17
|
+
|
|
18
|
+
Event = asyncio.Event | trio.Event
|
|
19
|
+
|
|
20
|
+
__all__ = ["ASGITransport"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_running_trio() -> bool:
|
|
24
|
+
try:
|
|
25
|
+
# sniffio is a dependency of trio.
|
|
26
|
+
|
|
27
|
+
# See https://github.com/python-trio/trio/issues/2802
|
|
28
|
+
import sniffio
|
|
29
|
+
|
|
30
|
+
if sniffio.current_async_library() == "trio":
|
|
31
|
+
return True
|
|
32
|
+
except ImportError: # pragma: nocover
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_event() -> Event:
|
|
39
|
+
if is_running_trio():
|
|
40
|
+
import trio
|
|
41
|
+
|
|
42
|
+
return trio.Event()
|
|
43
|
+
|
|
44
|
+
import asyncio
|
|
45
|
+
|
|
46
|
+
return asyncio.Event()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ASGIResponseStream(AsyncByteStream):
|
|
50
|
+
def __init__(self, body: list[bytes]) -> None:
|
|
51
|
+
self._body = body
|
|
52
|
+
|
|
53
|
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
|
54
|
+
yield b"".join(self._body)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ASGITransport(ASGITransportBase):
|
|
58
|
+
"""
|
|
59
|
+
A custom AsyncTransport that handles sending requests directly to an ASGI app.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
transport = httpx.ASGITransport(
|
|
63
|
+
app=app,
|
|
64
|
+
root_path="/submount",
|
|
65
|
+
client=("1.2.3.4", 123)
|
|
66
|
+
)
|
|
67
|
+
client = httpx.AsyncClient(transport=transport)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Arguments:
|
|
71
|
+
|
|
72
|
+
* `app` - The ASGI application.
|
|
73
|
+
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
|
|
74
|
+
should be raised. Default to `True`. Can be set to `False` for use cases
|
|
75
|
+
such as testing the content of a client 500 response.
|
|
76
|
+
* `root_path` - The root path on which the ASGI application should be mounted.
|
|
77
|
+
* `client` - A two-tuple indicating the client IP and port of incoming requests.
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
async def handle_async_request(
|
|
82
|
+
self,
|
|
83
|
+
request: Request,
|
|
84
|
+
) -> Response:
|
|
85
|
+
from langgraph_api.asyncio import call_soon_in_main_loop
|
|
86
|
+
|
|
87
|
+
assert isinstance(request.stream, AsyncByteStream)
|
|
88
|
+
|
|
89
|
+
# ASGI scope.
|
|
90
|
+
scope = {
|
|
91
|
+
"type": "http",
|
|
92
|
+
"asgi": {"version": "3.0"},
|
|
93
|
+
"http_version": "1.1",
|
|
94
|
+
"method": request.method,
|
|
95
|
+
"headers": [(k.lower(), v) for (k, v) in request.headers.raw],
|
|
96
|
+
"scheme": request.url.scheme,
|
|
97
|
+
"path": request.url.path,
|
|
98
|
+
"raw_path": request.url.raw_path.split(b"?")[0],
|
|
99
|
+
"query_string": request.url.query,
|
|
100
|
+
"server": (request.url.host, request.url.port),
|
|
101
|
+
"client": self.client,
|
|
102
|
+
"root_path": self.root_path,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Request.
|
|
106
|
+
request_body_chunks = request.stream.__aiter__()
|
|
107
|
+
request_complete = False
|
|
108
|
+
|
|
109
|
+
# Response.
|
|
110
|
+
status_code = None
|
|
111
|
+
response_headers = None
|
|
112
|
+
body_parts = []
|
|
113
|
+
response_started = False
|
|
114
|
+
response_complete = create_event()
|
|
115
|
+
|
|
116
|
+
# ASGI callables.
|
|
117
|
+
|
|
118
|
+
async def receive() -> dict[str, typing.Any]:
|
|
119
|
+
nonlocal request_complete
|
|
120
|
+
|
|
121
|
+
if request_complete:
|
|
122
|
+
await response_complete.wait()
|
|
123
|
+
return {"type": "http.disconnect"}
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
body = await request_body_chunks.__anext__()
|
|
127
|
+
except StopAsyncIteration:
|
|
128
|
+
request_complete = True
|
|
129
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
130
|
+
return {"type": "http.request", "body": body, "more_body": True}
|
|
131
|
+
|
|
132
|
+
async def send(message: typing.MutableMapping[str, typing.Any]) -> None:
|
|
133
|
+
nonlocal status_code, response_headers, response_started
|
|
134
|
+
|
|
135
|
+
if message["type"] == "http.response.start":
|
|
136
|
+
assert not response_started
|
|
137
|
+
|
|
138
|
+
status_code = message["status"]
|
|
139
|
+
response_headers = message.get("headers", [])
|
|
140
|
+
response_started = True
|
|
141
|
+
|
|
142
|
+
elif message["type"] == "http.response.body":
|
|
143
|
+
assert not response_complete.is_set()
|
|
144
|
+
body = message.get("body", b"")
|
|
145
|
+
more_body = message.get("more_body", False)
|
|
146
|
+
|
|
147
|
+
if body and request.method != "HEAD":
|
|
148
|
+
body_parts.append(body)
|
|
149
|
+
|
|
150
|
+
if not more_body:
|
|
151
|
+
response_complete.set()
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
await call_soon_in_main_loop(self.app(scope, receive, send))
|
|
155
|
+
except Exception: # noqa: PIE-786
|
|
156
|
+
if self.raise_app_exceptions:
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
response_complete.set()
|
|
160
|
+
if status_code is None:
|
|
161
|
+
status_code = 500
|
|
162
|
+
if response_headers is None:
|
|
163
|
+
response_headers = {}
|
|
164
|
+
|
|
165
|
+
assert response_complete.is_set()
|
|
166
|
+
assert status_code is not None
|
|
167
|
+
assert response_headers is not None
|
|
168
|
+
|
|
169
|
+
stream = ASGIResponseStream(body_parts)
|
|
170
|
+
|
|
171
|
+
return Response(status_code, headers=response_headers, stream=stream)
|
|
@@ -6,6 +6,7 @@ from functools import partial
|
|
|
6
6
|
from typing import Any, Generic, TypeVar
|
|
7
7
|
|
|
8
8
|
import structlog
|
|
9
|
+
from langgraph.utils.future import chain_future
|
|
9
10
|
|
|
10
11
|
T = TypeVar("T")
|
|
11
12
|
|
|
@@ -19,6 +20,12 @@ def set_event_loop(loop: asyncio.AbstractEventLoop) -> None:
|
|
|
19
20
|
_MAIN_LOOP = loop
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def get_event_loop() -> asyncio.AbstractEventLoop:
|
|
24
|
+
if _MAIN_LOOP is None:
|
|
25
|
+
raise RuntimeError("No event loop set")
|
|
26
|
+
return _MAIN_LOOP
|
|
27
|
+
|
|
28
|
+
|
|
22
29
|
async def sleep_if_not_done(delay: float, done: asyncio.Event) -> None:
|
|
23
30
|
try:
|
|
24
31
|
await asyncio.wait_for(done.wait(), delay)
|
|
@@ -118,6 +125,16 @@ def run_coroutine_threadsafe(
|
|
|
118
125
|
return future
|
|
119
126
|
|
|
120
127
|
|
|
128
|
+
def call_soon_in_main_loop(coro: Coroutine[Any, Any, T]) -> asyncio.Future[T]:
|
|
129
|
+
"""Run a coroutine in the main event loop."""
|
|
130
|
+
if _MAIN_LOOP is None:
|
|
131
|
+
raise RuntimeError("No event loop set")
|
|
132
|
+
main_loop_fut = asyncio.ensure_future(coro, loop=_MAIN_LOOP)
|
|
133
|
+
this_loop_fut = asyncio.get_running_loop().create_future()
|
|
134
|
+
_MAIN_LOOP.call_soon_threadsafe(chain_future, main_loop_fut, this_loop_fut)
|
|
135
|
+
return this_loop_fut
|
|
136
|
+
|
|
137
|
+
|
|
121
138
|
class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
|
|
122
139
|
"""An async task group that can be configured to wait and/or cancel tasks on exit.
|
|
123
140
|
|