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

1import argparse 

2import json 

3import logging 

4import os 

5import subprocess 

6from pathlib import Path 

7from typing import Dict, List, Mapping, Optional, Union, cast 

8 

9from langsmith import env as ls_env 

10from langsmith import utils as ls_utils 

11 

12logging.basicConfig(level=logging.INFO, format="%(message)s") 

13logger = logging.getLogger(__name__) 

14 

15_DIR = Path(__file__).parent 

16 

17 

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) 

33 

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) 

47 

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)) 

55 

56 

57class LangSmithCommand: 

58 """Manage the LangSmith Tracing server.""" 

59 

60 def __init__(self) -> None: 

61 self.docker_compose_file = ( 

62 Path(__file__).absolute().parent / "docker-compose.yaml" 

63 ) 

64 

65 @property 

66 def docker_compose_command(self) -> List[str]: 

67 return ls_utils.get_docker_compose_command() 

68 

69 def _open_browser(self, url: str) -> None: 

70 try: 

71 subprocess.run(["open", url]) 

72 except FileNotFoundError: 

73 pass 

74 

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 ) 

96 

97 logger.info("\tLANGSMITH_TRACING=true") 

98 logger.info("\tLANGSMITH_ENDPOINT=http://localhost:80/api\n") 

99 self._open_browser("http://localhost") 

100 

101 def pull( 

102 self, 

103 *, 

104 version: str = "0.5.7", 

105 ) -> None: 

106 """Pull the latest LangSmith images. 

107 

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 ) 

120 

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. 

129 

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() 

147 

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") 

166 

167 subprocess.run(cmd) 

168 

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 ) 

179 

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 ] 

190 

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 

208 

209 

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()) 

215 

216 # calculate the max length of keys 

217 max_key_length = max(len(key) for key in env.keys()) 

218 

219 logger.info("LangChain Environment:") 

220 for k, v in env.items(): 

221 logger.info(f"{k:{max_key_length}}: {v}") 

222 

223 

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") 

228 

229 parser = argparse.ArgumentParser() 

230 subparsers = parser.add_subparsers(description="LangSmith CLI commands") 

231 

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 ) 

264 

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 ) 

276 

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()) 

298 

299 args = parser.parse_args() 

300 if not hasattr(args, "func"): 

301 parser.print_help() 

302 return 

303 args.func(args) 

304 

305 

306if __name__ == "__main__": 

307 main()