Spaces:
Build error
Build error
| 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 <ngrokAuthtoken>", "Your ngrok auth token. If this is set, --expose is implied.") | |
| .option("--stage <stage>", "Which version of LangSmith to run. Options: prod, dev, beta (default: prod)") | |
| .option("--openai-api-key <openaiApiKey>", "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 <langsmithLicenseKey>", "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 <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); | |