diff --git a/docs/architecture.md b/docs/architecture.md index 0a7a8686..fc2876dd 100755 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,68 +1,65 @@ # Architecture ## Overview - Databao CLI is a Python CLI application built with Click that provides a command-line and web interface to the Databao Agent and Context Engine. -Key layers: +Project structure: +``` +src/databao_cli/ +- commands/ # CLI commands +- ui/ # Web UI (Streamlit) +- mcp/ # MCP server +- project/ # Project management +- log/ # Logging +``` -- CLI commands (`src/databao_cli/commands/`) -- Web UI via Streamlit (`src/databao_cli/ui/`) -- MCP server (`src/databao_cli/mcp/`) -- Project management (`src/databao_cli/project/`) -- Logging (`src/databao_cli/log/`) +## Key Dependencies +- `databao-context-engine` — core context indexing and querying +- `databao-agent` — AI agent for natural language interaction +- `click` — CLI framework +- `streamlit` — web UI framework +- `fastmcp` — MCP server framework ## Entry Point - -`databao_cli.__main__:cli` — a Click group that registers all subcommands. +`databao_cli.__main__:cli` — a Click group that registers all subcommands and +is exposed as the `databao` console script (run as `databao ...` after installation). +During development, it can also be invoked via `uv run databao`. ## CLI Commands +The CLI is implemented using the Click framework. -| Command | Module | Purpose | -|----------------------|---------------------------------|---------------------------------| -| `databao init` | `commands/init.py` | Initialize a new project | -| `databao status` | `commands/status.py` | Show project status | -| `databao datasource` | `commands/datasource/` | Add/check data sources | -| `databao build` | `commands/build.py` | Build context for datasources | -| `databao ask` | `commands/ask.py` | Interactive chat with agent | -| `databao app` | `commands/app.py` | Launch Streamlit web UI | -| `databao mcp` | `commands/mcp.py` | Run MCP server | - -## Web UI (Streamlit) +### Structure +- All CLI commands are located in `src/databao_cli/commands/` +- Single command = single file. +- Each command MUST be implemented in its own module. +- File name SHOULD match the command name. +- Grouped commands MUST be organized in subdirectories. Example: `src/databao_cli/commands/datasource/` -`src/databao_cli/ui/` contains the Streamlit application: +### Registration +- All commands and groups MUST be explicitly registered in `src/databao_cli/__main__.py` +- Commands and groups are exposed via the COMMANDS collection. Every new command or group MUST be added to this collection to be discoverable by the CLI. -- `app.py` — main Streamlit entry -- `pages/` — individual UI pages -- `components/` — reusable UI components -- `services/` — backend service wrappers -- `models/` — data models for UI state +## Web UI (Streamlit) +``` +src/databao_cli/ui/ # contains the Streamlit application + app.py # main Streamlit entry + pages/ # individual UI pages + components/ # reusable UI components + services/ # backend service wrappers + models/ # data models for UI state +``` ## MCP Server - -`src/databao_cli/mcp/` exposes tools via the Model Context Protocol: - -- `server.py` — FastMCP server setup -- `tools/` — individual tool handlers - -## Key Dependencies - -- `databao-context-engine` — core context indexing and querying -- `databao-agent` — AI agent for natural language interaction -- `click` — CLI framework -- `streamlit` — web UI framework -- `fastmcp` — MCP server framework - -## Database Support - -Multi-database via optional dependencies: Snowflake (default), BigQuery, -ClickHouse, Athena, MS SQL, with ADBC drivers. +``` +src/databao_cli/mcp/ # exposes tools via the Model Context Protocol: + server.py # FastMCP server setup + tools/ # individual tool handlers +``` ## Extension Points - -- Add CLI command: create module in `commands/`, register with Click group - in `__main__.py`. +- Add CLI command: create module in `commands/`, then add it to the + `COMMANDS` collection in `src/databao_cli/__main__.py`. - Add MCP tool: add handler in `mcp/tools/`, register in `mcp/server.py`. - Add datasource type: extend via `databao-context-engine` optional deps. - Add UI page: add to `ui/pages/`. diff --git a/src/databao_cli/__main__.py b/src/databao_cli/__main__.py index fe6a92f7..a45d0c5d 100644 --- a/src/databao_cli/__main__.py +++ b/src/databao_cli/__main__.py @@ -1,11 +1,20 @@ -import sys from pathlib import Path import click -from click import Context - +from click import Command, Context + +from databao_cli.commands.app import app +from databao_cli.commands.ask import ask +from databao_cli.commands.build import build +from databao_cli.commands.datasource import datasource +from databao_cli.commands.index import index +from databao_cli.commands.init import init +from databao_cli.commands.mcp import mcp +from databao_cli.commands.status import status from databao_cli.log.logging import configure_logging -from databao_cli.project.layout import ROOT_DOMAIN, ProjectLayout, find_project +from databao_cli.project.layout import find_project + +COMMANDS: list[Command] = [app, ask, build, datasource, index, init, mcp, status] @click.group() @@ -27,307 +36,8 @@ def cli(ctx: Context, verbose: bool, project_dir: Path | None) -> None: configure_logging(find_project(project_path), verbose=verbose) -@cli.command() -@click.pass_context -def status(ctx: Context) -> None: - """Display project status and system-wide information.""" - from databao_cli.commands.status import status_impl - - status_message = status_impl(ctx.obj["project_dir"]) - click.echo(status_message) - - -@cli.command() -@click.pass_context -def init(ctx: Context) -> None: - """Create a new Databao project.""" - from databao_cli.commands.datasource.add_datasource_config import add_datasource_config_interactive_impl - from databao_cli.commands.init import InitDatabaoProjectError, ProjectDirDoesnotExistError, init_impl - - project_dir = ctx.obj["project_dir"] - project_layout: ProjectLayout - try: - project_layout = init_impl(project_dir) - except ProjectDirDoesnotExistError: - if click.confirm( - f"The directory {project_dir.resolve()} does not exist. Do you want to create it?", - default=True, - ): - project_dir.mkdir(parents=True, exist_ok=False) - project_layout = init_impl(project_dir) - else: - return - except InitDatabaoProjectError as e: - click.echo(e.message, err=True) - sys.exit(1) - - click.echo(f"Project initialized successfully at {project_dir.resolve()}") - - # todo install ollama - # try: - # install_ollama_if_needed() - # except RuntimeError as e: - # click.echo(str(e), err=True) - - if not click.confirm("\nDo you want to configure a domain now?"): - return - - add_datasource_config_interactive_impl(project_layout, ROOT_DOMAIN) - - while click.confirm("\nDo you want to add more datasources?"): - add_datasource_config_interactive_impl(project_layout, ROOT_DOMAIN) - - -@cli.group() -def datasource() -> None: - """Manage datasource configurations.""" - pass - - -@datasource.command(name="add") -@click.option( - "-d", - "--domain", - type=click.STRING, - default="root", - help="Databao domain name", -) -@click.pass_context -def add_datasource_config(ctx: Context, domain: str) -> None: - """Add a new datasource configuration. - - The command will ask all relevant information for that datasource and save it in a chosen Databao domain - """ - from databao_cli.commands.datasource.add_datasource_config import add_datasource_config_interactive_impl - - project_layout = _get_project_or_exit(ctx.obj["project_dir"]) - add_datasource_config_interactive_impl(project_layout, domain) - - -@datasource.command(name="check") -@click.argument( - "domains", - type=click.STRING, - nargs=-1, -) -@click.pass_context -def check_datasource_config(ctx: Context, domains: list[str] | None) -> None: - """Check whether a datasource configuration is valid. - - The configuration is considered as valid if a connection with the datasource can be established. - - By default, all declared datasources across all domains in the project will be checked. - You can explicitely list which domains to validate by using the [DOMAINS] argument. - """ - from databao_cli.commands.datasource.check_datasource_connection import check_datasource_connection_impl - - project_layout = _get_project_or_exit(ctx.obj["project_dir"]) - check_datasource_connection_impl(project_layout, requested_domains=domains if domains else None) - - -@cli.command() -@click.option( - "-d", - "--domain", - type=click.STRING, - default="root", - help="Databao domain name", -) -@click.option( - "--should-index/--should-not-index", - default=True, - show_default=True, - help="Whether to index the context. If disabled, the context will be built but not indexed.", -) -@click.pass_context -def build(ctx: Context, domain: str, should_index: bool) -> None: - """Build context for all domain's datasources. - - The output of the build command will be saved in the domain's output directory. - - Internally, this indexes the context to be used by the MCP server and the "retrieve" command. - """ - from databao_cli.commands.build import build_impl - - project_layout = _get_project_or_exit(ctx.obj["project_dir"]) - results = build_impl(project_layout, domain, should_index) - click.echo(f"Build complete. Processed {len(results)} datasources.") - - -@cli.command() -@click.option( - "-d", - "--domain", - type=click.STRING, - default="root", - help="Databao domain name", -) -@click.argument( - "datasources-config-files", - nargs=-1, - type=click.STRING, -) -@click.pass_context -def index(ctx: Context, domain: str, datasources_config_files: tuple[str, ...]) -> None: - """Index built contexts into the embeddings database. - - If one or more datasource config file strings are provided, only those datasources will be indexed. - If no values are provided, all built contexts for the selected domain will be indexed. - """ - from databao_cli.commands.index import index_impl - - project_layout = _get_project_or_exit(ctx.obj["project_dir"]) - datasources = list(datasources_config_files) if datasources_config_files else None - results = index_impl(project_layout, domain, datasources) - click.echo(f"Index complete. Processed {len(results)} contexts.") - - -@cli.command() -@click.argument("question", required=False) -@click.option( - "--one-shot", - is_flag=True, - default=False, - help="Run single question and exit (default: interactive mode)", -) -@click.option( - "-m", - "--model", - type=str, - default=None, - help="LLM model in format provider:name (e.g., openai:gpt-4o, anthropic:claude-sonnet-4-6)", -) -@click.option( - "-t", - "--temperature", - type=float, - default=0.0, - help="Temperature 0.0-1.0 (default: 0.0)", -) -@click.option( - "--show-thinking/--no-show-thinking", - default=True, - help="Display reasoning/thinking output (streaming is implicit when enabled)", -) -@click.pass_context -def ask( - ctx: click.Context, - question: str | None, - one_shot: bool, - model: str | None, - temperature: float, - show_thinking: bool, -) -> None: - """Chat with the Databao agent. - - By default, starts an interactive chat session. Use --one-shot with a - QUESTION argument to run a single query and exit. - - \b - Examples: - databao ask # Interactive mode - databao ask --one-shot "What tables exist?" # One-shot mode - databao ask --model anthropic:claude-sonnet-4-6 # With custom model - databao ask --no-show-thinking # Hide reasoning - """ - from databao_cli.commands.ask import ask_impl - - ask_impl(ctx, question, one_shot, model, temperature, show_thinking) - - -@cli.command( - context_settings={"ignore_unknown_options": True, "allow_extra_args": True}, -) -@click.option( - "--read-only-domain", - is_flag=True, - default=False, - help="Disable all domain-editing operations (init, datasources, build) in the UI", -) -@click.option( - "--hide-suggested-questions", - is_flag=True, - default=False, - help="Hide the suggested questions on the empty chat screen", -) -@click.option( - "--hide-build-context-hint", - is_flag=True, - default=False, - help=( - "Hide the 'Context isn't built yet' warning on the empty chat screen and " - "remove the Build Context step from the setup wizard" - ), -) -@click.pass_context -def app(ctx: click.Context, read_only_domain: bool, hide_suggested_questions: bool, hide_build_context_hint: bool) -> None: - """Launch the Databao Streamlit web interface. - - All additional arguments are passed directly to streamlit run. - - \b - Examples: - databao app - databao app --server.port 8502 - databao app --server.headless true - databao app --read-only-domain - """ - from databao_cli.commands.app import app_impl - - ctx.obj["read_only_domain"] = read_only_domain - ctx.obj["hide_suggested_questions"] = hide_suggested_questions - ctx.obj["hide_build_context_hint"] = hide_build_context_hint - app_impl(ctx) - - -@cli.command() -@click.option( - "--transport", - type=click.Choice(["stdio", "sse"]), - default="stdio", - show_default=True, - help="MCP transport type.", -) -@click.option( - "--host", - type=str, - default="localhost", - show_default=True, - help="Host to bind to (SSE transport only).", -) -@click.option( - "--port", - type=int, - default=8765, - show_default=True, - help="Port to listen on (SSE transport only).", -) -@click.pass_context -def mcp(ctx: click.Context, transport: str, host: str, port: int) -> None: - """Run an MCP server exposing Databao tools. - - Starts a Model Context Protocol server that provides tools for - interacting with your Databao project programmatically. - - \b - Examples: - databao mcp # stdio (default) - databao mcp --transport sse # SSE on localhost:8765 - databao mcp --transport sse --port 9000 # SSE on custom port - """ - from databao_cli.commands.mcp import mcp_impl - - if transport == "stdio": - configure_logging(find_project(ctx.obj["project_dir"]), quiet=True) - mcp_impl(ctx.obj["project_dir"], transport, host, port) - - -def _get_project_or_exit(project_dir: Path) -> ProjectLayout: - project_layout = find_project(project_dir) - if not project_layout: - click.echo("No project found.") - exit(1) - return project_layout +for command in COMMANDS: + cli.add_command(command) if __name__ == "__main__": diff --git a/src/databao_cli/commands/_utils.py b/src/databao_cli/commands/_utils.py new file mode 100644 index 00000000..dd76260b --- /dev/null +++ b/src/databao_cli/commands/_utils.py @@ -0,0 +1,17 @@ +"""Shared CLI utilities for command implementations.""" + +import sys +from pathlib import Path + +import click + +from databao_cli.project.layout import ProjectLayout, find_project + + +def get_project_or_exit(project_dir: Path) -> ProjectLayout: + """Return the project layout or exit with an error if no project is found.""" + project_layout = find_project(project_dir) + if not project_layout: + click.echo("No project found.") + sys.exit(1) + return project_layout diff --git a/src/databao_cli/commands/app.py b/src/databao_cli/commands/app.py index 00198fb8..da22f211 100644 --- a/src/databao_cli/commands/app.py +++ b/src/databao_cli/commands/app.py @@ -2,22 +2,80 @@ import subprocess import sys +from pathlib import Path import click from databao_cli.ui.cli import bootstrap_streamlit_app -def app_impl(ctx: click.Context) -> None: +@click.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True}, +) +@click.option( + "--read-only-domain", + is_flag=True, + default=False, + help="Disable all domain-editing operations (init, datasources, build) in the UI", +) +@click.option( + "--hide-suggested-questions", + is_flag=True, + default=False, + help="Hide the suggested questions on the empty chat screen", +) +@click.option( + "--hide-build-context-hint", + is_flag=True, + default=False, + help=( + "Hide the 'Context isn't built yet' warning on the empty chat screen and " + "remove the Build Context step from the setup wizard" + ), +) +@click.pass_context +def app( + ctx: click.Context, + read_only_domain: bool, + hide_suggested_questions: bool, + hide_build_context_hint: bool, +) -> None: + """Launch the Databao Streamlit web interface. + + All additional arguments are passed directly to streamlit run. + + \b + Examples: + databao app + databao app --server.port 8502 + databao app --server.headless true + databao app --read-only-domain + """ + app_impl( + ctx.obj["project_dir"], + ctx.args, + read_only_domain=read_only_domain, + hide_suggested_questions=hide_suggested_questions, + hide_build_context_hint=hide_build_context_hint, + ) + + +def app_impl( + project_dir: Path, + extra_args: list[str], + read_only_domain: bool = False, + hide_suggested_questions: bool = False, + hide_build_context_hint: bool = False, +) -> None: click.echo("Starting Databao UI...") try: bootstrap_streamlit_app( - ctx.obj["project_dir"], - ctx.args, - read_only_domain=ctx.obj.get("read_only_domain", False), - hide_suggested_questions=ctx.obj.get("hide_suggested_questions", False), - hide_build_context_hint=ctx.obj.get("hide_build_context_hint", False), + project_dir, + extra_args, + read_only_domain=read_only_domain, + hide_suggested_questions=hide_suggested_questions, + hide_build_context_hint=hide_build_context_hint, ) except subprocess.CalledProcessError as e: click.echo(f"Error running Streamlit: {e}", err=True) diff --git a/src/databao_cli/commands/ask.py b/src/databao_cli/commands/ask.py index e72404c8..80494324 100644 --- a/src/databao_cli/commands/ask.py +++ b/src/databao_cli/commands/ask.py @@ -21,6 +21,57 @@ DEFAULT_MAX_DISPLAY_ROWS = 10 +@click.command() +@click.argument("question", required=False) +@click.option( + "--one-shot", + is_flag=True, + default=False, + help="Run single question and exit (default: interactive mode)", +) +@click.option( + "-m", + "--model", + type=str, + default=None, + help="LLM model in format provider:name (e.g., openai:gpt-4o, anthropic:claude-sonnet-4-6)", +) +@click.option( + "-t", + "--temperature", + type=float, + default=0.0, + help="Temperature 0.0-1.0 (default: 0.0)", +) +@click.option( + "--show-thinking/--no-show-thinking", + default=True, + help="Display reasoning/thinking output (streaming is implicit when enabled)", +) +@click.pass_context +def ask( + ctx: click.Context, + question: str | None, + one_shot: bool, + model: str | None, + temperature: float, + show_thinking: bool, +) -> None: + """Chat with the Databao agent. + + By default, starts an interactive chat session. Use --one-shot with a + QUESTION argument to run a single query and exit. + + \b + Examples: + databao ask # Interactive mode + databao ask --one-shot "What tables exist?" # One-shot mode + databao ask --model anthropic:claude-sonnet-4-6 # With custom model + databao ask --no-show-thinking # Hide reasoning + """ + ask_impl(ctx.obj["project_dir"], question, one_shot, model, temperature, show_thinking) + + def _create_cli_writer() -> StreamingWriter: """Create a StreamingWriter that echoes output to the CLI in real-time.""" return StreamingWriter(on_write=lambda text: click.echo(text, nl=False)) @@ -201,7 +252,7 @@ def run_one_shot_mode(agent: Agent, question: str, show_thinking: bool) -> None: def ask_impl( - ctx: click.Context, + project_path: Path, question: str | None, one_shot: bool, model: str | None, @@ -213,9 +264,6 @@ def ask_impl( click.echo("Error: QUESTION argument is required in one-shot mode.", err=True) sys.exit(1) - # Get project path from CLI context - project_path: Path = ctx.obj["project_dir"] - # Initialize agent (with progress indicator if not showing thinking) if not show_thinking: click.echo("Initializing agent...") diff --git a/src/databao_cli/commands/build.py b/src/databao_cli/commands/build.py index bf4b00df..1a88c3e9 100644 --- a/src/databao_cli/commands/build.py +++ b/src/databao_cli/commands/build.py @@ -1,8 +1,38 @@ +import click from databao_context_engine import BuildDatasourceResult, DatabaoContextDomainManager from databao_cli.project.layout import ProjectLayout +@click.command() +@click.option( + "-d", + "--domain", + type=click.STRING, + default="root", + help="Databao domain name", +) +@click.option( + "--should-index/--should-not-index", + default=True, + show_default=True, + help="Whether to index the context. If disabled, the context will be built but not indexed.", +) +@click.pass_context +def build(ctx: click.Context, domain: str, should_index: bool) -> None: + """Build context for all domain's datasources. + + The output of the build command will be saved in the domain's output directory. + + Internally, this indexes the context to be used by the MCP server and the "retrieve" command. + """ + from databao_cli.commands._utils import get_project_or_exit + + project_layout = get_project_or_exit(ctx.obj["project_dir"]) + results = build_impl(project_layout, domain, should_index) + click.echo(f"Build complete. Processed {len(results)} datasources.") + + def build_impl(project_layout: ProjectLayout, domain: str, should_index: bool) -> list[BuildDatasourceResult]: dce_project_dir = project_layout.domains_dir / domain results: list[BuildDatasourceResult] = DatabaoContextDomainManager(domain_dir=dce_project_dir).build_context( diff --git a/src/databao_cli/commands/datasource/__init__.py b/src/databao_cli/commands/datasource/__init__.py index e69de29b..531ccc5f 100644 --- a/src/databao_cli/commands/datasource/__init__.py +++ b/src/databao_cli/commands/datasource/__init__.py @@ -0,0 +1,13 @@ +import click + +from databao_cli.commands.datasource.add import add +from databao_cli.commands.datasource.check import check + + +@click.group() +def datasource() -> None: + """Manage datasource configurations.""" + + +datasource.add_command(add) +datasource.add_command(check) diff --git a/src/databao_cli/commands/datasource/add_datasource_config.py b/src/databao_cli/commands/datasource/add.py similarity index 76% rename from src/databao_cli/commands/datasource/add_datasource_config.py rename to src/databao_cli/commands/datasource/add.py index b1bb83e2..83f9c45d 100644 --- a/src/databao_cli/commands/datasource/add_datasource_config.py +++ b/src/databao_cli/commands/datasource/add.py @@ -7,12 +7,32 @@ DatasourceType, ) +from databao_cli.commands._utils import get_project_or_exit from databao_cli.commands.context_engine_cli import ClickUserInputCallback -from databao_cli.commands.datasource.check_datasource_connection import print_connection_check_results from databao_cli.project.layout import ProjectLayout -def add_datasource_config_interactive_impl(project_layout: ProjectLayout, domain: str) -> None: +@click.command(name="add") +@click.option( + "-d", + "--domain", + type=click.STRING, + default="root", + help="Databao domain name", +) +@click.pass_context +def add(ctx: click.Context, domain: str) -> None: + """Add a new datasource configuration. + + The command will ask all relevant information for that datasource and save it in a chosen Databao domain + """ + project_layout = get_project_or_exit(ctx.obj["project_dir"]) + add_impl(project_layout, domain) + + +def add_impl(project_layout: ProjectLayout, domain: str) -> None: + from databao_cli.commands.datasource.check import print_connection_check_results + domain_dir = project_layout.domains_dir / domain domain_manager = DatabaoContextDomainManager(domain_dir=domain_dir) plugin_loader = DatabaoContextPluginLoader() diff --git a/src/databao_cli/commands/datasource/check_datasource_connection.py b/src/databao_cli/commands/datasource/check.py similarity index 69% rename from src/databao_cli/commands/datasource/check_datasource_connection.py rename to src/databao_cli/commands/datasource/check.py index 7aad5411..1d2f72f2 100644 --- a/src/databao_cli/commands/datasource/check_datasource_connection.py +++ b/src/databao_cli/commands/datasource/check.py @@ -7,24 +7,30 @@ DatasourceId, ) +from databao_cli.commands._utils import get_project_or_exit from databao_cli.project.layout import ProjectLayout -def print_connection_check_results( - domain: str, datasource_results: dict[DatasourceId, CheckDatasourceConnectionResult] -) -> None: - for result in datasource_results.values(): - fq_datasource_name = domain + os.pathsep + str(result.datasource_id) - status = str(result.connection_status.value) - if result.summary: - status += f" - {result.summary}" - if result.full_message: - status += f": {result.full_message}" +@click.command(name="check") +@click.argument( + "domains", + type=click.STRING, + nargs=-1, +) +@click.pass_context +def check(ctx: click.Context, domains: tuple[str, ...]) -> None: + """Check whether a datasource configuration is valid. - click.echo(f"{fq_datasource_name}: {status}") + The configuration is considered as valid if a connection with the datasource can be established. + + By default, all declared datasources across all domains in the project will be checked. + You can explicitly list which domains to validate by using the [DOMAINS] argument. + """ + project_layout = get_project_or_exit(ctx.obj["project_dir"]) + check_impl(project_layout, requested_domains=list(domains) if domains else None) -def check_datasource_connection_impl(project_layout: ProjectLayout, requested_domains: list[str] | None) -> None: +def check_impl(project_layout: ProjectLayout, requested_domains: list[str] | None) -> None: domains: list[str] = project_layout.get_domain_names() if requested_domains is None else requested_domains results = _check_domains(project_layout, domains) @@ -37,6 +43,20 @@ def check_datasource_connection_impl(project_layout: ProjectLayout, requested_do print_connection_check_results(domain, datasource_results) +def print_connection_check_results( + domain: str, datasource_results: dict[DatasourceId, CheckDatasourceConnectionResult] +) -> None: + for result in datasource_results.values(): + fq_datasource_name = domain + os.pathsep + str(result.datasource_id) + status = str(result.connection_status.value) + if result.summary: + status += f" - {result.summary}" + if result.full_message: + status += f": {result.full_message}" + + click.echo(f"{fq_datasource_name}: {status}") + + def _check_domains( project_layout: ProjectLayout, domains: list[str] ) -> dict[str, dict[DatasourceId, CheckDatasourceConnectionResult]]: diff --git a/src/databao_cli/commands/index.py b/src/databao_cli/commands/index.py index 0e8c4406..445e9135 100644 --- a/src/databao_cli/commands/index.py +++ b/src/databao_cli/commands/index.py @@ -1,8 +1,37 @@ +import click from databao_context_engine import ChunkEmbeddingMode, DatabaoContextDomainManager, DatasourceId, IndexDatasourceResult from databao_cli.project.layout import ProjectLayout +@click.command() +@click.option( + "-d", + "--domain", + type=click.STRING, + default="root", + help="Databao domain name", +) +@click.argument( + "datasources-config-files", + nargs=-1, + type=click.STRING, +) +@click.pass_context +def index(ctx: click.Context, domain: str, datasources_config_files: tuple[str, ...]) -> None: + """Index built contexts into the embeddings database. + + If one or more datasource config file strings are provided, only those datasources will be indexed. + If no values are provided, all built contexts for the selected domain will be indexed. + """ + from databao_cli.commands._utils import get_project_or_exit + + project_layout = get_project_or_exit(ctx.obj["project_dir"]) + datasources = list(datasources_config_files) if datasources_config_files else None + results = index_impl(project_layout, domain, datasources) + click.echo(f"Index complete. Processed {len(results)} contexts.") + + def index_impl( project_layout: ProjectLayout, domain: str, datasources_config_files: list[str] | None ) -> list[IndexDatasourceResult]: diff --git a/src/databao_cli/commands/init.py b/src/databao_cli/commands/init.py index bbcb795c..d05d1a72 100644 --- a/src/databao_cli/commands/init.py +++ b/src/databao_cli/commands/init.py @@ -1,8 +1,44 @@ +import sys from pathlib import Path +import click from databao_context_engine import InitDomainError, init_dce_domain -from databao_cli.project.layout import ProjectLayout, find_project +from databao_cli.project.layout import ROOT_DOMAIN, ProjectLayout, find_project + + +@click.command() +@click.pass_context +def init(ctx: click.Context) -> None: + """Create a new Databao project.""" + from databao_cli.commands.datasource.add import add_impl as add_datasource_config_interactive_impl + + project_dir: Path = ctx.obj["project_dir"] + project_layout: ProjectLayout + try: + project_layout = init_impl(project_dir) + except ProjectDirDoesnotExistError: + if click.confirm( + f"The directory {project_dir.resolve()} does not exist. Do you want to create it?", + default=True, + ): + project_dir.mkdir(parents=True, exist_ok=False) + project_layout = init_impl(project_dir) + else: + return + except InitDatabaoProjectError as e: + click.echo(e.message, err=True) + sys.exit(1) + + click.echo(f"Project initialized successfully at {project_dir.resolve()}") + + if not click.confirm("\nDo you want to configure a domain now?"): + return + + add_datasource_config_interactive_impl(project_layout, ROOT_DOMAIN) + + while click.confirm("\nDo you want to add more datasources?"): + add_datasource_config_interactive_impl(project_layout, ROOT_DOMAIN) class InitDatabaoProjectError(ValueError): diff --git a/src/databao_cli/commands/mcp.py b/src/databao_cli/commands/mcp.py index 614a7d28..880e644a 100644 --- a/src/databao_cli/commands/mcp.py +++ b/src/databao_cli/commands/mcp.py @@ -2,9 +2,54 @@ from pathlib import Path +import click + from databao_cli.mcp.server import McpContext, run_server +@click.command() +@click.option( + "--transport", + type=click.Choice(["stdio", "sse"]), + default="stdio", + show_default=True, + help="MCP transport type.", +) +@click.option( + "--host", + type=str, + default="localhost", + show_default=True, + help="Host to bind to (SSE transport only).", +) +@click.option( + "--port", + type=int, + default=8765, + show_default=True, + help="Port to listen on (SSE transport only).", +) +@click.pass_context +def mcp(ctx: click.Context, transport: str, host: str, port: int) -> None: + """Run an MCP server exposing Databao tools. + + Starts a Model Context Protocol server that provides tools for + interacting with your Databao project programmatically. + + \b + Examples: + databao mcp # stdio (default) + databao mcp --transport sse # SSE on localhost:8765 + databao mcp --transport sse --port 9000 # SSE on custom port + """ + from databao_cli.log.logging import configure_logging + from databao_cli.project.layout import find_project + + if transport == "stdio": + configure_logging(find_project(ctx.obj["project_dir"]), quiet=True) + mcp_impl(ctx.obj["project_dir"], transport, host, port) + + def mcp_impl(project_dir: Path, transport: str, host: str, port: int) -> None: context = McpContext(project_dir=project_dir) run_server(context, transport=transport, host=host, port=port) diff --git a/src/databao_cli/commands/status.py b/src/databao_cli/commands/status.py index 7737396c..0ee09b86 100644 --- a/src/databao_cli/commands/status.py +++ b/src/databao_cli/commands/status.py @@ -3,6 +3,7 @@ from importlib.metadata import version from pathlib import Path +import click from databao_context_engine import ( DceDomainInfo, DceInfo, @@ -13,6 +14,13 @@ from databao_cli.project.layout import find_project +@click.command() +@click.pass_context +def status(ctx: click.Context) -> None: + """Display project status and system-wide information.""" + click.echo(status_impl(ctx.obj["project_dir"])) + + def status_impl(project_dir: Path) -> str: project_layout = find_project(project_dir) diff --git a/tests/test_build.py b/tests/test_build.py index 3067e7b5..a624950c 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -8,8 +8,9 @@ def test_databao_build_duckdb_datasource(project_layout: ProjectLayout, tmp_path_factory: TempPathFactory) -> None: test_db = tmp_path_factory.mktemp("duckdb") / "test_db.duckdb" - duckdb.connect(str(test_db)) - duckdb.execute("CREATE TABLE t1 AS SELECT 1 AS i, 2 AS j;") + conn = duckdb.connect(str(test_db)) + conn.execute("CREATE TABLE t1 AS SELECT 1 AS i, 2 AS j;") + conn.close() dce_domain_manager = DatabaoContextDomainManager(domain_dir=project_layout.root_domain_dir) dce_domain_manager.create_datasource_config( diff --git a/tests/test_index.py b/tests/test_index.py index 2d2a3310..8399f0d5 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -8,8 +8,9 @@ def test_databao_index_duckdb_datasource(project_layout: ProjectLayout, tmp_path_factory: TempPathFactory) -> None: test_db = tmp_path_factory.mktemp("duckdb") / "test_db.duckdb" - duckdb.connect(str(test_db)) - duckdb.execute("CREATE TABLE t1 AS SELECT 1 AS i, 2 AS j;") + conn = duckdb.connect(str(test_db)) + conn.execute("CREATE TABLE t1 AS SELECT 1 AS i, 2 AS j;") + conn.close() dce_domain_manager = DatabaoContextDomainManager(domain_dir=project_layout.root_domain_dir) dce_domain_manager.create_datasource_config(