Coverage for langsmith/cli/main.py: 0%
121 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-05 18:36 -0800
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-05 18:36 -0800
1import argparse
2import json
3import logging
4import os
5import subprocess
6from pathlib import Path
7from typing import Dict, List, Mapping, Optional, Union, cast
9from langsmith import env as ls_env
10from langsmith import utils as ls_utils
12logging.basicConfig(level=logging.INFO, format="%(message)s")
13logger = logging.getLogger(__name__)
15_DIR = Path(__file__).parent
18def pprint_services(services_status: List[Mapping[str, Union[str, List[str]]]]) -> None:
19 # Loop through and collect Service, State, and Publishers["PublishedPorts"]
20 # for each service
21 services = []
22 for service in services_status:
23 service_status: Dict[str, str] = {
24 "Service": str(service["Service"]),
25 "Status": str(service["Status"]),
26 }
27 publishers = cast(List[Dict], service.get("Publishers", []))
28 if publishers:
29 service_status["PublishedPorts"] = ", ".join(
30 [str(publisher["PublishedPort"]) for publisher in publishers]
31 )
32 services.append(service_status)
34 max_service_len = max(len(service["Service"]) for service in services)
35 max_state_len = max(len(service["Status"]) for service in services)
36 service_message = [
37 "\n"
38 + "Service".ljust(max_service_len + 2)
39 + "Status".ljust(max_state_len + 2)
40 + "Published Ports"
41 ]
42 for service in services:
43 service_str = service["Service"].ljust(max_service_len + 2)
44 state_str = service["Status"].ljust(max_state_len + 2)
45 ports_str = service.get("PublishedPorts", "")
46 service_message.append(service_str + state_str + ports_str)
48 service_message.append(
49 "\nTo connect, set the following environment variables"
50 " in your LangChain application:"
51 "\nLANGSMITH_TRACING_V2=true"
52 "\nLANGSMITH_ENDPOINT=http://localhost:80/api"
53 )
54 logger.info("\n".join(service_message))
57class LangSmithCommand:
58 """Manage the LangSmith Tracing server."""
60 def __init__(self) -> None:
61 self.docker_compose_file = (
62 Path(__file__).absolute().parent / "docker-compose.yaml"
63 )
65 @property
66 def docker_compose_command(self) -> List[str]:
67 return ls_utils.get_docker_compose_command()
69 def _open_browser(self, url: str) -> None:
70 try:
71 subprocess.run(["open", url])
72 except FileNotFoundError:
73 pass
75 def _start_local(self) -> None:
76 command = [
77 *self.docker_compose_command,
78 "-f",
79 str(self.docker_compose_file),
80 ]
81 subprocess.run(
82 [
83 *command,
84 "up",
85 "--quiet-pull",
86 "--wait",
87 ]
88 )
89 logger.info(
90 "LangSmith server is running at http://localhost:80/api.\n"
91 "To view the app, navigate your browser to http://localhost:80"
92 "\n\nTo connect your LangChain application to the server"
93 " locally,\nset the following environment variable"
94 " when running your LangChain application.\n"
95 )
97 logger.info("\tLANGSMITH_TRACING=true")
98 logger.info("\tLANGSMITH_ENDPOINT=http://localhost:80/api\n")
99 self._open_browser("http://localhost")
101 def pull(
102 self,
103 *,
104 version: str = "0.5.7",
105 ) -> None:
106 """Pull the latest LangSmith images.
108 Args:
109 version: The LangSmith version to use for LangSmith. Defaults to 0.5.7
110 """
111 os.environ["_LANGSMITH_IMAGE_VERSION"] = version
112 subprocess.run(
113 [
114 *self.docker_compose_command,
115 "-f",
116 str(self.docker_compose_file),
117 "pull",
118 ]
119 )
121 def start(
122 self,
123 *,
124 openai_api_key: Optional[str] = None,
125 langsmith_license_key: str,
126 version: str = "0.5.7",
127 ) -> None:
128 """Run the LangSmith server locally.
130 Args:
131 openai_api_key: The OpenAI API key to use for LangSmith
132 If not provided, the OpenAI API Key will be read from the
133 OPENAI_API_KEY environment variable. If neither are provided,
134 some features of LangSmith will not be available.
135 langsmith_license_key: The LangSmith license key to use for LangSmith
136 If not provided, the LangSmith license key will be read from the
137 LANGSMITH_LICENSE_KEY environment variable. If neither are provided,
138 Langsmith will not start up.
139 version: The LangSmith version to use for LangSmith. Defaults to latest.
140 """
141 if openai_api_key is not None:
142 os.environ["OPENAI_API_KEY"] = openai_api_key
143 if langsmith_license_key is not None:
144 os.environ["LANGSMITH_LICENSE_KEY"] = langsmith_license_key
145 self.pull(version=version)
146 self._start_local()
148 def stop(self, clear_volumes: bool = False) -> None:
149 """Stop the LangSmith server."""
150 cmd = [
151 *self.docker_compose_command,
152 "-f",
153 str(self.docker_compose_file),
154 "down",
155 ]
156 if clear_volumes:
157 confirm = input(
158 "You are about to delete all the locally cached "
159 "LangSmith containers and volumes. "
160 "This operation cannot be undone. Are you sure? [y/N]"
161 )
162 if confirm.lower() != "y":
163 print("Aborting.")
164 return
165 cmd.append("--volumes")
167 subprocess.run(cmd)
169 def logs(self) -> None:
170 """Print the logs from the LangSmith server."""
171 subprocess.run(
172 [
173 *self.docker_compose_command,
174 "-f",
175 str(self.docker_compose_file),
176 "logs",
177 ]
178 )
180 def status(self) -> None:
181 """Provide information about the status LangSmith server."""
182 command = [
183 *self.docker_compose_command,
184 "-f",
185 str(self.docker_compose_file),
186 "ps",
187 "--format",
188 "json",
189 ]
191 result = subprocess.run(
192 command,
193 stdout=subprocess.PIPE,
194 stderr=subprocess.PIPE,
195 )
196 try:
197 command_stdout = result.stdout.decode("utf-8")
198 services_status = json.loads(command_stdout)
199 except json.JSONDecodeError:
200 logger.error("Error checking LangSmith server status.")
201 return
202 if services_status:
203 logger.info("The LangSmith server is currently running.")
204 pprint_services(services_status)
205 else:
206 logger.info("The LangSmith server is not running.")
207 return
210def env() -> None:
211 """Print the runtime environment information."""
212 env = ls_env.get_runtime_environment()
213 env.update(ls_env.get_docker_environment())
214 env.update(ls_env.get_langchain_env_vars())
216 # calculate the max length of keys
217 max_key_length = max(len(key) for key in env.keys())
219 logger.info("LangChain Environment:")
220 for k, v in env.items():
221 logger.info(f"{k:{max_key_length}}: {v}")
224def main() -> None:
225 """Main entrypoint for the CLI."""
226 print("BY USING THIS SOFTWARE YOU AGREE TO THE TERMS OF SERVICE AT:")
227 print("https://smith.langchain.com/terms-of-service.pdf")
229 parser = argparse.ArgumentParser()
230 subparsers = parser.add_subparsers(description="LangSmith CLI commands")
232 server_command = LangSmithCommand()
233 server_start_parser = subparsers.add_parser(
234 "start", description="Start the LangSmith server."
235 )
236 server_start_parser.add_argument(
237 "--openai-api-key",
238 default=os.getenv("OPENAI_API_KEY"),
239 help="The OpenAI API key to use for LangSmith."
240 " If not provided, the OpenAI API Key will be read from the"
241 " OPENAI_API_KEY environment variable. If neither are provided,"
242 " some features of LangSmith will not be available.",
243 )
244 server_start_parser.add_argument(
245 "--langsmith-license-key",
246 default=os.getenv("LANGSMITH_LICENSE_KEY"),
247 help="The LangSmith license key to use for LangSmith."
248 " If not provided, the LangSmith License Key will be read from the"
249 " LANGSMITH_LICENSE_KEY environment variable. If neither are provided,"
250 " the Langsmith application will not spin up.",
251 )
252 server_start_parser.add_argument(
253 "--version",
254 default="0.5.7",
255 help="The LangSmith version to use for LangSmith. Defaults to 0.5.7.",
256 )
257 server_start_parser.set_defaults(
258 func=lambda args: server_command.start(
259 openai_api_key=args.openai_api_key,
260 langsmith_license_key=args.langsmith_license_key,
261 version=args.version,
262 )
263 )
265 server_stop_parser = subparsers.add_parser(
266 "stop", description="Stop the LangSmith server."
267 )
268 server_stop_parser.add_argument(
269 "--clear-volumes",
270 action="store_true",
271 help="Delete all the locally cached LangSmith containers and volumes.",
272 )
273 server_stop_parser.set_defaults(
274 func=lambda args: server_command.stop(clear_volumes=args.clear_volumes)
275 )
277 server_pull_parser = subparsers.add_parser(
278 "pull", description="Pull the latest LangSmith images."
279 )
280 server_pull_parser.add_argument(
281 "--version",
282 default="0.5.7",
283 help="The LangSmith version to use for LangSmith. Defaults to 0.5.7.",
284 )
285 server_pull_parser.set_defaults(
286 func=lambda args: server_command.pull(version=args.version)
287 )
288 server_logs_parser = subparsers.add_parser(
289 "logs", description="Show the LangSmith server logs."
290 )
291 server_logs_parser.set_defaults(func=lambda args: server_command.logs())
292 server_status_parser = subparsers.add_parser(
293 "status", description="Show the LangSmith server status."
294 )
295 server_status_parser.set_defaults(func=lambda args: server_command.status())
296 env_parser = subparsers.add_parser("env")
297 env_parser.set_defaults(func=lambda args: env())
299 args = parser.parse_args()
300 if not hasattr(args, "func"):
301 parser.print_help()
302 return
303 args.func(args)
306if __name__ == "__main__":
307 main()