import * as child_process from "child_process"; import * as fs from "fs"; import * as path from "path"; import * as util from "util"; import { getLangChainEnvVars, getRuntimeEnvironment, setEnvironmentVariable, } from "../utils/env.js"; import { Command } from "commander"; import { spawn } from "child_process"; const currentFileName = __filename; const currentDirName = __dirname; const program = new Command(); async function getDockerComposeCommand() { const exec = util.promisify(child_process.exec); try { await exec("docker compose --version"); return ["docker", "compose"]; } catch { try { await exec("docker-compose --version"); return ["docker-compose"]; } catch { throw new Error("Neither 'docker compose' nor 'docker-compose' commands are available. Please install the Docker server following the instructions for your operating system at https://docs.docker.com/engine/install/"); } } } async function pprintServices(servicesStatus) { const services = []; for (const service of servicesStatus) { const serviceStatus = { Service: String(service["Service"]), Status: String(service["Status"]), }; const publishers = service["Publishers"] || []; if (publishers) { serviceStatus["PublishedPorts"] = publishers .map((publisher) => String(publisher["PublishedPort"])) .join(", "); } services.push(serviceStatus); } const maxServiceLen = Math.max(...services.map((service) => service["Service"].length)); const maxStateLen = Math.max(...services.map((service) => service["Status"].length)); const serviceMessage = [ "\n" + "Service".padEnd(maxServiceLen + 2) + "Status".padEnd(maxStateLen + 2) + "Published Ports", ]; for (const service of services) { const serviceStr = service["Service"].padEnd(maxServiceLen + 2); const stateStr = service["Status"].padEnd(maxStateLen + 2); const portsStr = service["PublishedPorts"] || ""; serviceMessage.push(serviceStr + stateStr + portsStr); } let langchainEndpoint = "http://localhost:1984"; const usedNgrok = services.some((service) => service["Service"].includes("ngrok")); if (usedNgrok) { langchainEndpoint = await getNgrokUrl(); } serviceMessage.push("\nTo connect, set the following environment variables" + " in your LangChain application:" + "\nLANGCHAIN_TRACING_V2=true" + `\nLANGCHAIN_ENDPOINT=${langchainEndpoint}`); console.info(serviceMessage.join("\n")); } async function getNgrokUrl() { const ngrokUrl = "http://localhost:4040/api/tunnels"; try { // const response = await axios.get(ngrokUrl); const response = await fetch(ngrokUrl); if (response.status !== 200) { throw new Error(`Could not connect to ngrok console. ${response.status}, ${response.statusText}`); } const result = await response.json(); const exposedUrl = result["tunnels"][0]["public_url"]; return exposedUrl; } catch (error) { throw new Error(`Could not connect to ngrok console. ${error}`); } } async function createNgrokConfig(authToken) { const configPath = path.join(currentDirName, "ngrok_config.yaml"); // Check if is a directory if (fs.existsSync(configPath) && fs.lstatSync(configPath).isDirectory()) { fs.rmdirSync(configPath, { recursive: true }); } else if (fs.existsSync(configPath)) { fs.unlinkSync(configPath); } let ngrokConfig = ` region: us tunnels: langchain: addr: langchain-backend:1984 proto: http version: '2' `; if (authToken !== null) { ngrokConfig += `authtoken: ${authToken}`; } fs.writeFileSync(configPath, ngrokConfig); return configPath; } class SmithCommand { constructor({ dockerComposeCommand }) { Object.defineProperty(this, "dockerComposeCommand", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "dockerComposeFile", { enumerable: true, configurable: true, writable: true, value: "" }); Object.defineProperty(this, "dockerComposeDevFile", { enumerable: true, configurable: true, writable: true, value: "" }); Object.defineProperty(this, "dockerComposeBetaFile", { enumerable: true, configurable: true, writable: true, value: "" }); Object.defineProperty(this, "ngrokPath", { enumerable: true, configurable: true, writable: true, value: "" }); this.dockerComposeCommand = dockerComposeCommand; this.dockerComposeFile = path.join(path.dirname(currentFileName), "docker-compose.yaml"); this.dockerComposeDevFile = path.join(path.dirname(currentFileName), "docker-compose.dev.yaml"); this.dockerComposeBetaFile = path.join(path.dirname(currentFileName), "docker-compose.beta.yaml"); this.ngrokPath = path.join(path.dirname(currentFileName), "docker-compose.ngrok.yaml"); } async executeCommand(command) { return new Promise((resolve, reject) => { const child = spawn(command[0], command.slice(1), { stdio: "inherit" }); child.on("error", (error) => { console.error(`error: ${error.message}`); reject(error); }); child.on("close", (code) => { if (code !== 0) { reject(new Error(`Process exited with code ${code}`)); } else { resolve(); } }); }); } static async create() { console.info("BY USING THIS SOFTWARE YOU AGREE TO THE TERMS OF SERVICE AT:"); console.info("https://smith.langchain.com/terms-of-service.pdf"); const dockerComposeCommand = await getDockerComposeCommand(); return new SmithCommand({ dockerComposeCommand }); } async pull({ stage = "prod" }) { if (stage === "dev") { setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "dev-"); } else if (stage === "beta") { setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "rc-"); } const command = [ ...this.dockerComposeCommand, "-f", this.dockerComposeFile, "pull", ]; await this.executeCommand(command); } async startLocal(stage = "prod") { const command = [ ...this.dockerComposeCommand, "-f", this.dockerComposeFile, ]; if (stage === "dev") { command.push("-f", this.dockerComposeDevFile); } else if (stage === "beta") { command.push("-f", this.dockerComposeBetaFile); } command.push("up", "--quiet-pull", "--wait"); await this.executeCommand(command); console.info("LangSmith server is running at http://localhost:1984.\n" + "To view the app, navigate your browser to http://localhost:80" + "\n\nTo connect your LangChain application to the server" + " locally, set the following environment variable" + " when running your LangChain application."); console.info("\tLANGCHAIN_TRACING_V2=true"); } async startAndExpose(ngrokAuthToken, stage = "prod") { const configPath = await createNgrokConfig(ngrokAuthToken); const command = [ ...this.dockerComposeCommand, "-f", this.dockerComposeFile, "-f", this.ngrokPath, ]; if (stage === "dev") { command.push("-f", this.dockerComposeDevFile); } else if (stage === "beta") { command.push("-f", this.dockerComposeBetaFile); } command.push("up", "--quiet-pull", "--wait"); await this.executeCommand(command); console.info("ngrok is running. You can view the dashboard at http://0.0.0.0:4040"); const ngrokUrl = await getNgrokUrl(); console.info("LangSmith server is running at http://localhost:1984." + "To view the app, navigate your browser to http://localhost:80" + "\n\nTo connect your LangChain application to the server" + " remotely, set the following environment variable" + " when running your LangChain application."); console.info("\tLANGCHAIN_TRACING_V2=true"); console.info(`\tLANGCHAIN_ENDPOINT=${ngrokUrl}`); fs.unlinkSync(configPath); } async stop() { const command = [ ...this.dockerComposeCommand, "-f", this.dockerComposeFile, "-f", this.ngrokPath, "down", ]; await this.executeCommand(command); } async status() { const command = [ ...this.dockerComposeCommand, "-f", this.dockerComposeFile, "ps", "--format", "json", ]; const exec = util.promisify(child_process.exec); const result = await exec(command.join(" ")); const servicesStatus = JSON.parse(result.stdout); if (servicesStatus) { console.info("The LangSmith server is currently running."); await pprintServices(servicesStatus); } else { console.info("The LangSmith server is not running."); } } async env() { const env = await getRuntimeEnvironment(); const envVars = await getLangChainEnvVars(); const envDict = { ...env, ...envVars, }; // Pretty print const maxKeyLength = Math.max(...Object.keys(envDict).map((key) => key.length)); console.info("LangChain Environment:"); for (const [key, value] of Object.entries(envDict)) { console.info(`${key.padEnd(maxKeyLength)}: ${value}`); } } } const startCommand = new Command("start") .description("Start the LangSmith server") .option("--expose", "Expose the server to the internet via ngrok (requires ngrok to be installed)") .option("--ngrok-authtoken ", "Your ngrok auth token. If this is set, --expose is implied.") .option("--stage ", "Which version of LangSmith to run. Options: prod, dev, beta (default: prod)") .option("--openai-api-key ", "Your OpenAI API key. If not provided, the OpenAI API Key will be read" + " from the OPENAI_API_KEY environment variable. If neither are provided," + " some features of LangSmith will not be available.") .option("--langsmith-license-key ", "The LangSmith license key to use for LangSmith. If not provided, the LangSmith" + " License Key will be read from the LANGSMITH_LICENSE_KEY environment variable." + " If neither are provided, the Langsmith application will not spin up.") .action(async (args) => { const smith = await SmithCommand.create(); if (args.stage === "dev") { setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "dev-"); } else if (args.stage === "beta") { setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "rc-"); } if (args.openaiApiKey) { setEnvironmentVariable("OPENAI_API_KEY", args.openaiApiKey); } if (args.langsmithLicenseKey) { setEnvironmentVariable("LANGSMITH_LICENSE_KEY", args.langsmithLicenseKey); } await smith.pull({ stage: args.stage }); if (args.expose) { await smith.startAndExpose(args.ngrokAuthtoken, args.stage); } else { await smith.startLocal(args.stage); } }); const stopCommand = new Command("stop") .description("Stop the LangSmith server") .action(async () => { const smith = await SmithCommand.create(); await smith.stop(); }); const pullCommand = new Command("pull") .description("Pull the latest version of the LangSmith server") .option("--stage ", "Which version of LangSmith to pull. Options: prod, dev, beta (default: prod)") .action(async (args) => { const smith = await SmithCommand.create(); if (args.stage === "dev") { setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "dev-"); } else if (args.stage === "beta") { setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "rc-"); } await smith.pull({ stage: args.stage }); }); const statusCommand = new Command("status") .description("Get the status of the LangSmith server") .action(async () => { const smith = await SmithCommand.create(); await smith.status(); }); const envCommand = new Command("env") .description("Get relevant environment information for the LangSmith server") .action(async () => { const smith = await SmithCommand.create(); await smith.env(); }); program .description("Manage the LangSmith server") .addCommand(startCommand) .addCommand(stopCommand) .addCommand(pullCommand) .addCommand(statusCommand) .addCommand(envCommand); program.parse(process.argv);