Skip to content

Commit

Permalink
Add JS Server Cli (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
vowelparrot authored Jun 5, 2023
1 parent 0a5ea60 commit 42c043d
Show file tree
Hide file tree
Showing 8 changed files with 382 additions and 3 deletions.
6 changes: 5 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
],
"type": "module",
"main": "./dist/index.js",
"bin": {
"langchain": "./dist/cli/main.cjs"
},
"types": "./dist/index.d.ts",
"scripts": {
"build": "yarn clean && yarn build:esm && yarn build:cjs && node scripts/create-entrypoints.js",
"build": "yarn clean && yarn build:esm && yarn build:cjs && node scripts/create-entrypoints.js && node scripts/create-cli.js",
"clean": "rm -rf dist/ && node scripts/create-entrypoints.js clean",
"build:esm": "tsc --outDir dist/ && rm -rf dist/tests dist/**/tests",
"build:cjs": "tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs",
Expand Down Expand Up @@ -73,6 +76,7 @@
},
"dependencies": {
"@types/uuid": "^9.0.1",
"commander": "^10.0.1",
"p-queue": "^6.6.2",
"p-retry": "^5.1.2",
"uuid": "^9.0.0"
Expand Down
25 changes: 25 additions & 0 deletions js/scripts/create-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as fs from "fs";
import * as path from "path";

let dirname = new URL(".", import.meta.url).pathname;
// If on Windows, remove the leading slash
if (process.platform === "win32" && dirname.startsWith("/")) {
dirname = dirname.slice(1);
}

const mainPath = path.join(dirname, "../dist/cli/main.cjs");
const mainContents = fs.readFileSync(mainPath).toString();
const shebang = "#!/usr/bin/env node\n";
const newContents = shebang + mainContents;
// Update file contents
fs.writeFileSync(mainPath, newContents);
// Make the file executable
fs.chmodSync(mainPath, "755");

// Copy the docker compose files over
const yamlFiles = fs.readdirSync(path.join(dirname, "../src/cli"));
for (const yamlFile of yamlFiles) {
const srcPath = path.join(dirname, "../src/cli", yamlFile);
const destPath = path.join(dirname, "../dist/cli", yamlFile);
fs.copyFileSync(srcPath, destPath);
}
17 changes: 17 additions & 0 deletions js/src/cli/docker-compose.ngrok.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '3'
services:
ngrok:
image: ngrok/ngrok:latest
restart: unless-stopped
command:
- "start"
- "--all"
- "--config"
- "/etc/ngrok.yml"
volumes:
- ./ngrok_config.yaml:/etc/ngrok.yml
ports:
- 4040:4040
langchain-backend:
depends_on:
- ngrok
49 changes: 49 additions & 0 deletions js/src/cli/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
version: '3'
services:
langchain-frontend:
image: langchain/${_LANGCHAINPLUS_IMAGE_PREFIX-}langchainplus-frontend:latest
ports:
- 80:80
environment:
- REACT_APP_BACKEND_URL=http://localhost:1984
depends_on:
- langchain-backend
volumes:
- ./conf/nginx.conf:/etc/nginx/default.conf:ro
build:
context: frontend-react/.
dockerfile: Dockerfile
langchain-backend:
image: langchain/${_LANGCHAINPLUS_IMAGE_PREFIX-}langchainplus-backend:latest
environment:
- PORT=1984
- LANGCHAIN_ENV=local_docker
- LOG_LEVEL=warning
- OPENAI_API_KEY=${OPENAI_API_KEY}
ports:
- 1984:1984
depends_on:
- langchain-db
build:
context: backend/.
dockerfile: Dockerfile
langchain-db:
image: postgres:14.1
command:
[
"postgres",
"-c",
"log_min_messages=WARNING",
"-c",
"client_min_messages=WARNING"
]
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
- POSTGRES_DB=postgres
volumes:
- langchain-db-data:/var/lib/postgresql/data
ports:
- 5433:5432
volumes:
langchain-db-data:
272 changes: 272 additions & 0 deletions js/src/cli/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import * as fs from "fs";
import * as path from "path";
import * as util from "util";
import { Command } from "commander";
import * as child_process from "child_process";
import { setEnvironmentVariable } from "../utils/env.js";

const currentFileName = __filename;
const currentDirName = __dirname;

const exec = util.promisify(child_process.exec);

const program = new Command();

async function getDockerComposeCommand(): Promise<string[]> {
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) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serviceStatus: Record<string, any> = {
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);
}

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.log(serviceMessage.join("\n"));
}

async function getNgrokUrl(): Promise<string> {
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: string | null): Promise<string> {
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:8000
proto: http
version: '2'
`;

if (authToken !== null) {
ngrokConfig += `authtoken: ${authToken}`;
}
fs.writeFileSync(configPath, ngrokConfig);
return configPath;
}

class PlusCommand {
dockerComposeCommand: string[] = [];
dockerComposeFile = "";
ngrokPath = "";

constructor({ dockerComposeCommand }: { dockerComposeCommand: string[] }) {
this.dockerComposeCommand = dockerComposeCommand;
this.dockerComposeFile = path.join(
path.dirname(currentFileName),
"docker-compose.yaml"
);
this.ngrokPath = path.join(
path.dirname(currentFileName),
"docker-compose.ngrok.yaml"
);
}

public static async create() {
const dockerComposeCommand = await getDockerComposeCommand();
return new PlusCommand({ dockerComposeCommand });
}

async start(args: any) {
if (args.dev) {
setEnvironmentVariable("_LANGCHAINPLUS_IMAGE_PREFIX", "rc-");
}
if (args.openaiApiKey) {
setEnvironmentVariable("OPENAI_API_KEY", args.openaiApiKey);
}

if (args.expose) {
await this.startAndExpose(args.ngrokAuthtoken);
} else {
await this.startLocal();
}
}

async startLocal() {
const command = [
...this.dockerComposeCommand,
"-f",
this.dockerComposeFile,
"up",
"--pull=always",
"--quiet-pull",
"--wait",
];
await exec(command.join(" "));
console.info(
"LangChainPlus server is running at http://localhost. To connect locally, set the following environment variable when running your LangChain application."
);
console.info("\tLANGCHAIN_TRACING_V2=true");
}

async startAndExpose(ngrokAuthToken: string | null) {
const configPath = await createNgrokConfig(ngrokAuthToken);
const command = [
...this.dockerComposeCommand,
"-f",
this.dockerComposeFile,
"-f",
this.ngrokPath,
"up",
"--pull=always",
"--quiet-pull",
"--wait",
];
await exec(command.join(" "));
console.info(
"ngrok is running. You can view the dashboard at http://0.0.0.0:4040"
);
const ngrokUrl = await getNgrokUrl();
console.info(
"LangChainPlus server is running at http://localhost. To connect 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 exec(command.join(" "));
}
async status() {
const command = [
...this.dockerComposeCommand,
"-f",
this.dockerComposeFile,
"ps",
"--format",
"json",
];
const result = await exec(command.join(" "));
const servicesStatus = JSON.parse(result.stdout);
if (servicesStatus) {
console.info("The LangChainPlus server is currently running.");
console.info(pprintServices(servicesStatus));
} else {
console.info("The LangChainPlus server is not running.");
}
}
}

const startCommand = new Command("start")
.description("Start the LangChainPlus 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("--dev", "Run the development version of the LangChainPlus server")
.option(
"--openai-api-key <openaiApiKey>",
"Your OpenAI API key. If this is set, the server will be able to process text and return enhanced plus results."
)
.action(async (args: string[]) => (await PlusCommand.create()).start(args));

const stopCommand = new Command("stop")
.command("stop")
.description("Stop the LangChainPlus server")
.action(async () => (await PlusCommand.create()).stop());

const statusCommand = new Command("status")
.command("status")
.description("Get the status of the LangChainPlus server")
.action(async () => (await PlusCommand.create()).status());

program
.command("plus")
.description("Manage the LangChainPlus server")
.addCommand(startCommand)
.addCommand(stopCommand)
.addCommand(statusCommand);

program.parse(process.argv);
2 changes: 1 addition & 1 deletion js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class LangChainPlusClient {

private caller: AsyncCaller;

constructor(config: LangChainPlusClientConfig) {
constructor(config: LangChainPlusClientConfig = {}) {
const defaultConfig = LangChainPlusClient.getDefaultClientConfig();

this.apiUrl = config.apiUrl ?? defaultConfig.apiUrl;
Expand Down
Loading

0 comments on commit 42c043d

Please sign in to comment.