2026-04-22 22:14:26 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Annotated, Callable
|
|
|
|
|
|
|
|
|
|
import typer
|
|
|
|
|
from chromadb.errors import InternalError, NotFoundError
|
2026-04-24 18:23:02 +02:00
|
|
|
from rich import print
|
2026-04-22 22:14:26 +02:00
|
|
|
|
2026-05-10 16:35:37 +02:00
|
|
|
from chromy.chroma_functions import CHROMA_FOLDER_ENV_VAR
|
2026-05-06 21:23:37 +02:00
|
|
|
from chromy.errors import ChromaPathError
|
2026-04-22 22:14:26 +02:00
|
|
|
from chromy.handlers.count_collection import handle_count_collection
|
|
|
|
|
from chromy.handlers.create_collection import handle_create_collection
|
|
|
|
|
from chromy.handlers.delete_collection import (
|
|
|
|
|
handle_delete_collection,
|
|
|
|
|
handle_delete_records,
|
|
|
|
|
)
|
2026-04-24 18:23:02 +02:00
|
|
|
from chromy.handlers.import_data import handle_import
|
2026-04-22 22:14:26 +02:00
|
|
|
from chromy.handlers.list_collections import handle_list_collections
|
|
|
|
|
from chromy.handlers.query import handle_query
|
|
|
|
|
|
2026-05-10 16:35:37 +02:00
|
|
|
app = typer.Typer(
|
|
|
|
|
help=(
|
|
|
|
|
"Chromy, local RAG CLI based on Chromadb.\n\n"
|
|
|
|
|
"Storage location:\n"
|
|
|
|
|
"- By default, Chromy uses Chroma's default persistent location behavior.\n"
|
|
|
|
|
f"- Set {CHROMA_FOLDER_ENV_VAR} to a parent directory to override it.\n"
|
2026-05-10 16:56:50 +02:00
|
|
|
f"- Chromy stores data in <{CHROMA_FOLDER_ENV_VAR}>/chroma.\n\n"
|
|
|
|
|
"Command aliases:\n"
|
|
|
|
|
"- list-collections: lc\n"
|
|
|
|
|
"- create-collection: cc\n"
|
|
|
|
|
"- delete-collection: dc\n"
|
|
|
|
|
"- count: c\n"
|
|
|
|
|
"- import: i\n"
|
|
|
|
|
"- query: q\n"
|
|
|
|
|
"- delete: del"
|
2026-05-10 16:52:31 +02:00
|
|
|
),
|
|
|
|
|
invoke_without_command=True,
|
2026-05-10 16:35:37 +02:00
|
|
|
)
|
2026-04-22 22:14:26 +02:00
|
|
|
|
|
|
|
|
ExitCodeHandler = Callable[[], int]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run(handler: ExitCodeHandler) -> None:
|
2026-05-06 21:23:37 +02:00
|
|
|
try:
|
|
|
|
|
exit_code = handler()
|
|
|
|
|
except ChromaPathError as exc:
|
|
|
|
|
_fail(str(exc))
|
|
|
|
|
|
2026-04-22 22:14:26 +02:00
|
|
|
if exit_code != 0:
|
|
|
|
|
raise typer.Exit(exit_code)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fail(message: str) -> None:
|
2026-04-23 21:49:46 +02:00
|
|
|
print("[bold red]Error[/]:", message)
|
2026-04-22 22:14:26 +02:00
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
|
2026-05-10 16:52:31 +02:00
|
|
|
@app.callback()
|
|
|
|
|
def main(ctx: typer.Context) -> None:
|
|
|
|
|
"""Run the CLI and show help when no command is provided."""
|
|
|
|
|
if ctx.invoked_subcommand is None:
|
|
|
|
|
print("[bold red]Error[/]: Missing command.")
|
|
|
|
|
typer.echo(ctx.get_help())
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# LIST COLLECTIONS
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("lc", help="Alias for list-collections.")
|
2026-04-22 22:14:26 +02:00
|
|
|
@app.command(
|
|
|
|
|
"list-collections",
|
2026-05-10 16:56:50 +02:00
|
|
|
help="List all collections stored in the local Chroma database. Alias: lc.",
|
2026-04-22 22:14:26 +02:00
|
|
|
)
|
|
|
|
|
def list_collections() -> None:
|
|
|
|
|
_run(handle_list_collections)
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# CREATE A COLLECTION
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("cc", help="Alias for create-collection.")
|
2026-04-22 22:14:26 +02:00
|
|
|
@app.command(
|
|
|
|
|
"create-collection",
|
2026-05-10 16:56:50 +02:00
|
|
|
help="Create a collection in the local Chroma database. Alias: cc.",
|
2026-04-22 22:14:26 +02:00
|
|
|
)
|
|
|
|
|
def create_collection(
|
|
|
|
|
collection: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="Name of the collection to create."),
|
|
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
try:
|
|
|
|
|
_run(lambda: handle_create_collection(collection))
|
|
|
|
|
except InternalError:
|
|
|
|
|
_fail(f"Collection '{collection}' already exists.")
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# DELETE A COLLECTION
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("dc", help="Alias for delete-collection.")
|
2026-04-22 22:14:26 +02:00
|
|
|
@app.command(
|
|
|
|
|
"delete-collection",
|
2026-05-10 16:56:50 +02:00
|
|
|
help="Delete a collection from the local Chroma database. Alias: dc.",
|
2026-04-22 22:14:26 +02:00
|
|
|
)
|
|
|
|
|
def delete_collection(
|
|
|
|
|
collection: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="Name of the collection to delete."),
|
|
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
try:
|
|
|
|
|
_run(lambda: handle_delete_collection(collection))
|
|
|
|
|
except NotFoundError:
|
|
|
|
|
_fail(f"Collection '{collection}' does not exist.")
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# COUNT RECORDS
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("c", help="Alias for count.")
|
2026-04-22 22:14:26 +02:00
|
|
|
@app.command(
|
|
|
|
|
"count",
|
2026-05-10 16:56:50 +02:00
|
|
|
help="Count records in a collection from the local Chroma database. Alias: c.",
|
2026-04-22 22:14:26 +02:00
|
|
|
)
|
|
|
|
|
def count(
|
|
|
|
|
collection: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="Name of the collection to count."),
|
|
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
try:
|
|
|
|
|
_run(lambda: handle_count_collection(collection))
|
|
|
|
|
except NotFoundError:
|
|
|
|
|
_fail(f"Collection '{collection}' does not exist.")
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# IMPORT DATA
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("i", help="Alias for import.")
|
2026-04-22 22:14:26 +02:00
|
|
|
@app.command(
|
2026-04-23 19:34:59 +02:00
|
|
|
"import",
|
2026-04-29 15:39:42 +02:00
|
|
|
help=(
|
|
|
|
|
"Chunk, embed, and add one or more files to a collection in the "
|
2026-05-10 16:56:50 +02:00
|
|
|
"local Chroma database. Alias: i."
|
2026-04-29 15:39:42 +02:00
|
|
|
),
|
2026-04-22 22:14:26 +02:00
|
|
|
)
|
2026-04-23 19:34:59 +02:00
|
|
|
def import_data(
|
2026-04-22 22:14:26 +02:00
|
|
|
collection: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="Name of the target collection."),
|
|
|
|
|
],
|
2026-04-29 15:39:42 +02:00
|
|
|
files: Annotated[
|
|
|
|
|
list[str],
|
|
|
|
|
typer.Argument(
|
|
|
|
|
help="Path(s) to the file(s) to chunk and add to the collection."
|
|
|
|
|
),
|
2026-04-22 22:14:26 +02:00
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
try:
|
2026-04-29 15:39:42 +02:00
|
|
|
_run(lambda: handle_import(collection, files))
|
2026-04-22 22:14:26 +02:00
|
|
|
except NotFoundError:
|
|
|
|
|
_fail(f"Collection '{collection}' does not exist.")
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# QUERY
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("q", help="Alias for query.")
|
|
|
|
|
@app.command("query", help="Query a collection with the provided text. Alias: q.")
|
2026-04-22 22:14:26 +02:00
|
|
|
def query(
|
|
|
|
|
collection: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="Name of the target collection."),
|
|
|
|
|
],
|
|
|
|
|
query_text: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="The text to query."),
|
|
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
try:
|
|
|
|
|
_run(lambda: handle_query(collection, query_text))
|
|
|
|
|
except NotFoundError:
|
|
|
|
|
_fail(f"Collection '{collection}' does not exist.")
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 19:37:13 +02:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
# DELETE DATA
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2026-05-10 16:56:50 +02:00
|
|
|
@app.command("del", help="Alias for delete.")
|
|
|
|
|
@app.command(
|
|
|
|
|
"delete",
|
|
|
|
|
help="Delete records from a collection using a metadata filter. Alias: del.",
|
|
|
|
|
)
|
2026-04-22 22:14:26 +02:00
|
|
|
def delete_records(
|
|
|
|
|
collection: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Argument(help="Name of the target collection."),
|
|
|
|
|
],
|
|
|
|
|
where: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Option(
|
|
|
|
|
"--where",
|
|
|
|
|
help="Metadata filter in the format <condition>=<value>.",
|
|
|
|
|
metavar="CONDITION=VALUE",
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
try:
|
|
|
|
|
_run(lambda: handle_delete_records(collection, where))
|
|
|
|
|
except NotFoundError:
|
|
|
|
|
_fail(f"Collection '{collection}' does not exist.")
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
_fail(str(exc))
|