From bf0904b676f458386096a008155ffeb805bc52c5 Mon Sep 17 00:00:00 2001 From: Zander Chase <130414180+vowelparrot@users.noreply.github.com> Date: Tue, 16 May 2023 00:44:30 +0000 Subject: [PATCH] Add Server Command (#4695) Add Support for `langchain server {start|stop}` commands, with support for using ngrok to tunnel to a remote notebook --- langchain/callbacks/tracers/schemas.py | 2 - langchain/cli/__init__.py | 0 langchain/cli/conf/nginx.conf | 16 ++ langchain/cli/docker-compose.ngrok.yaml | 17 ++ langchain/cli/docker-compose.yaml | 46 +++++ langchain/cli/main.py | 223 ++++++++++++++++++++++++ pyproject.toml | 1 + 7 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 langchain/cli/__init__.py create mode 100644 langchain/cli/conf/nginx.conf create mode 100644 langchain/cli/docker-compose.ngrok.yaml create mode 100644 langchain/cli/docker-compose.yaml create mode 100644 langchain/cli/main.py diff --git a/langchain/callbacks/tracers/schemas.py b/langchain/callbacks/tracers/schemas.py index 5e036667..d34bb182 100644 --- a/langchain/callbacks/tracers/schemas.py +++ b/langchain/callbacks/tracers/schemas.py @@ -23,8 +23,6 @@ class TracerSessionV1Base(BaseModel): class TracerSessionV1Create(TracerSessionV1Base): """Create class for TracerSessionV1.""" - pass - class TracerSessionV1(TracerSessionV1Base): """TracerSessionV1 schema.""" diff --git a/langchain/cli/__init__.py b/langchain/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/langchain/cli/conf/nginx.conf b/langchain/cli/conf/nginx.conf new file mode 100644 index 00000000..317c81a5 --- /dev/null +++ b/langchain/cli/conf/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name localhost; + error_log /var/log/nginx/error.log warn; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/langchain/cli/docker-compose.ngrok.yaml b/langchain/cli/docker-compose.ngrok.yaml new file mode 100644 index 00000000..e094ae5a --- /dev/null +++ b/langchain/cli/docker-compose.ngrok.yaml @@ -0,0 +1,17 @@ +version: '3' +services: + ngrok: + image: ngrok/ngrok:latest + restart: unless-stopped + command: + - "start" + - "--all" + - "--config" + - "/etc/ngrok.yml" + volumes: + - ./ngrok_config.yaml:/etc/ngrok.yml + ports: + - 4040:4040 + langchain-backend: + depends_on: + - ngrok diff --git a/langchain/cli/docker-compose.yaml b/langchain/cli/docker-compose.yaml new file mode 100644 index 00000000..bcaad011 --- /dev/null +++ b/langchain/cli/docker-compose.yaml @@ -0,0 +1,46 @@ +version: '3' +services: + langchain-frontend: + image: langchain/langchainplus-frontend:latest + ports: + - 80:80 + environment: + - BACKEND_URL=http://langchain-backend:8000 + - PUBLIC_BASE_URL=http://localhost:8000 + - PUBLIC_DEV_MODE=true + depends_on: + - langchain-backend + volumes: + - ./conf/nginx.conf:/etc/nginx/default.conf:ro + build: + context: frontend-react/. + dockerfile: Dockerfile + langchain-backend: + image: langchain/langchainplus-backend:latest + environment: + - PORT=8000 + - LANGCHAIN_ENV=local_docker + - LOG_LEVEL=warning + ports: + - 8000:8000 + depends_on: + - langchain-db + build: + context: backend/. + dockerfile: Dockerfile + langchain-db: + image: postgres:14.1 + command: + [ + "postgres", + "-c", + "log_min_messages=WARNING", + "-c", + "client_min_messages=WARNING" + ] + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_USER=postgres + - POSTGRES_DB=postgres + ports: + - 5433:5432 diff --git a/langchain/cli/main.py b/langchain/cli/main.py new file mode 100644 index 00000000..74b8830e --- /dev/null +++ b/langchain/cli/main.py @@ -0,0 +1,223 @@ +import argparse +import logging +import os +import shutil +import subprocess +from contextlib import contextmanager +from pathlib import Path +from typing import Generator, List, Optional + +import requests +import yaml + +from langchain.env import get_runtime_environment + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +_DIR = Path(__file__).parent + + +def get_docker_compose_command() -> List[str]: + if shutil.which("docker-compose") is None: + return ["docker", "compose"] + else: + return ["docker-compose"] + + +def get_ngrok_url(auth_token: Optional[str]) -> str: + """Get the ngrok URL for the LangChainPlus server.""" + ngrok_url = "http://localhost:4040/api/tunnels" + try: + response = requests.get(ngrok_url) + response.raise_for_status() + exposed_url = response.json()["tunnels"][0]["public_url"] + except requests.exceptions.HTTPError: + raise ValueError("Could not connect to ngrok console.") + except (KeyError, IndexError): + message = "ngrok failed to start correctly. " + if auth_token is not None: + message += "Please check that your authtoken is correct." + raise ValueError(message) + return exposed_url + + +@contextmanager +def create_ngrok_config( + auth_token: Optional[str] = None, +) -> Generator[Path, None, None]: + """Create the ngrok configuration file.""" + config_path = _DIR / "ngrok_config.yaml" + if config_path.exists(): + # If there was an error in a prior run, it's possible + # Docker made this a directory instead of a file + if config_path.is_dir(): + shutil.rmtree(config_path) + else: + config_path.unlink() + ngrok_config = { + "tunnels": { + "langchain": { + "proto": "http", + "addr": "langchain-backend:8000", + } + }, + "version": "2", + "region": "us", + } + if auth_token is not None: + ngrok_config["authtoken"] = auth_token + config_path = _DIR / "ngrok_config.yaml" + with config_path.open("w") as f: + yaml.dump(ngrok_config, f) + yield config_path + # Delete the config file after use + config_path.unlink(missing_ok=True) + + +class ServerCommand: + """Manage the LangChainPlus Tracing server.""" + + def __init__(self) -> None: + self.docker_compose_command = get_docker_compose_command() + self.docker_compose_file = ( + Path(__file__).absolute().parent / "docker-compose.yaml" + ) + self.ngrok_path = Path(__file__).absolute().parent / "docker-compose.ngrok.yaml" + + def _start_local(self) -> None: + command = [ + *self.docker_compose_command, + "-f", + str(self.docker_compose_file), + ] + subprocess.run( + [ + *command, + "up", + "--pull=always", + "--quiet-pull", + "--wait", + ] + ) + logger.info( + "LangChain server is running at http://localhost. To connect" + " locally, set the following environment variable" + " when running your LangChain application." + ) + + logger.info("\tLANGCHAIN_TRACING_V2=true") + subprocess.run(["open", "http://localhost"]) + + def _start_and_expose(self, auth_token: Optional[str]) -> None: + with create_ngrok_config(auth_token=auth_token): + command = [ + *self.docker_compose_command, + "-f", + str(self.docker_compose_file), + "-f", + str(self.ngrok_path), + ] + subprocess.run( + [ + *command, + "up", + "--pull=always", + "--quiet-pull", + "--wait", + ] + ) + logger.info( + "ngrok is running. You can view the dashboard at http://0.0.0.0:4040" + ) + ngrok_url = get_ngrok_url(auth_token) + logger.info( + "LangChain server is running at http://localhost." + " To connect remotely, set the following environment" + " variable when running your LangChain application." + ) + logger.info("\tLANGCHAIN_TRACING_V2=true") + logger.info(f"\tLANGCHAIN_ENDPOINT={ngrok_url}") + subprocess.run(["open", "http://localhost"]) + + def start(self, *, expose: bool = False, auth_token: Optional[str] = None) -> None: + """Run the LangChainPlus server locally. + + Args: + expose: If True, expose the server to the internet using ngrok. + auth_token: The ngrok authtoken to use (visible in the ngrok dashboard). + If not provided, ngrok server session length will be restricted. + """ + + if expose: + self._start_and_expose(auth_token=auth_token) + else: + self._start_local() + + def stop(self) -> None: + """Stop the LangChainPlus server.""" + subprocess.run( + [ + *self.docker_compose_command, + "-f", + str(self.docker_compose_file), + "-f", + str(self.ngrok_path), + "down", + ] + ) + + +def env() -> None: + """Print the runtime environment information.""" + env = get_runtime_environment() + logger.info("LangChain Environment:") + logger.info("\n".join(f"{k}:{v}" for k, v in env.items())) + + +def main() -> None: + """Main entrypoint for the CLI.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(description="LangChainPlus CLI commands") + + server_command = ServerCommand() + server_parser = subparsers.add_parser("server", description=server_command.__doc__) + server_subparsers = server_parser.add_subparsers() + + server_start_parser = server_subparsers.add_parser( + "start", description="Start the LangChainPlus server." + ) + server_start_parser.add_argument( + "--expose", + action="store_true", + help="Expose the server to the internet using ngrok.", + ) + server_start_parser.add_argument( + "--ngrok-authtoken", + default=os.getenv("NGROK_AUTHTOKEN"), + help="The ngrok authtoken to use (visible in the ngrok dashboard)." + " If not provided, ngrok server session length will be restricted.", + ) + server_start_parser.set_defaults( + func=lambda args: server_command.start( + expose=args.expose, auth_token=args.ngrok_authtoken + ) + ) + + server_stop_parser = server_subparsers.add_parser( + "stop", description="Stop the LangChainPlus server." + ) + server_stop_parser.set_defaults(func=lambda args: server_command.stop()) + + env_parser = subparsers.add_parser("env") + env_parser.set_defaults(func=lambda args: env()) + + args = parser.parse_args() + if not hasattr(args, "func"): + parser.print_help() + return + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 79f26d27..92948f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ repository = "https://www.github.com/hwchase17/langchain" [tool.poetry.scripts] langchain-server = "langchain.server:main" +langchain = "langchain.cli.main:main" [tool.poetry.dependencies] python = ">=3.8.1,<4.0"