2023-12-13 16:55:30 +00:00
|
|
|
"""
|
|
|
|
Develop integration packages for LangChain.
|
|
|
|
"""
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
import typer
|
|
|
|
from typing_extensions import Annotated, TypedDict
|
|
|
|
|
2024-05-29 20:34:58 +00:00
|
|
|
from langchain_cli.utils.find_replace import replace_file, replace_glob
|
2023-12-13 16:55:30 +00:00
|
|
|
|
|
|
|
integration_cli = typer.Typer(no_args_is_help=True, add_completion=False)
|
|
|
|
|
|
|
|
Replacements = TypedDict(
|
|
|
|
"Replacements",
|
|
|
|
{
|
|
|
|
"__package_name__": str,
|
|
|
|
"__module_name__": str,
|
|
|
|
"__ModuleName__": str,
|
2024-05-29 20:34:58 +00:00
|
|
|
"__MODULE_NAME__": str,
|
2023-12-13 16:55:30 +00:00
|
|
|
"__package_name_short__": str,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _process_name(name: str):
|
|
|
|
preprocessed = name.replace("_", "-").lower()
|
|
|
|
|
|
|
|
if preprocessed.startswith("langchain-"):
|
|
|
|
preprocessed = preprocessed[len("langchain-") :]
|
|
|
|
|
|
|
|
if not re.match(r"^[a-z][a-z0-9-]*$", preprocessed):
|
|
|
|
raise ValueError(
|
|
|
|
"Name should only contain lowercase letters (a-z), numbers, and hyphens"
|
|
|
|
", and start with a letter."
|
|
|
|
)
|
|
|
|
if preprocessed.endswith("-"):
|
|
|
|
raise ValueError("Name should not end with `-`.")
|
|
|
|
if preprocessed.find("--") != -1:
|
|
|
|
raise ValueError("Name should not contain consecutive hyphens.")
|
|
|
|
return Replacements(
|
|
|
|
{
|
|
|
|
"__package_name__": f"langchain-{preprocessed}",
|
|
|
|
"__module_name__": "langchain_" + preprocessed.replace("-", "_"),
|
|
|
|
"__ModuleName__": preprocessed.title().replace("-", ""),
|
2024-05-29 20:34:58 +00:00
|
|
|
"__MODULE_NAME__": preprocessed.upper().replace("-", ""),
|
2023-12-13 16:55:30 +00:00
|
|
|
"__package_name_short__": preprocessed,
|
2024-05-29 20:34:58 +00:00
|
|
|
"__package_name_short_snake__": preprocessed.replace("-", "_"),
|
2023-12-13 16:55:30 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@integration_cli.command()
|
|
|
|
def new(
|
|
|
|
name: Annotated[
|
|
|
|
str,
|
|
|
|
typer.Option(
|
|
|
|
help="The name of the integration to create (e.g. `my-integration`)",
|
2024-05-29 20:34:58 +00:00
|
|
|
prompt="The name of the integration to create (e.g. `my-integration`)",
|
2023-12-13 16:55:30 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
name_class: Annotated[
|
|
|
|
Optional[str],
|
|
|
|
typer.Option(
|
|
|
|
help="The name of the integration in PascalCase. e.g. `MyIntegration`."
|
|
|
|
" This is used to name classes like `MyIntegrationVectorStore`"
|
|
|
|
),
|
|
|
|
] = None,
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Creates a new integration package.
|
|
|
|
|
|
|
|
Should be run from libs/partners
|
|
|
|
"""
|
|
|
|
# confirm that we are in the right directory
|
|
|
|
if not Path.cwd().name == "partners" or not Path.cwd().parent.name == "libs":
|
|
|
|
typer.echo(
|
|
|
|
"This command should be run from the `libs/partners` directory in the "
|
|
|
|
"langchain-ai/langchain monorepo. Continuing is NOT recommended."
|
|
|
|
)
|
|
|
|
typer.confirm("Are you sure you want to continue?", abort=True)
|
|
|
|
|
|
|
|
try:
|
|
|
|
replacements = _process_name(name)
|
|
|
|
except ValueError as e:
|
|
|
|
typer.echo(e)
|
|
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
|
|
if name_class:
|
|
|
|
if not re.match(r"^[A-Z][a-zA-Z0-9]*$", name_class):
|
|
|
|
typer.echo(
|
|
|
|
"Name should only contain letters (a-z, A-Z), numbers, and underscores"
|
|
|
|
", and start with a capital letter."
|
|
|
|
)
|
|
|
|
raise typer.Exit(code=1)
|
|
|
|
replacements["__ModuleName__"] = name_class
|
|
|
|
else:
|
|
|
|
replacements["__ModuleName__"] = typer.prompt(
|
|
|
|
"Name of integration in PascalCase", default=replacements["__ModuleName__"]
|
|
|
|
)
|
|
|
|
|
|
|
|
destination_dir = Path.cwd() / replacements["__package_name_short__"]
|
|
|
|
if destination_dir.exists():
|
|
|
|
typer.echo(f"Folder {destination_dir} exists.")
|
|
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
|
|
# copy over template from ../integration_template
|
|
|
|
project_template_dir = Path(__file__).parents[1] / "integration_template"
|
|
|
|
shutil.copytree(project_template_dir, destination_dir, dirs_exist_ok=False)
|
|
|
|
|
|
|
|
# folder movement
|
|
|
|
package_dir = destination_dir / replacements["__module_name__"]
|
|
|
|
shutil.move(destination_dir / "integration_template", package_dir)
|
|
|
|
|
|
|
|
# replacements in files
|
|
|
|
replace_glob(destination_dir, "**/*", replacements)
|
|
|
|
|
|
|
|
# poetry install
|
|
|
|
subprocess.run(
|
|
|
|
["poetry", "install", "--with", "lint,test,typing,test_integration"],
|
|
|
|
cwd=destination_dir,
|
|
|
|
)
|
2024-05-29 20:34:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
@integration_cli.command()
|
|
|
|
def create_doc(
|
|
|
|
name: Annotated[
|
|
|
|
str,
|
|
|
|
typer.Option(
|
|
|
|
help=(
|
|
|
|
"The kebab-case name of the integration (e.g. `openai`, "
|
|
|
|
"`google-vertexai`). Do not include a 'langchain-' prefix."
|
|
|
|
),
|
|
|
|
prompt=(
|
|
|
|
"The kebab-case name of the integration (e.g. `openai`, "
|
|
|
|
"`google-vertexai`). Do not include a 'langchain-' prefix."
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
name_class: Annotated[
|
|
|
|
Optional[str],
|
|
|
|
typer.Option(
|
|
|
|
help=(
|
|
|
|
"The PascalCase name of the integration (e.g. `OpenAI`, "
|
|
|
|
"`VertexAI`). Do not include a 'Chat', 'VectorStore', etc. "
|
|
|
|
"prefix/suffix."
|
|
|
|
),
|
|
|
|
),
|
|
|
|
] = None,
|
|
|
|
component_type: Annotated[
|
|
|
|
str,
|
|
|
|
typer.Option(
|
2024-06-14 02:28:57 +00:00
|
|
|
help=("The type of component. Currently only 'ChatModel', 'DocumentLoader' supported."),
|
2024-05-29 20:34:58 +00:00
|
|
|
),
|
|
|
|
] = "ChatModel",
|
|
|
|
destination_dir: Annotated[
|
|
|
|
str,
|
|
|
|
typer.Option(
|
|
|
|
help="The relative path to the docs directory to place the new file in.",
|
|
|
|
prompt="The relative path to the docs directory to place the new file in.",
|
|
|
|
),
|
|
|
|
] = "docs/docs/integrations/chat/",
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Creates a new integration doc.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
replacements = _process_name(name)
|
|
|
|
except ValueError as e:
|
|
|
|
typer.echo(e)
|
|
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
|
|
if name_class:
|
|
|
|
if not re.match(r"^[A-Z][a-zA-Z0-9]*$", name_class):
|
|
|
|
typer.echo(
|
|
|
|
"Name should only contain letters (a-z, A-Z), numbers, and underscores"
|
|
|
|
", and start with a capital letter."
|
|
|
|
)
|
|
|
|
raise typer.Exit(code=1)
|
|
|
|
replacements["__ModuleName__"] = name_class
|
|
|
|
else:
|
|
|
|
replacements["__ModuleName__"] = typer.prompt(
|
|
|
|
(
|
|
|
|
"The PascalCase name of the integration (e.g. `OpenAI`, `VertexAI`). "
|
|
|
|
"Do not include a 'Chat', 'VectorStore', etc. prefix/suffix."
|
|
|
|
),
|
|
|
|
default=replacements["__ModuleName__"],
|
|
|
|
)
|
|
|
|
destination_path = (
|
|
|
|
Path.cwd()
|
|
|
|
/ destination_dir
|
|
|
|
/ (replacements["__package_name_short_snake__"] + ".ipynb")
|
|
|
|
)
|
|
|
|
|
|
|
|
# copy over template from ../integration_template
|
2024-06-14 02:28:57 +00:00
|
|
|
if component_type == "ChatModel":
|
|
|
|
docs_template = Path(__file__).parents[1] / "integration_template/docs/chat.ipynb"
|
|
|
|
elif component_type == "DocumentLoader":
|
|
|
|
docs_template = Path(__file__).parents[1] / "integration_template/docs/document_loaders.ipynb"
|
2024-05-29 20:34:58 +00:00
|
|
|
shutil.copy(docs_template, destination_path)
|
|
|
|
|
|
|
|
# replacements in file
|
|
|
|
replace_file(destination_path, replacements)
|