Skip to content

Commit 74134dd

Browse files
Erick Friisnfcampos
andauthored
cli pyproject updating (langchain-ai#12945)
`langchain app add` and `langchain app remove` will now keep the dependencies list updated. --------- Co-authored-by: Nuno Campos <nuno@boringbits.io>
1 parent d9abcf1 commit 74134dd

File tree

8 files changed

+346
-176
lines changed

8 files changed

+346
-176
lines changed

libs/cli/langchain_cli/dev_scripts.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66

77
from fastapi import FastAPI
88
from langserve import add_routes
9-
from langserve.packages import get_langserve_export
109

11-
from langchain_cli.utils.packages import get_package_root
10+
from langchain_cli.utils.packages import get_langserve_export, get_package_root
1211

1312

1413
def create_demo_server(

libs/cli/langchain_cli/namespaces/app.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from typing import Dict, List, Optional, Tuple
1010

1111
import typer
12-
from langserve.packages import get_langserve_export
1312
from typing_extensions import Annotated
1413

1514
from langchain_cli.utils.events import create_events
@@ -19,7 +18,15 @@
1918
parse_dependencies,
2019
update_repo,
2120
)
22-
from langchain_cli.utils.packages import get_package_root
21+
from langchain_cli.utils.packages import (
22+
LangServeExport,
23+
get_langserve_export,
24+
get_package_root,
25+
)
26+
from langchain_cli.utils.pyproject import (
27+
add_dependencies_to_pyproject_toml,
28+
remove_dependencies_from_pyproject_toml,
29+
)
2330

2431
REPO_DIR = Path(typer.get_app_dir("langchain")) / "git_repos"
2532

@@ -98,7 +105,8 @@ def add(
98105
grouped[key_tup] = lst
99106

100107
installed_destination_paths: List[Path] = []
101-
installed_exports: List[Dict] = []
108+
installed_destination_names: List[str] = []
109+
installed_exports: List[LangServeExport] = []
102110

103111
for (git, ref), group_deps in grouped.items():
104112
if len(group_deps) == 1:
@@ -131,23 +139,38 @@ def add(
131139
copy_repo(source_path, destination_path)
132140
typer.echo(f" - Downloaded {dep['subdirectory']} to {inner_api_path}")
133141
installed_destination_paths.append(destination_path)
142+
installed_destination_names.append(inner_api_path)
134143
installed_exports.append(langserve_export)
135144

136145
if len(installed_destination_paths) == 0:
137146
typer.echo("No packages installed. Exiting.")
138147
return
139148

140-
cwd = Path.cwd()
141-
installed_desination_strs = [
142-
str(p.relative_to(cwd)) for p in installed_destination_paths
143-
]
144-
cmd = ["pip", "install", "-e"] + installed_desination_strs
145-
cmd_str = " \\\n ".join(installed_desination_strs)
146-
install_str = f"To install:\n\npip install -e \\\n {cmd_str}"
147-
typer.echo(install_str)
148-
149-
if typer.confirm("Run it?"):
150-
subprocess.run(cmd, cwd=cwd)
149+
try:
150+
add_dependencies_to_pyproject_toml(
151+
project_root / "pyproject.toml",
152+
zip(installed_destination_names, installed_destination_paths),
153+
)
154+
except Exception:
155+
# Can fail if user modified/removed pyproject.toml
156+
typer.echo("Failed to add dependencies to pyproject.toml, continuing...")
157+
158+
try:
159+
cwd = Path.cwd()
160+
installed_destination_strs = [
161+
str(p.relative_to(cwd)) for p in installed_destination_paths
162+
]
163+
except ValueError:
164+
# Can fail if the cwd is not a parent of the package
165+
typer.echo("Failed to print install command, continuing...")
166+
else:
167+
cmd = ["pip", "install", "-e"] + installed_destination_strs
168+
cmd_str = " \\\n ".join(installed_destination_strs)
169+
install_str = f"To install:\n\npip install -e \\\n {cmd_str}"
170+
typer.echo(install_str)
171+
172+
if typer.confirm("Run it?"):
173+
subprocess.run(cmd, cwd=cwd)
151174

152175
if typer.confirm("\nGenerate route code for these packages?", default=True):
153176
chain_names = []
@@ -187,20 +210,43 @@ def add(
187210
@app_cli.command()
188211
def remove(
189212
api_paths: Annotated[List[str], typer.Argument(help="The API paths to remove")],
213+
*,
214+
project_dir: Annotated[
215+
Optional[Path], typer.Option(help="The project directory")
216+
] = None,
190217
):
191218
"""
192219
Removes the specified package from the current LangServe app.
193220
"""
221+
222+
project_root = get_package_root(project_dir)
223+
224+
project_pyproject = project_root / "pyproject.toml"
225+
226+
package_root = project_root / "packages"
227+
228+
remove_deps: List[str] = []
229+
194230
for api_path in api_paths:
195-
package_dir = Path.cwd() / "packages" / api_path
231+
package_dir = package_root / api_path
196232
if not package_dir.exists():
197-
typer.echo(f"Endpoint {api_path} does not exist. Skipping...")
233+
typer.echo(f"Package {api_path} does not exist. Skipping...")
198234
continue
199-
pyproject = package_dir / "pyproject.toml"
200-
langserve_export = get_langserve_export(pyproject)
201-
typer.echo(f"Removing {langserve_export['package_name']}...")
202-
203-
shutil.rmtree(package_dir)
235+
try:
236+
pyproject = package_dir / "pyproject.toml"
237+
langserve_export = get_langserve_export(pyproject)
238+
typer.echo(f"Removing {langserve_export['package_name']}...")
239+
240+
shutil.rmtree(package_dir)
241+
remove_deps.append(api_path)
242+
except Exception:
243+
pass
244+
245+
try:
246+
remove_dependencies_from_pyproject_toml(project_pyproject, remove_deps)
247+
except Exception:
248+
# Can fail if user modified/removed pyproject.toml
249+
typer.echo("Failed to remove dependencies from pyproject.toml.")
204250

205251

206252
@app_cli.command()

libs/cli/langchain_cli/namespaces/template.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
from typing import Optional
1010

1111
import typer
12-
from langserve.packages import get_langserve_export
1312
from typing_extensions import Annotated
1413

15-
from langchain_cli.utils.packages import get_package_root
14+
from langchain_cli.utils.packages import get_langserve_export, get_package_root
1615

1716
package_cli = typer.Typer(no_args_is_help=True, add_completion=False)
1817

libs/cli/langchain_cli/project_template/pyproject.toml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
[tool.poetry]
2-
name = "langservehub-template"
2+
name = "__app_name__"
33
version = "0.1.0"
44
description = ""
55
authors = ["Your Name <you@example.com>"]
66
readme = "README.md"
77

88
[tool.poetry.dependencies]
99
python = "^3.11"
10-
sse-starlette = "^1.6.5"
11-
tomli-w = "^1.0.0"
1210
uvicorn = "^0.23.2"
13-
fastapi = "^0.103.2"
14-
langserve = ">=0.0.16"
11+
langserve = {extras = ["server"], version = ">=0.0.22"}
1512

16-
[tool.poetry.group.dev.dependencies]
17-
uvicorn = "^0.23.2"
18-
pygithub = "^2.1.1"
1913

14+
[tool.poetry.group.dev.dependencies]
15+
langchain-cli = ">=0.0.15"
2016

2117
[build-system]
2218
requires = ["poetry-core"]

libs/cli/langchain_cli/utils/packages.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pathlib import Path
2-
from typing import Optional, Set
2+
from typing import Any, Dict, Optional, Set, TypedDict
3+
4+
from tomlkit import load
35

46

57
def get_package_root(cwd: Optional[Path] = None) -> Path:
@@ -14,3 +16,30 @@ def get_package_root(cwd: Optional[Path] = None) -> Path:
1416
return package_root
1517
package_root = package_root.parent
1618
raise FileNotFoundError("No pyproject.toml found")
19+
20+
21+
class LangServeExport(TypedDict):
22+
"""
23+
Fields from pyproject.toml that are relevant to LangServe
24+
25+
Attributes:
26+
module: The module to import from, tool.langserve.export_module
27+
attr: The attribute to import from the module, tool.langserve.export_attr
28+
package_name: The name of the package, tool.poetry.name
29+
"""
30+
31+
module: str
32+
attr: str
33+
package_name: str
34+
35+
36+
def get_langserve_export(filepath: Path) -> LangServeExport:
37+
with open(filepath) as f:
38+
data: Dict[str, Any] = load(f)
39+
try:
40+
module = data["tool"]["langserve"]["export_module"]
41+
attr = data["tool"]["langserve"]["export_attr"]
42+
package_name = data["tool"]["poetry"]["name"]
43+
except KeyError as e:
44+
raise KeyError("Invalid LangServe PyProject.toml") from e
45+
return LangServeExport(module=module, attr=attr, package_name=package_name)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from pathlib import Path
2+
from typing import Any, Dict, Iterable
3+
4+
from tomlkit import dump, inline_table, load
5+
from tomlkit.items import InlineTable
6+
7+
8+
def _get_dep_inline_table(path: Path) -> InlineTable:
9+
dep = inline_table()
10+
dep.update({"path": str(path), "develop": True})
11+
return dep
12+
13+
14+
def add_dependencies_to_pyproject_toml(
15+
pyproject_toml: Path, local_editable_dependencies: Iterable[tuple[str, Path]]
16+
) -> None:
17+
"""Add dependencies to pyproject.toml."""
18+
with open(pyproject_toml, encoding="utf-8") as f:
19+
# tomlkit types aren't amazing - treat as Dict instead
20+
pyproject: Dict[str, Any] = load(f)
21+
pyproject["tool"]["poetry"]["dependencies"].update(
22+
{
23+
name: _get_dep_inline_table(loc.relative_to(pyproject_toml.parent))
24+
for name, loc in local_editable_dependencies
25+
}
26+
)
27+
with open(pyproject_toml, "w", encoding="utf-8") as f:
28+
dump(pyproject, f)
29+
30+
31+
def remove_dependencies_from_pyproject_toml(
32+
pyproject_toml: Path, local_editable_dependencies: Iterable[str]
33+
) -> None:
34+
"""Remove dependencies from pyproject.toml."""
35+
with open(pyproject_toml, encoding="utf-8") as f:
36+
pyproject: Dict[str, Any] = load(f)
37+
# tomlkit types aren't amazing - treat as Dict instead
38+
dependencies = pyproject["tool"]["poetry"]["dependencies"]
39+
for name in local_editable_dependencies:
40+
try:
41+
del dependencies[name]
42+
except KeyError:
43+
pass
44+
with open(pyproject_toml, "w", encoding="utf-8") as f:
45+
dump(pyproject, f)

0 commit comments

Comments
 (0)