langgraph-cli 0.1.0__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary:
5
5
  Author: Nuno Campos
6
6
  Author-email: nuno@langchain.dev
@@ -67,8 +67,9 @@ async def gather(*coros: Coroutine):
67
67
 
68
68
 
69
69
  OPT_O = click.option(
70
- "--override",
71
- "-o",
70
+ "--docker-compose",
71
+ "-d",
72
+ help="Advanced: Path to docker-compose.yml file with additional services to launch",
72
73
  type=click.Path(
73
74
  exists=True,
74
75
  file_okay=True,
@@ -80,6 +81,23 @@ OPT_O = click.option(
80
81
  OPT_C = click.option(
81
82
  "--config",
82
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",
83
101
  type=click.Path(
84
102
  exists=True,
85
103
  file_okay=True,
@@ -88,7 +106,9 @@ OPT_C = click.option(
88
106
  path_type=pathlib.Path,
89
107
  ),
90
108
  )
91
- OPT_PORT = click.option("--port", "-p", type=int, default=8123)
109
+ OPT_PORT = click.option(
110
+ "--port", "-p", type=int, default=8123, show_default=True, help="Port to expose"
111
+ )
92
112
 
93
113
 
94
114
  @click.group()
@@ -96,21 +116,26 @@ def cli():
96
116
  pass
97
117
 
98
118
 
119
+ @click.option(
120
+ "--recreate/--no-recreate",
121
+ default=False,
122
+ show_default=True,
123
+ help="Clear previous data",
124
+ )
125
+ @click.option(
126
+ "--pull/--no-pull", default=True, show_default=True, help="Pull latest images"
127
+ )
128
+ @OPT_PORT
99
129
  @OPT_O
100
130
  @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()
131
+ @cli.command(help="Start langgraph API server")
105
132
  def up(
106
- override: Optional[pathlib.Path],
107
- config: Optional[pathlib.Path],
133
+ config: pathlib.Path,
134
+ docker_compose: Optional[pathlib.Path],
108
135
  port: int,
109
136
  recreate: bool,
110
137
  pull: bool,
111
138
  ):
112
- if not override and not config:
113
- raise click.UsageError("Must provide either --override or --config")
114
139
  with asyncio.Runner() as runner:
115
140
  # check docker available
116
141
  try:
@@ -126,13 +151,13 @@ def up(
126
151
  stdin = langgraph_cli.docker.compose(port=port)
127
152
  args = [
128
153
  "--project-directory",
129
- override.parent if override else config.parent,
154
+ config.parent,
130
155
  "-f",
131
156
  "-", # stdin
132
157
  ]
133
158
  # apply options
134
- if override:
135
- args.extend(["-f", str(override)])
159
+ if docker_compose:
160
+ args.extend(["-f", str(docker_compose)])
136
161
  args.append("up")
137
162
  if config:
138
163
  with open(config) as f:
@@ -0,0 +1,110 @@
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: dict[pathlib.Path, str] = {}
16
+ faux_pkgs: dict[pathlib.Path, str] = {}
17
+ pkg_names = 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 pkg_names:
31
+ raise ValueError(f"Duplicate local dependency: {resolved}")
32
+ else:
33
+ pkg_names.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[resolved] = local_dep
40
+ elif "setup.py" in files:
41
+ local_pkgs[resolved] = local_dep
42
+ else:
43
+ faux_pkgs[resolved] = local_dep
44
+
45
+ for graph_id, import_str in config["graphs"].items():
46
+ module_str, _, attr_str = import_str.partition(":")
47
+ if not module_str or not attr_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
+ if module_str.startswith("."):
53
+ resolved = config_path.parent / module_str
54
+ if not resolved.exists():
55
+ raise FileNotFoundError(f"Could not find local module: {resolved}")
56
+ elif not resolved.is_file():
57
+ raise IsADirectoryError(f"Local module must be a file: {resolved}")
58
+ else:
59
+ for local_pkg in local_pkgs:
60
+ if resolved.is_relative_to(local_pkg):
61
+ resolved = resolved.relative_to(local_pkg)
62
+ break
63
+ else:
64
+ for faux_pkg in faux_pkgs:
65
+ if resolved.is_relative_to(faux_pkg):
66
+ resolved = resolved.relative_to(faux_pkg.parent)
67
+ break
68
+ else:
69
+ raise ValueError(
70
+ f"Module '{import_str}' not found in 'dependencies' list"
71
+ "Add its containing package to 'dependencies' list."
72
+ )
73
+ # rewrite module_str to be a python import path
74
+ module_str = f"{'.'.join(resolved.parts[:-1])}"
75
+ if resolved.stem == "__init__":
76
+ pass
77
+ else:
78
+ module_str = f"{module_str}.{resolved.stem}"
79
+ # update the config
80
+ config["graphs"][graph_id] = f"{module_str}:{attr_str}"
81
+
82
+ faux_pkgs_str = "\n\n".join(
83
+ f"ADD {relpath} /tmp/{fullpath.name}/{fullpath.name}\n RUN touch /tmp/{fullpath.name}/pyproject.toml"
84
+ for fullpath, relpath in faux_pkgs.items()
85
+ )
86
+ local_pkgs_str = f"ADD {' '.join(local_pkgs.values())} /tmp/" if local_pkgs else ""
87
+ env_vars_str = (
88
+ "\n".join(f" {k}: {v}" for k, v in config["env"].items())
89
+ if isinstance(config["env"], dict)
90
+ else ""
91
+ )
92
+ env_file_str = (
93
+ f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
94
+ )
95
+
96
+ return f"""
97
+ LANGSERVE_GRAPHS: '{json.dumps(config["graphs"])}'
98
+ {env_vars_str}
99
+ {env_file_str}
100
+ pull_policy: build
101
+ build:
102
+ dockerfile_inline: |
103
+ FROM langchain/langserve
104
+
105
+ {local_pkgs_str}
106
+
107
+ {faux_pkgs_str}
108
+
109
+ RUN pip install {' '.join(pypi_deps)} /tmp/*
110
+ """
@@ -2,7 +2,7 @@ import pathlib
2
2
  from typing import Optional
3
3
 
4
4
 
5
- ROOT = pathlib.Path(__file__).parent.parent.resolve()
5
+ ROOT = pathlib.Path(__file__).parent.resolve()
6
6
 
7
7
 
8
8
  DB = f"""
@@ -0,0 +1 @@
1
+ create schema if not exists langserve;
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = ""
5
5
  authors = ["Nuno Campos <nuno@langchain.dev>"]
6
6
  readme = "README.md"
@@ -1,81 +0,0 @@
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
- """
File without changes