Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scripts[minor]: Add CLI for creating integration docs #6279

Merged
merged 7 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading