langsmith-cai
Version:
Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.
290 lines (260 loc) • 9.24 kB
text/typescript
import * as child_process from "child_process";
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 program = new Command();
async function getDockerComposeCommand(): Promise<string[]> {
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: any[]) {
const services = [];
for (const service of servicesStatus) {
const serviceStatus: Record<string, string> = {
Service: String(service["Service"]),
Status: String(service["Status"]),
};
const publishers = service["Publishers"] || [];
if (publishers) {
serviceStatus["PublishedPorts"] = publishers
.map((publisher: any) => 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);
}
serviceMessage.push(
"\nTo connect, set the following environment variables" +
" in your LangChain application:" +
"\nLANGCHAIN_TRACING_V2=true" +
`\nLANGCHAIN_ENDPOINT=http://localhost:80/api`
);
console.info(serviceMessage.join("\n"));
}
class SmithCommand {
dockerComposeCommand: string[] = [];
dockerComposeFile = "";
constructor({ dockerComposeCommand }: { dockerComposeCommand: string[] }) {
this.dockerComposeCommand = dockerComposeCommand;
this.dockerComposeFile = path.join(
path.dirname(currentFileName),
"docker-compose.yaml"
);
}
async executeCommand(command: string[]) {
return new Promise<void>((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();
}
});
});
}
public 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", version = "latest" }) {
if (stage === "dev") {
setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "dev-");
} else if (stage === "beta") {
setEnvironmentVariable("_LANGSMITH_IMAGE_PREFIX", "rc-");
}
setEnvironmentVariable("_LANGSMITH_IMAGE_VERSION", version);
const command = [
...this.dockerComposeCommand,
"-f",
this.dockerComposeFile,
"pull",
];
await this.executeCommand(command);
}
async startLocal() {
const command = [
...this.dockerComposeCommand,
"-f",
this.dockerComposeFile,
];
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");
console.info("\tLANGCHAIN_ENDPOINT=http://localhost:80/api");
}
async stop() {
const command = [
...this.dockerComposeCommand,
"-f",
this.dockerComposeFile,
"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(
"--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."
)
.option(
"--version <version>",
"The LangSmith version to use for LangSmith. Defaults to latest." +
" We recommend pegging this to the latest static version available at" +
" https://hub.docker.com/repository/docker/langchain/langchainplus-backend" +
" if you are using Langsmith in production."
)
.action(async (args) => {
const smith = await SmithCommand.create();
if (args.openaiApiKey) {
setEnvironmentVariable("OPENAI_API_KEY", args.openaiApiKey);
}
if (args.langsmithLicenseKey) {
setEnvironmentVariable("LANGSMITH_LICENSE_KEY", args.langsmithLicenseKey);
}
await smith.pull({ stage: args.stage, version: args.version });
await smith.startLocal();
});
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)"
)
.option(
"--version <version>",
"The LangSmith version to use for LangSmith. Defaults to latest." +
" We recommend pegging this to the latest static version available at" +
" https://hub.docker.com/repository/docker/langchain/langchainplus-backend" +
" if you are using Langsmith in production."
)
.action(async (args) => {
const smith = await SmithCommand.create();
await smith.pull({ stage: args.stage, version: args.version });
});
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);