From 12c4ca49591b4f3ba86b3a8491b3d8c6963b6ecb Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Tue, 30 Jul 2024 13:21:40 -0700 Subject: [PATCH] scripts[minor]: Add CLI for creating integration docs (#6279) * scripts[minor]: Add CLI for creating integration docs * implemented script * wrap up cli * lint/format * fix * cr * cr --- libs/langchain-scripts/package.json | 4 +- libs/langchain-scripts/src/cli/docs/chat.ts | 227 ++++++++++++++++++ libs/langchain-scripts/src/cli/docs/index.ts | 49 ++++ .../src/cli/docs/templates}/chat.ipynb | 37 ++- .../src/cli/utils/get-input.ts | 47 ++++ yarn.lock | 1 + 6 files changed, 345 insertions(+), 20 deletions(-) create mode 100644 libs/langchain-scripts/src/cli/docs/chat.ts create mode 100644 libs/langchain-scripts/src/cli/docs/index.ts rename libs/{create-langchain-integration/docs => langchain-scripts/src/cli/docs/templates}/chat.ipynb (78%) create mode 100644 libs/langchain-scripts/src/cli/utils/get-input.ts diff --git a/libs/langchain-scripts/package.json b/libs/langchain-scripts/package.json index 388c5222a2d6..82048e1f88a6 100644 --- a/libs/langchain-scripts/package.json +++ b/libs/langchain-scripts/package.json @@ -34,7 +34,8 @@ "test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.cjs --testTimeout 100000", "test:int": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=\\.int\\.test.ts --testTimeout 100000 --maxWorkers=50%", "format": "prettier --write \"src\"", - "format:check": "prettier --check \"src\"" + "format:check": "prettier --check \"src\"", + "create:integration:doc": "node dist/cli/docs/index.js" }, "author": "LangChain", "license": "MIT", @@ -43,6 +44,7 @@ "axios": "^1.6.7", "commander": "^11.1.0", "glob": "^10.3.10", + "readline": "^1.3.0", "rimraf": "^5.0.1", "rollup": "^4.5.2", "ts-morph": "^21.0.1", diff --git a/libs/langchain-scripts/src/cli/docs/chat.ts b/libs/langchain-scripts/src/cli/docs/chat.ts new file mode 100644 index 000000000000..c0760bb32c54 --- /dev/null +++ b/libs/langchain-scripts/src/cli/docs/chat.ts @@ -0,0 +1,227 @@ +import * as path from "node:path"; +import * as fs from "node:fs"; +import { + boldText, + getUserInput, + greenText, + redBackground, +} from "../utils/get-input.js"; + +const PACKAGE_NAME_PLACEHOLDER = "__package_name__"; +const PACKAGE_NAME_SHORT_SNAKE_CASE_PLACEHOLDER = + "__package_name_short_snake_case__"; +const PACKAGE_NAME_SNAKE_CASE_PLACEHOLDER = "__package_name_snake_case__"; +const PACKAGE_NAME_PRETTY_PLACEHOLDER = "__package_name_pretty__"; +const MODULE_NAME_PLACEHOLDER = "__ModuleName__"; +// This should not be prefixed with `Chat` as it's used for API keys. +const MODULE_NAME_ALL_CAPS_PLACEHOLDER = "__MODULE_NAME_ALL_CAPS__"; + +const TOOL_CALLING_PLACEHOLDER = "__tool_calling__"; +const JSON_MODE_PLACEHOLDER = "__json_mode__"; +const IMAGE_INPUT_PLACEHOLDER = "__image_input__"; +const AUDIO_INPUT_PLACEHOLDER = "__audio_input__"; +const VIDEO_INPUT_PLACEHOLDER = "__video_input__"; +const TOKEN_LEVEL_STREAMING_PLACEHOLDER = "__token_level_streaming__"; +const TOKEN_USAGE_PLACEHOLDER = "__token_usage__"; +const LOGPROBS_PLACEHOLDER = "__logprobs__"; + +const SERIALIZABLE_PLACEHOLDER = "__serializable__"; +const LOCAL_PLACEHOLDER = "__local__"; +const PY_SUPPORT_PLACEHOLDER = "__py_support__"; + +const API_REF_BASE_PACKAGE_URL = `https://api.js.langchain.com/modules/langchain_${PACKAGE_NAME_PLACEHOLDER}.html`; +const API_REF_BASE_MODULE_URL = `https://api.js.langchain.com/classes/langchain_${PACKAGE_NAME_PLACEHOLDER}.${MODULE_NAME_PLACEHOLDER}.html`; +const TEMPLATE_PATH = path.resolve("./src/cli/docs/templates/chat.ipynb"); +const INTEGRATIONS_DOCS_PATH = path.resolve( + "../../docs/core_docs/docs/integrations/chat" +); + +const fetchAPIRefUrl = async (url: string): Promise => { + try { + const res = await fetch(url); + if (res.status !== 200) { + throw new Error(`API Reference URL ${url} not found.`); + } + return true; + } catch (_) { + return false; + } +}; + +type ExtraFields = { + /** + * If tool calling is true, structured output will also be true. + */ + toolCalling: boolean; + jsonMode: boolean; + imageInput: boolean; + audioInput: boolean; + videoInput: boolean; + tokenLevelStreaming: boolean; + tokenUsage: boolean; + logprobs: boolean; + local: boolean; + serializable: boolean; + pySupport: boolean; +}; + +async function promptExtraFields(): Promise { + const hasToolCalling = await getUserInput( + "Does the tool support tool calling? (y/n) ", + undefined, + true + ); + const hasJsonMode = await getUserInput( + "Does the tool support JSON mode? (y/n) ", + undefined, + true + ); + const hasImageInput = await getUserInput( + "Does the tool support image input? (y/n) ", + undefined, + true + ); + const hasAudioInput = await getUserInput( + "Does the tool support audio input? (y/n) ", + undefined, + true + ); + const hasVideoInput = await getUserInput( + "Does the tool support video input? (y/n) ", + undefined, + true + ); + const hasTokenLevelStreaming = await getUserInput( + "Does the tool support token level streaming? (y/n) ", + undefined, + true + ); + const hasTokenUsage = await getUserInput( + "Does the tool support token usage? (y/n) ", + undefined, + true + ); + const hasLogprobs = await getUserInput( + "Does the tool support logprobs? (y/n) ", + undefined, + true + ); + const hasLocal = await getUserInput( + "Does the tool support local usage? (y/n) ", + undefined, + true + ); + const hasSerializable = await getUserInput( + "Does the tool support serializable output? (y/n) ", + undefined, + true + ); + const hasPySupport = await getUserInput( + "Does the tool support Python support? (y/n) ", + undefined, + true + ); + + return { + toolCalling: hasToolCalling.toLowerCase() === "y", + jsonMode: hasJsonMode.toLowerCase() === "y", + imageInput: hasImageInput.toLowerCase() === "y", + audioInput: hasAudioInput.toLowerCase() === "y", + videoInput: hasVideoInput.toLowerCase() === "y", + tokenLevelStreaming: hasTokenLevelStreaming.toLowerCase() === "y", + tokenUsage: hasTokenUsage.toLowerCase() === "y", + logprobs: hasLogprobs.toLowerCase() === "y", + local: hasLocal.toLowerCase() === "y", + serializable: hasSerializable.toLowerCase() === "y", + pySupport: hasPySupport.toLowerCase() === "y", + }; +} + +export async function fillChatIntegrationDocTemplate(fields: { + packageName: string; + moduleName: string; +}) { + // Ask the user if they'd like to fill in extra fields, if so, prompt them. + let extraFields: ExtraFields | undefined; + const shouldPromptExtraFields = await getUserInput( + "Would you like to fill out optional fields? (y/n) ", + "white_background" + ); + if (shouldPromptExtraFields.toLowerCase() === "y") { + extraFields = await promptExtraFields(); + } + + const formattedApiRefPackageUrl = API_REF_BASE_PACKAGE_URL.replace( + PACKAGE_NAME_PLACEHOLDER, + fields.packageName + ); + const formattedApiRefModuleUrl = API_REF_BASE_MODULE_URL.replace( + PACKAGE_NAME_PLACEHOLDER, + fields.packageName + ).replace(MODULE_NAME_PLACEHOLDER, fields.moduleName); + + const success = await Promise.all([ + fetchAPIRefUrl(formattedApiRefPackageUrl), + fetchAPIRefUrl(formattedApiRefModuleUrl), + ]); + if (success.some((s) => s === false)) { + // Don't error out because this might be used before the package is released. + console.error("Invalid package or module name. API reference not found."); + } + + const packageNameShortSnakeCase = fields.packageName.replaceAll("-", "_"); + const fullPackageNameSnakeCase = `langchain_${packageNameShortSnakeCase}`; + const packageNamePretty = `@langchain/${fields.packageName}`; + let moduleNameAllCaps = fields.moduleName.toUpperCase(); + if (moduleNameAllCaps.startsWith("CHAT")) { + moduleNameAllCaps = moduleNameAllCaps.replace("CHAT", ""); + } + + const docTemplate = (await fs.promises.readFile(TEMPLATE_PATH, "utf-8")) + .replaceAll(PACKAGE_NAME_PLACEHOLDER, fields.packageName) + .replaceAll(PACKAGE_NAME_SNAKE_CASE_PLACEHOLDER, fullPackageNameSnakeCase) + .replaceAll( + PACKAGE_NAME_SHORT_SNAKE_CASE_PLACEHOLDER, + packageNameShortSnakeCase + ) + .replaceAll(PACKAGE_NAME_PRETTY_PLACEHOLDER, packageNamePretty) + .replaceAll(MODULE_NAME_PLACEHOLDER, fields.moduleName) + .replaceAll(MODULE_NAME_ALL_CAPS_PLACEHOLDER, moduleNameAllCaps) + .replaceAll( + TOOL_CALLING_PLACEHOLDER, + extraFields?.toolCalling ? "✅" : "❌" + ) + .replace(JSON_MODE_PLACEHOLDER, extraFields?.jsonMode ? "✅" : "❌") + .replace(IMAGE_INPUT_PLACEHOLDER, extraFields?.imageInput ? "✅" : "❌") + .replace(AUDIO_INPUT_PLACEHOLDER, extraFields?.audioInput ? "✅" : "❌") + .replace(VIDEO_INPUT_PLACEHOLDER, extraFields?.videoInput ? "✅" : "❌") + .replace( + TOKEN_LEVEL_STREAMING_PLACEHOLDER, + extraFields?.tokenLevelStreaming ? "✅" : "❌" + ) + .replace(TOKEN_USAGE_PLACEHOLDER, extraFields?.tokenUsage ? "✅" : "❌") + .replace(LOGPROBS_PLACEHOLDER, extraFields?.logprobs ? "✅" : "❌") + .replace(LOCAL_PLACEHOLDER, extraFields?.local ? "✅" : "❌") + .replace(SERIALIZABLE_PLACEHOLDER, extraFields?.serializable ? "✅" : "❌") + .replace(PY_SUPPORT_PLACEHOLDER, extraFields?.pySupport ? "✅" : "❌"); + + const docPath = path.join( + INTEGRATIONS_DOCS_PATH, + `${packageNameShortSnakeCase}.ipynb` + ); + await fs.promises.writeFile(docPath, docTemplate); + const prettyDocPath = docPath.split("docs/core_docs/")[1]; + + const updatePythonDocUrlText = ` ${redBackground( + "- Update the Python documentation URL with the proper URL." + )}`; + const successText = `\nSuccessfully created new chat model integration doc at ${prettyDocPath}.`; + + console.log( + `${greenText(successText)}\n +${boldText("Next steps:")} +${extraFields?.pySupport ? updatePythonDocUrlText : ""} + - Run all code cells in the generated doc to record the outputs. + - Add extra sections on integration specific features.\n` + ); +} diff --git a/libs/langchain-scripts/src/cli/docs/index.ts b/libs/langchain-scripts/src/cli/docs/index.ts new file mode 100644 index 000000000000..87543142e703 --- /dev/null +++ b/libs/langchain-scripts/src/cli/docs/index.ts @@ -0,0 +1,49 @@ +// --------------------------------------------- +// CLI for creating integration docs. +// --------------------------------------------- +import { Command } from "commander"; +import { fillChatIntegrationDocTemplate } from "./chat.js"; + +type CLIInput = { + package: string; + module: string; + type: string; +}; + +async function main() { + const program = new Command(); + program + .description("Create a new integration doc.") + .option( + "--package ", + "Package name, eg openai. Should be value of @langchain/" + ) + .option("--module ", "Module name, e.g ChatOpenAI") + .option("--type ", "Type of integration, e.g. 'chat'"); + + program.parse(); + + const options = program.opts(); + + const { module: moduleName, type } = options; + let { package: packageName } = options; + + if (packageName.startsWith("@langchain/")) { + packageName = packageName.replace("@langchain/", ""); + } + + switch (type) { + case "chat": + await fillChatIntegrationDocTemplate({ packageName, moduleName }); + break; + default: + console.error( + `Invalid type: ${type}.\nOnly 'chat' is supported at this time.` + ); + process.exit(1); + } +} + +main().catch((err) => { + throw err; +}); diff --git a/libs/create-langchain-integration/docs/chat.ipynb b/libs/langchain-scripts/src/cli/docs/templates/chat.ipynb similarity index 78% rename from libs/create-langchain-integration/docs/chat.ipynb rename to libs/langchain-scripts/src/cli/docs/templates/chat.ipynb index 76af6036af47..8aaca9a8d14c 100644 --- a/libs/create-langchain-integration/docs/chat.ipynb +++ b/libs/langchain-scripts/src/cli/docs/templates/chat.ipynb @@ -15,11 +15,11 @@ "id": "e49f1e0d", "metadata": {}, "source": [ - "# Chat__ModuleName__\n", + "# __ModuleName__\n", "\n", "- TODO: Make sure API reference link is correct.\n", "\n", - "This will help you getting started with __ModuleName__ [chat models](/docs/concepts/#chat-models). For detailed documentation of all Chat__ModuleName__ features and configurations head to the [API reference](https://api.js.langchain.com/classes/langchain___package_name__.__ModuleName__.html).\n", + "This will help you getting started with __ModuleName__ [chat models](/docs/concepts/#chat-models). For detailed documentation of all __ModuleName__ features and configurations head to the [API reference](https://api.js.langchain.com/classes/__package_name_snake_case__.__ModuleName__.html).\n", "\n", "- TODO: Add any other relevant links, like information about models, prices, context windows, etc. See https://js.langchain.com/v0.2/docs/integrations/chat/openai/ for an example.\n", "\n", @@ -30,31 +30,31 @@ "- TODO: Remove PY support link if not relevant, otherwise ensure link is correct.\n", "- TODO: Make sure API reference links are correct.\n", "\n", - "| Class | Package | Local | Serializable | [PY support](https:/py.langchain.com/v0.2/docs/integrations/chat/__package_name_short_snake__) | Package downloads | Package latest |\n", + "| Class | Package | Local | Serializable | [PY support](https:/py.langchain.com/v0.2/docs/integrations/chat/__package_name_short_snake_case__) | Package downloads | Package latest |\n", "| :--- | :--- | :---: | :---: | :---: | :---: | :---: |\n", - "| [Chat__ModuleName__](https://api.js.langchain.com/classes/langchain___PackageName__.__ModuleName__.html) | [__package_name__](https://api.js.langchain.com/modules/__package_snake_name__.html) | ✅/❌ | beta/❌ | ✅/❌ | ![NPM - Downloads](https://img.shields.io/npm/dm/__package_name__?style=flat-square&label=%20) | ![NPM - Version](https://img.shields.io/npm/v/__package_name__?style=flat-square&label=%20) |\n", + "| [__ModuleName__](https://api.js.langchain.com/classes/__package_name_snake_case__.__ModuleName__.html) | [__package_name_pretty__](https://api.js.langchain.com/modules/__package_name_snake_case__.html) | __local__ | __serializable__ | __py_support__ | ![NPM - Downloads](https://img.shields.io/npm/dm/__package_name_pretty__?style=flat-square&label=%20) | ![NPM - Version](https://img.shields.io/npm/v/__package_name_pretty__?style=flat-square&label=%20) |\n", "\n", "### Model features\n", - "| [Tool calling](/docs/how_to/tool_calling) | [Structured output](/docs/how_to/structured_output/) | JSON mode | [Image input](/docs/how_to/multimodal_inputs/) | Audio input | Video input | [Token-level streaming](/docs/how_to/chat_streaming/) | Native async | [Token usage](/docs/how_to/chat_token_usage_tracking/) | [Logprobs](/docs/how_to/logprobs/) |\n", - "| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |\n", - "| ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | ✅/❌ | \n", + "| [Tool calling](/docs/how_to/tool_calling) | [Structured output](/docs/how_to/structured_output/) | JSON mode | [Image input](/docs/how_to/multimodal_inputs/) | Audio input | Video input | [Token-level streaming](/docs/how_to/chat_streaming/) | [Token usage](/docs/how_to/chat_token_usage_tracking/) | [Logprobs](/docs/how_to/logprobs/) |\n", + "| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |\n", + "| __tool_calling__ | __tool_calling__ | __json_mode__ | __image_input__ | __audio_input__ | __video_input__ | __token_level_streaming__ | __token_usage__ | __logprobs__ | \n", "\n", "## Setup\n", "\n", "- TODO: Update with relevant info.\n", "\n", - "To access __ModuleName__ models you'll need to create a/an __ModuleName__ account, get an API key, and install the `__package_name__` integration package.\n", + "To access __ModuleName__ models you'll need to create a/an __ModuleName__ account, get an API key, and install the `__package_name_pretty__` integration package.\n", "\n", "### Credentials\n", "\n", "- TODO: Update with relevant info.\n", "\n", - "Head to (TODO: link) to sign up to __ModuleName__ and generate an API key. Once you've done this set the __MODULE_NAME___API_KEY environment variable:\n", + "Head to (TODO: link) to sign up to __ModuleName__ and generate an API key. Once you've done this set the __MODULE_NAME_ALL_CAPS___API_KEY environment variable:\n", "\n", "```{=mdx}\n", "\n", "```bash\n", - "export __MODULE_NAME___API_KEY=\"your-api-key\"\n", + "export __MODULE_NAME_ALL_CAPS___API_KEY=\"your-api-key\"\n", "```\n", "\n", "```" @@ -84,12 +84,12 @@ "source": [ "### Installation\n", "\n", - "The LangChain __ModuleName__ integration lives in the `__package_name__` package:\n", + "The LangChain __ModuleName__ integration lives in the `__package_name_pretty__` package:\n", "\n", "```{=mdx}\n", "\n", "```bash npm2yarn\n", - "npm i __package_name__\n", + "npm i __package_name_pretty__\n", "```\n", "\n", "```" @@ -118,12 +118,12 @@ }, "outputs": [], "source": [ - "import { Chat__ModuleName__ } from \"__module_name__\" \n", + "import { __ModuleName__ } from \"__package_name_pretty__\" \n", "\n", - "const llm = new Chat__ModuleName__({\n", + "const llm = new __ModuleName__({\n", " model: \"model-name\",\n", " temperature: 0,\n", - " max_tokens: undefined,\n", + " maxTokens: undefined,\n", " timeout: undefined,\n", " maxRetries: 2,\n", " // other params...\n", @@ -152,14 +152,13 @@ }, "outputs": [], "source": [ - "const messages = [\n", + "const aiMsg = await llm.invoke([\n", " [\n", " \"system\",\n", " \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n", " ],\n", " [\"human\", \"I love programming.\"],\n", - "]\n", - "const aiMsg = await llm.invoke(messages)\n", + "])\n", "aiMsg" ] }, @@ -239,7 +238,7 @@ "source": [ "## API reference\n", "\n", - "For detailed documentation of all Chat__ModuleName__ features and configurations head to the API reference: https://api.js.langchain.com/classes/langchain___package_name__.__ModuleName__.html" + "For detailed documentation of all __ModuleName__ features and configurations head to the API reference: https://api.js.langchain.com/classes/__package_name_snake_case__.__ModuleName__.html" ] } ], diff --git a/libs/langchain-scripts/src/cli/utils/get-input.ts b/libs/langchain-scripts/src/cli/utils/get-input.ts new file mode 100644 index 000000000000..0f753dd807ea --- /dev/null +++ b/libs/langchain-scripts/src/cli/utils/get-input.ts @@ -0,0 +1,47 @@ +import * as readline from "readline"; + +type Color = "green" | "red_background" | "white_background"; + +export const greenText = (text: string) => `\x1b[1m\x1b[92m${text}\x1b[0m`; +export const boldText = (text: string) => `\x1b[1m${text}\x1b[0m`; +export const redBackground = (text: string) => `\x1b[41m\x1b[37m${text}\x1b[0m`; +export const whiteBackground = (text: string) => + `\x1b[30m\x1b[47m${text}\x1b[0m`; + +/** + * Prompts the user with a question and returns the user input. + * + * @param {string} question The question to log to the users terminal. + * @param {Color | undefined} color The color to use for the question. + * @param {boolean | undefined} bold Whether to make the question bold. + * @returns {Promise} The user input. + */ +export async function getUserInput( + question: string, + color?: Color, + bold?: boolean +): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + let questionWithStyling = question; + if (bold) { + questionWithStyling = boldText(questionWithStyling); + } + if (color === "green") { + questionWithStyling = greenText(questionWithStyling); + } else if (color === "red_background") { + questionWithStyling = redBackground(questionWithStyling); + } else if (color === "white_background") { + questionWithStyling = whiteBackground(questionWithStyling); + } + + return new Promise((resolve) => { + rl.question(questionWithStyling, (input) => { + rl.close(); + resolve(input); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index d64172ea9af6..03dec4a5a6c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12354,6 +12354,7 @@ __metadata: jest: ^29.5.0 jest-environment-node: ^29.6.4 prettier: ^2.8.3 + readline: ^1.3.0 release-it: ^15.10.1 rimraf: ^5.0.1 rollup: ^4.5.2