langgraph-cli 0.1.6__py3-none-any.whl

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.
File without changes
langgraph_cli/cli.py ADDED
@@ -0,0 +1,301 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import pathlib
5
+ import shutil
6
+ import signal
7
+ from datetime import datetime, timezone
8
+ from typing import Coroutine, Optional
9
+
10
+ import click
11
+
12
+ import langgraph_cli.config
13
+ import langgraph_cli.docker
14
+
15
+
16
+ async def exec(cmd: str, *args: str, input: str = None, wait: float = None):
17
+ if wait:
18
+ await asyncio.sleep(wait)
19
+ try:
20
+ proc = await asyncio.create_subprocess_exec(
21
+ cmd, *args, stdin=asyncio.subprocess.PIPE if input else None
22
+ )
23
+ await proc.communicate(input.encode() if input else None)
24
+ if (
25
+ proc.returncode != 0 # success
26
+ and proc.returncode != 130 # user interrupt
27
+ ):
28
+ raise click.exceptions.Exit(proc.returncode)
29
+ finally:
30
+ try:
31
+ if proc.returncode is None:
32
+ try:
33
+ os.killpg(os.getpgid(proc.pid), signal.SIGINT)
34
+ except (ProcessLookupError, KeyboardInterrupt):
35
+ pass
36
+ except UnboundLocalError:
37
+ pass
38
+
39
+
40
+ PLACEHOLDER_NOW = object()
41
+
42
+
43
+ async def exec_loop(cmd: str, *args: str, input: str = None):
44
+ now = datetime.now(timezone.utc).isoformat()
45
+ while True:
46
+ try:
47
+ await exec(
48
+ cmd, *(now if a is PLACEHOLDER_NOW else a for a in args), input=input
49
+ )
50
+ now = datetime.now(timezone.utc).isoformat()
51
+ await asyncio.sleep(1)
52
+ except Exception as e:
53
+ print(e)
54
+ pass
55
+
56
+
57
+ async def gather(*coros: Coroutine):
58
+ tasks = [asyncio.create_task(coro) for coro in coros]
59
+ exceptions = []
60
+ while tasks:
61
+ done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
62
+ for t in tasks:
63
+ t.cancel()
64
+ for d in done:
65
+ if exc := d.exception():
66
+ exceptions.append(exc)
67
+
68
+
69
+ OPT_O = click.option(
70
+ "--docker-compose",
71
+ "-d",
72
+ help="Advanced: Path to docker-compose.yml file with additional services to launch",
73
+ type=click.Path(
74
+ exists=True,
75
+ file_okay=True,
76
+ dir_okay=False,
77
+ resolve_path=True,
78
+ path_type=pathlib.Path,
79
+ ),
80
+ )
81
+ OPT_C = click.option(
82
+ "--config",
83
+ "-c",
84
+ help="""Path to configuration file declaring dependencies, graphs and environment variables.
85
+
86
+ \b
87
+ Example:
88
+ {
89
+ "dependencies": [
90
+ "langchain_openai",
91
+ "./your_package"
92
+ ],
93
+ "graphs": {
94
+ "my_graph_id": "./your_package/your_file.py:variable"
95
+ },
96
+ "env": "./.env"
97
+ }
98
+
99
+ Defaults to looking for langgraph.json in the current directory.""",
100
+ default="langgraph.json",
101
+ type=click.Path(
102
+ exists=True,
103
+ file_okay=True,
104
+ dir_okay=False,
105
+ resolve_path=True,
106
+ path_type=pathlib.Path,
107
+ ),
108
+ )
109
+ OPT_PORT = click.option(
110
+ "--port", "-p", type=int, default=8123, show_default=True, help="Port to expose"
111
+ )
112
+ OPT_DEBUGGER_PORT = click.option(
113
+ "--debugger-port",
114
+ "-dp",
115
+ type=int,
116
+ default=8124,
117
+ show_default=True,
118
+ help="Port to expose debug UI on",
119
+ )
120
+
121
+
122
+ @click.group()
123
+ def cli():
124
+ pass
125
+
126
+
127
+ @click.option(
128
+ "--recreate/--no-recreate",
129
+ default=False,
130
+ show_default=True,
131
+ help="Clear previous data",
132
+ )
133
+ @click.option(
134
+ "--pull/--no-pull", default=True, show_default=True, help="Pull latest images"
135
+ )
136
+ @OPT_DEBUGGER_PORT
137
+ @OPT_PORT
138
+ @OPT_O
139
+ @OPT_C
140
+ @cli.command(help="Start langgraph API server")
141
+ def up(
142
+ config: pathlib.Path,
143
+ docker_compose: Optional[pathlib.Path],
144
+ port: int,
145
+ recreate: bool,
146
+ pull: bool,
147
+ debugger_port: Optional[int],
148
+ ):
149
+ with asyncio.Runner() as runner:
150
+ # check docker available
151
+ try:
152
+ runner.run(exec("docker", "--version"))
153
+ runner.run(exec("docker", "compose", "version"))
154
+ except click.exceptions.Exit:
155
+ click.echo("Docker not installed or not running")
156
+ return
157
+ # pull latest images
158
+ if pull:
159
+ runner.run(exec("docker", "pull", "langchain/langserve"))
160
+ # prepare args
161
+ stdin = langgraph_cli.docker.compose(port=port, debugger_port=debugger_port)
162
+ args = [
163
+ "--project-directory",
164
+ config.parent,
165
+ "-f",
166
+ "-", # stdin
167
+ ]
168
+ # apply options
169
+ if docker_compose:
170
+ args.extend(["-f", str(docker_compose)])
171
+ args.append("up")
172
+ if config:
173
+ with open(config) as f:
174
+ stdin += langgraph_cli.config.config_to_compose(config, json.load(f))
175
+ if recreate:
176
+ args.extend(["--force-recreate", "--remove-orphans"])
177
+ shutil.rmtree(".langserve-data", ignore_errors=True)
178
+ # run docker compose
179
+ runner.run(exec("docker", "compose", *args, input=stdin))
180
+
181
+
182
+ @OPT_C
183
+ @click.option("--tag", "-t", help="Tag for the image", required=True)
184
+ @cli.command(help="Build langgraph API server image")
185
+ def build(
186
+ config: pathlib.Path,
187
+ tag: str,
188
+ ):
189
+ with asyncio.Runner() as runner:
190
+ # check docker available
191
+ try:
192
+ runner.run(exec("docker", "--version"))
193
+ runner.run(exec("docker", "compose", "version"))
194
+ except click.exceptions.Exit:
195
+ click.echo("Docker not installed or not running")
196
+ return
197
+ # apply options
198
+ args = [
199
+ "-f",
200
+ "-", # stdin
201
+ "-t",
202
+ tag,
203
+ ]
204
+ with open(config) as f:
205
+ stdin = langgraph_cli.config.config_to_docker(config, json.load(f))
206
+ # run docker build
207
+ runner.run(exec("docker", "build", *args, config.parent, input=stdin))
208
+
209
+
210
+ @OPT_DEBUGGER_PORT
211
+ @OPT_PORT
212
+ @OPT_O
213
+ @OPT_C
214
+ @cli.command(help="Write k8s files", hidden=True)
215
+ def k8s(
216
+ config: pathlib.Path,
217
+ docker_compose: Optional[pathlib.Path],
218
+ port: int,
219
+ debugger_port: Optional[int],
220
+ ):
221
+ with asyncio.Runner() as runner:
222
+ # check docker available
223
+ try:
224
+ runner.run(exec("docker", "--version"))
225
+ runner.run(exec("docker", "compose", "version"))
226
+ except click.exceptions.Exit:
227
+ click.echo("Docker not installed or not running")
228
+ return
229
+ # prepare args
230
+ stdin = langgraph_cli.docker.compose(port=port, debugger_port=debugger_port)
231
+ args = [
232
+ "-f",
233
+ "-", # stdin
234
+ ]
235
+ # apply options
236
+ if docker_compose:
237
+ args.extend(["-f", str(docker_compose)])
238
+ args.append("convert")
239
+ if config:
240
+ with open(config) as f:
241
+ stdin += langgraph_cli.config.config_to_compose(config, json.load(f))
242
+ # run kompose convert
243
+ runner.run(exec("kompose", *args, input=stdin))
244
+
245
+
246
+ @OPT_O
247
+ @OPT_PORT
248
+ @cli.command()
249
+ def watch(override: pathlib.Path, port: int):
250
+ compose = langgraph_cli.docker.compose(port=port)
251
+
252
+ with asyncio.Runner() as runner:
253
+ try:
254
+ runner.run(
255
+ gather(
256
+ exec(
257
+ "docker",
258
+ "compose",
259
+ "--project-directory",
260
+ override.parent,
261
+ "-f",
262
+ "-",
263
+ "-f",
264
+ str(override),
265
+ "watch",
266
+ input=compose,
267
+ ),
268
+ exec_loop(
269
+ "docker",
270
+ "compose",
271
+ "--project-directory",
272
+ override.parent,
273
+ "-f",
274
+ "-",
275
+ "-f",
276
+ str(override),
277
+ "logs",
278
+ "--follow",
279
+ "--since",
280
+ PLACEHOLDER_NOW,
281
+ "langserve",
282
+ input=compose,
283
+ ),
284
+ )
285
+ )
286
+ finally:
287
+ # docker compose watch doesn't clean up on exit, so we need to do it
288
+ runner.run(
289
+ exec(
290
+ "docker",
291
+ "compose",
292
+ "--project-directory",
293
+ override.parent,
294
+ "-f",
295
+ "-",
296
+ "-f",
297
+ str(override),
298
+ "kill",
299
+ input=compose,
300
+ )
301
+ )
@@ -0,0 +1,120 @@
1
+ import json
2
+ import os
3
+ import pathlib
4
+ import textwrap
5
+ from typing import TypedDict, Union
6
+
7
+
8
+ class Config(TypedDict):
9
+ dependencies: list[str]
10
+ graphs: dict[str, str]
11
+ env: Union[dict[str, str], str]
12
+
13
+
14
+ def config_to_docker(config_path: pathlib.Path, config: Config):
15
+ pypi_deps = [dep for dep in config["dependencies"] if not dep.startswith(".")]
16
+ local_pkgs: dict[pathlib.Path, str] = {}
17
+ faux_pkgs: dict[pathlib.Path, str] = {}
18
+ pkg_names = set()
19
+
20
+ for local_dep in config["dependencies"]:
21
+ if local_dep.startswith("."):
22
+ resolved = config_path.parent / local_dep
23
+
24
+ # validate local dependency
25
+ if not resolved.exists():
26
+ raise FileNotFoundError(f"Could not find local dependency: {resolved}")
27
+ elif not resolved.is_dir():
28
+ raise NotADirectoryError(
29
+ f"Local dependency must be a directory: {resolved}"
30
+ )
31
+ elif resolved.name in pkg_names:
32
+ raise ValueError(f"Duplicate local dependency: {resolved}")
33
+ else:
34
+ pkg_names.add(resolved.name)
35
+
36
+ # if it's installable, add it to local_pkgs
37
+ # otherwise, add it to faux_pkgs, and create a pyproject.toml
38
+ files = os.listdir(resolved)
39
+ if "pyproject.toml" in files:
40
+ local_pkgs[resolved] = local_dep
41
+ elif "setup.py" in files:
42
+ local_pkgs[resolved] = local_dep
43
+ else:
44
+ faux_pkgs[resolved] = local_dep
45
+
46
+ for graph_id, import_str in config["graphs"].items():
47
+ module_str, _, attr_str = import_str.partition(":")
48
+ if not module_str or not attr_str:
49
+ message = (
50
+ 'Import string "{import_str}" must be in format "<module>:<attribute>".'
51
+ )
52
+ raise ValueError(message.format(import_str=import_str))
53
+ if module_str.startswith("."):
54
+ resolved = config_path.parent / module_str
55
+ if not resolved.exists():
56
+ raise FileNotFoundError(f"Could not find local module: {resolved}")
57
+ elif not resolved.is_file():
58
+ raise IsADirectoryError(f"Local module must be a file: {resolved}")
59
+ else:
60
+ for local_pkg in local_pkgs:
61
+ if resolved.is_relative_to(local_pkg):
62
+ resolved = resolved.relative_to(local_pkg)
63
+ break
64
+ else:
65
+ for faux_pkg in faux_pkgs:
66
+ if resolved.is_relative_to(faux_pkg):
67
+ resolved = resolved.relative_to(faux_pkg.parent)
68
+ break
69
+ else:
70
+ raise ValueError(
71
+ f"Module '{import_str}' not found in 'dependencies' list"
72
+ "Add its containing package to 'dependencies' list."
73
+ )
74
+ # rewrite module_str to be a python import path
75
+ module_str = f"{'.'.join(resolved.parts[:-1])}"
76
+ if resolved.stem == "__init__":
77
+ pass
78
+ else:
79
+ module_str = f"{module_str}.{resolved.stem}"
80
+ # update the config
81
+ config["graphs"][graph_id] = f"{module_str}:{attr_str}"
82
+
83
+ faux_pkgs_str = "\n\n".join(
84
+ f"ADD {relpath} /tmp/{fullpath.name}/{fullpath.name}\n RUN touch /tmp/{fullpath.name}/pyproject.toml"
85
+ for fullpath, relpath in faux_pkgs.items()
86
+ )
87
+ local_pkgs_str = "\n".join(
88
+ f"ADD {relpath} /tmp/{fullpath.name}"
89
+ for fullpath, relpath in local_pkgs.items()
90
+ )
91
+
92
+ return f"""FROM langchain/langgraph-api
93
+
94
+ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
95
+
96
+ {local_pkgs_str}
97
+
98
+ {faux_pkgs_str}
99
+
100
+ RUN pip install {' '.join(pypi_deps)} /tmp/*"""
101
+
102
+
103
+ def config_to_compose(config_path: pathlib.Path, config: Config):
104
+ env_vars_str = (
105
+ "\n".join(f" {k}: {v}" for k, v in config["env"].items())
106
+ if isinstance(config["env"], dict)
107
+ else ""
108
+ )
109
+ env_file_str = (
110
+ f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
111
+ )
112
+
113
+ return f"""
114
+ {env_vars_str}
115
+ {env_file_str}
116
+ pull_policy: build
117
+ build:
118
+ dockerfile_inline: |
119
+ {textwrap.indent(config_to_docker(config_path, config), " ")}
120
+ """
@@ -0,0 +1,70 @@
1
+ import pathlib
2
+ from typing import Optional
3
+
4
+
5
+ ROOT = pathlib.Path(__file__).parent.resolve()
6
+
7
+
8
+ DB = f"""
9
+ langgraph-postgres:
10
+ image: postgres:16
11
+ restart: on-failure
12
+ healthcheck:
13
+ test: pg_isready -U postgres
14
+ start_interval: 1s
15
+ start_period: 5s
16
+ interval: 5s
17
+ retries: 5
18
+ ports:
19
+ - "5433:5432"
20
+ environment:
21
+ POSTGRES_DB: postgres
22
+ POSTGRES_USER: postgres
23
+ POSTGRES_PASSWORD: postgres
24
+ volumes:
25
+ - ./.langgraph-data:/var/lib/postgresql/data
26
+ - {ROOT}/initdb:/docker-entrypoint-initdb.d
27
+ """
28
+
29
+ DEBUGGER = """
30
+ langgraph-debugger:
31
+ image: langchain/langserve-debugger
32
+ restart: on-failure
33
+ ports:
34
+ - "{debugger_port}:5173"
35
+ depends_on:
36
+ langgraph-api:
37
+ condition: service_healthy
38
+ environment:
39
+ VITE_API_BASE_URL: http://localhost:{port}
40
+ """
41
+
42
+
43
+ def compose(
44
+ *,
45
+ # postgres://user:password@host:port/database?option=value
46
+ postgres_uri: Optional[str] = None,
47
+ port: int,
48
+ debugger_port: Optional[int] = None,
49
+ ) -> str:
50
+ if postgres_uri is None:
51
+ include_db = True
52
+ postgres_uri = "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable&search_path=langgraph"
53
+ else:
54
+ include_db = False
55
+
56
+ return f"""
57
+ services:
58
+ {DB if include_db else ""}
59
+ {DEBUGGER.format(port=port, debugger_port=debugger_port) if debugger_port else ""}
60
+ langgraph-api:
61
+ image: langchain/langgraph-api
62
+ restart: on-failure
63
+ ports:
64
+ - "{port}:8000"
65
+ depends_on:
66
+ langgraph-postgres:
67
+ condition: service_healthy
68
+ environment:
69
+ POSTGRES_URI: {postgres_uri}
70
+ """
@@ -0,0 +1 @@
1
+ create schema if not exists langgraph;
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: langgraph-cli
3
+ Version: 0.1.6
4
+ Summary:
5
+ Author: Nuno Campos
6
+ Author-email: nuno@langchain.dev
7
+ Requires-Python: >=3.9.0,<3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Requires-Dist: click (>=8.1.7,<9.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # langserve
16
+
@@ -0,0 +1,9 @@
1
+ langgraph_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ langgraph_cli/cli.py,sha256=v-A0IM2da2qH4B0pkT70z0vGkGIe9qc0-SIfPhOtsAE,8471
3
+ langgraph_cli/config.py,sha256=iqyEHyDLP9N6EKIZmJqTRnu0-Mb01M1eDGBqWq6hVgs,4425
4
+ langgraph_cli/docker.py,sha256=aTDT6G5jBZvAErRCdi37e5i6_8BURclPeN7DsmTpd7s,1828
5
+ langgraph_cli/initdb/init.sql,sha256=ncuhSZOin6Kqw3XenItA33zKsf-U3RkGGwh56mx9hAM,39
6
+ langgraph_cli-0.1.6.dist-info/METADATA,sha256=UrRZZzjPihQSmuaOF0KKf_gjzu-1d22vrZZQOp155AY,443
7
+ langgraph_cli-0.1.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
8
+ langgraph_cli-0.1.6.dist-info/entry_points.txt,sha256=pq7AcWiYJM1-9PtAAF5jZKrOJw6WxnmYuVR_h6i1j4g,51
9
+ langgraph_cli-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ langgraph=langgraph_cli.cli:cli
3
+