langgraph-cli 0.1.0__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.
- langgraph_cli/__init__.py +0 -0
- langgraph_cli/cli.py +202 -0
- langgraph_cli/config.py +81 -0
- langgraph_cli/docker.py +63 -0
- langgraph_cli-0.1.0.dist-info/METADATA +16 -0
- langgraph_cli-0.1.0.dist-info/RECORD +8 -0
- langgraph_cli-0.1.0.dist-info/WHEEL +4 -0
- langgraph_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
File without changes
|
langgraph_cli/cli.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
"--override",
|
|
71
|
+
"-o",
|
|
72
|
+
type=click.Path(
|
|
73
|
+
exists=True,
|
|
74
|
+
file_okay=True,
|
|
75
|
+
dir_okay=False,
|
|
76
|
+
resolve_path=True,
|
|
77
|
+
path_type=pathlib.Path,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
OPT_C = click.option(
|
|
81
|
+
"--config",
|
|
82
|
+
"-c",
|
|
83
|
+
type=click.Path(
|
|
84
|
+
exists=True,
|
|
85
|
+
file_okay=True,
|
|
86
|
+
dir_okay=False,
|
|
87
|
+
resolve_path=True,
|
|
88
|
+
path_type=pathlib.Path,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
OPT_PORT = click.option("--port", "-p", type=int, default=8123)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@click.group()
|
|
95
|
+
def cli():
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@OPT_O
|
|
100
|
+
@OPT_C
|
|
101
|
+
@OPT_PORT
|
|
102
|
+
@click.option("--recreate", is_flag=True, default=False)
|
|
103
|
+
@click.option("--pull", is_flag=True, default=False)
|
|
104
|
+
@cli.command()
|
|
105
|
+
def up(
|
|
106
|
+
override: Optional[pathlib.Path],
|
|
107
|
+
config: Optional[pathlib.Path],
|
|
108
|
+
port: int,
|
|
109
|
+
recreate: bool,
|
|
110
|
+
pull: bool,
|
|
111
|
+
):
|
|
112
|
+
if not override and not config:
|
|
113
|
+
raise click.UsageError("Must provide either --override or --config")
|
|
114
|
+
with asyncio.Runner() as runner:
|
|
115
|
+
# check docker available
|
|
116
|
+
try:
|
|
117
|
+
runner.run(exec("docker", "--version"))
|
|
118
|
+
runner.run(exec("docker", "compose", "version"))
|
|
119
|
+
except click.exceptions.Exit:
|
|
120
|
+
click.echo("Docker not installed or not running")
|
|
121
|
+
return
|
|
122
|
+
# pull latest images
|
|
123
|
+
if pull:
|
|
124
|
+
runner.run(exec("docker", "pull", "langchain/langserve"))
|
|
125
|
+
# prepare args
|
|
126
|
+
stdin = langgraph_cli.docker.compose(port=port)
|
|
127
|
+
args = [
|
|
128
|
+
"--project-directory",
|
|
129
|
+
override.parent if override else config.parent,
|
|
130
|
+
"-f",
|
|
131
|
+
"-", # stdin
|
|
132
|
+
]
|
|
133
|
+
# apply options
|
|
134
|
+
if override:
|
|
135
|
+
args.extend(["-f", str(override)])
|
|
136
|
+
args.append("up")
|
|
137
|
+
if config:
|
|
138
|
+
with open(config) as f:
|
|
139
|
+
stdin += langgraph_cli.config.config_to_compose(config, json.load(f))
|
|
140
|
+
if recreate:
|
|
141
|
+
args.extend(["--force-recreate", "--remove-orphans"])
|
|
142
|
+
shutil.rmtree(".langserve-data", ignore_errors=True)
|
|
143
|
+
# run docker compose
|
|
144
|
+
runner.run(exec("docker", "compose", *args, input=stdin))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@OPT_O
|
|
148
|
+
@OPT_PORT
|
|
149
|
+
@cli.command()
|
|
150
|
+
def watch(override: pathlib.Path, port: int):
|
|
151
|
+
compose = langgraph_cli.docker.compose(port=port)
|
|
152
|
+
|
|
153
|
+
with asyncio.Runner() as runner:
|
|
154
|
+
try:
|
|
155
|
+
runner.run(
|
|
156
|
+
gather(
|
|
157
|
+
exec(
|
|
158
|
+
"docker",
|
|
159
|
+
"compose",
|
|
160
|
+
"--project-directory",
|
|
161
|
+
override.parent,
|
|
162
|
+
"-f",
|
|
163
|
+
"-",
|
|
164
|
+
"-f",
|
|
165
|
+
str(override),
|
|
166
|
+
"watch",
|
|
167
|
+
input=compose,
|
|
168
|
+
),
|
|
169
|
+
exec_loop(
|
|
170
|
+
"docker",
|
|
171
|
+
"compose",
|
|
172
|
+
"--project-directory",
|
|
173
|
+
override.parent,
|
|
174
|
+
"-f",
|
|
175
|
+
"-",
|
|
176
|
+
"-f",
|
|
177
|
+
str(override),
|
|
178
|
+
"logs",
|
|
179
|
+
"--follow",
|
|
180
|
+
"--since",
|
|
181
|
+
PLACEHOLDER_NOW,
|
|
182
|
+
"langserve",
|
|
183
|
+
input=compose,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
finally:
|
|
188
|
+
# docker compose watch doesn't clean up on exit, so we need to do it
|
|
189
|
+
runner.run(
|
|
190
|
+
exec(
|
|
191
|
+
"docker",
|
|
192
|
+
"compose",
|
|
193
|
+
"--project-directory",
|
|
194
|
+
override.parent,
|
|
195
|
+
"-f",
|
|
196
|
+
"-",
|
|
197
|
+
"-f",
|
|
198
|
+
str(override),
|
|
199
|
+
"kill",
|
|
200
|
+
input=compose,
|
|
201
|
+
)
|
|
202
|
+
)
|
langgraph_cli/config.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
from typing import TypedDict, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config(TypedDict):
|
|
8
|
+
dependencies: list[str]
|
|
9
|
+
graphs: dict[str, str]
|
|
10
|
+
env: Union[dict[str, str], str]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def config_to_compose(config_path: pathlib.Path, config: Config):
|
|
14
|
+
pypi_deps = [dep for dep in config["dependencies"] if not dep.startswith(".")]
|
|
15
|
+
local_pkgs = []
|
|
16
|
+
faux_pkgs = {}
|
|
17
|
+
locals_set = set()
|
|
18
|
+
|
|
19
|
+
for local_dep in config["dependencies"]:
|
|
20
|
+
if local_dep.startswith("."):
|
|
21
|
+
resolved = config_path.parent / local_dep
|
|
22
|
+
|
|
23
|
+
# validate local dependency
|
|
24
|
+
if not resolved.exists():
|
|
25
|
+
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
|
|
26
|
+
elif not resolved.is_dir():
|
|
27
|
+
raise NotADirectoryError(
|
|
28
|
+
f"Local dependency must be a directory: {resolved}"
|
|
29
|
+
)
|
|
30
|
+
elif resolved.name in locals_set:
|
|
31
|
+
raise ValueError(f"Duplicate local dependency: {resolved}")
|
|
32
|
+
else:
|
|
33
|
+
locals_set.add(resolved.name)
|
|
34
|
+
|
|
35
|
+
# if it's installable, add it to local_pkgs
|
|
36
|
+
# otherwise, add it to faux_pkgs, and create a pyproject.toml
|
|
37
|
+
files = os.listdir(resolved)
|
|
38
|
+
if "pyproject.toml" in files:
|
|
39
|
+
local_pkgs.append(local_dep)
|
|
40
|
+
elif "setup.py" in files:
|
|
41
|
+
local_pkgs.append(local_dep)
|
|
42
|
+
else:
|
|
43
|
+
faux_pkgs[resolved.name] = local_dep
|
|
44
|
+
|
|
45
|
+
for _, import_str in config["graphs"].items():
|
|
46
|
+
module_str, _, attrs_str = import_str.partition(":")
|
|
47
|
+
if not module_str or not attrs_str:
|
|
48
|
+
message = (
|
|
49
|
+
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
|
50
|
+
)
|
|
51
|
+
raise ValueError(message.format(import_str=import_str))
|
|
52
|
+
|
|
53
|
+
faux_pkgs_str = "\n\n".join(
|
|
54
|
+
f"ADD {path} /tmp/{name}/{name}\n RUN touch /tmp/{name}/pyproject.toml"
|
|
55
|
+
for name, path in faux_pkgs.items()
|
|
56
|
+
)
|
|
57
|
+
local_pkgs_str = f"ADD {' '.join(local_pkgs)} /tmp/" if local_pkgs else ""
|
|
58
|
+
env_vars_str = (
|
|
59
|
+
"\n".join(f" {k}: {v}" for k, v in config["env"].items())
|
|
60
|
+
if isinstance(config["env"], dict)
|
|
61
|
+
else ""
|
|
62
|
+
)
|
|
63
|
+
env_file_str = (
|
|
64
|
+
f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return f"""
|
|
68
|
+
LANGSERVE_GRAPHS: '{json.dumps(config["graphs"])}'
|
|
69
|
+
{env_vars_str}
|
|
70
|
+
{env_file_str}
|
|
71
|
+
pull_policy: build
|
|
72
|
+
build:
|
|
73
|
+
dockerfile_inline: |
|
|
74
|
+
FROM langchain/langserve
|
|
75
|
+
|
|
76
|
+
{local_pkgs_str}
|
|
77
|
+
|
|
78
|
+
{faux_pkgs_str}
|
|
79
|
+
|
|
80
|
+
RUN pip install {' '.join(pypi_deps)} /tmp/*
|
|
81
|
+
"""
|
langgraph_cli/docker.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
ROOT = pathlib.Path(__file__).parent.parent.resolve()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DB = f"""
|
|
9
|
+
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
|
+
- ./.langserve-data:/var/lib/postgresql/data
|
|
26
|
+
- {ROOT}/initdb:/docker-entrypoint-initdb.d
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compose(
|
|
31
|
+
*,
|
|
32
|
+
# postgres://user:password@host:port/database?option=value
|
|
33
|
+
postgres_uri: Optional[str] = None,
|
|
34
|
+
port: int,
|
|
35
|
+
) -> str:
|
|
36
|
+
if postgres_uri is None:
|
|
37
|
+
include_db = True
|
|
38
|
+
postgres_uri = "postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable&search_path=langserve"
|
|
39
|
+
else:
|
|
40
|
+
include_db = False
|
|
41
|
+
|
|
42
|
+
return f"""
|
|
43
|
+
services:
|
|
44
|
+
{DB if include_db else ""}
|
|
45
|
+
migrate:
|
|
46
|
+
image: langchain/langserve-migrate
|
|
47
|
+
pull_policy: always
|
|
48
|
+
{'''depends_on:
|
|
49
|
+
postgres:
|
|
50
|
+
condition: service_healthy''' if include_db else ""}
|
|
51
|
+
environment:
|
|
52
|
+
POSTGRES_URI: {postgres_uri}
|
|
53
|
+
langserve:
|
|
54
|
+
image: langchain/langserve
|
|
55
|
+
restart: on-failure:3
|
|
56
|
+
ports:
|
|
57
|
+
- "{port}:8000"
|
|
58
|
+
depends_on:
|
|
59
|
+
migrate:
|
|
60
|
+
condition: service_completed_successfully
|
|
61
|
+
environment:
|
|
62
|
+
POSTGRES_URI: {postgres_uri}
|
|
63
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: langgraph-cli
|
|
3
|
+
Version: 0.1.0
|
|
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,8 @@
|
|
|
1
|
+
langgraph_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
langgraph_cli/cli.py,sha256=pgCOyXO7PGCFYr4QAnfNhEhsHendIWTkw8m7i22dICM,5685
|
|
3
|
+
langgraph_cli/config.py,sha256=ZtVy9xfzCEb0HZyuSRhIh5895dnsKMTZ_JbNcoyqEp4,2738
|
|
4
|
+
langgraph_cli/docker.py,sha256=O9kIheEbPYizsy72Ygj29l301Ods53DXG8aiQXZc-Dc,1616
|
|
5
|
+
langgraph_cli-0.1.0.dist-info/METADATA,sha256=PYvi7zZojPUb4gL6gOC3GbCyDzd5iHDF26p9YhdKxA0,443
|
|
6
|
+
langgraph_cli-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
7
|
+
langgraph_cli-0.1.0.dist-info/entry_points.txt,sha256=pq7AcWiYJM1-9PtAAF5jZKrOJw6WxnmYuVR_h6i1j4g,51
|
|
8
|
+
langgraph_cli-0.1.0.dist-info/RECORD,,
|