Skip to content

Commit

Permalink
scripts[minor]: Add CLI for creating integration docs (#6279)
Browse files Browse the repository at this point in the history
* scripts[minor]: Add CLI for creating integration docs

* implemented script

* wrap up cli

* lint/format

* fix

* cr

* cr
  • Loading branch information
bracesproul authored Jul 30, 2024
1 parent fb23656 commit 12c4ca4
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 20 deletions.
4 changes: 3 additions & 1 deletion libs/langchain-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
227 changes: 227 additions & 0 deletions libs/langchain-scripts/src/cli/docs/chat.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> => {
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<ExtraFields> {
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`
);
}
49 changes: 49 additions & 0 deletions libs/langchain-scripts/src/cli/docs/index.ts
Original file line number Diff line number Diff line change
@@ -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>",
"Package name, eg openai. Should be value of @langchain/<package>"
)
.option("--module <module>", "Module name, e.g ChatOpenAI")
.option("--type <type>", "Type of integration, e.g. 'chat'");

program.parse();

const options = program.opts<CLIInput>();

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;
});
Loading

0 comments on commit 12c4ca4

Please sign in to comment.