From 295885f021a2eb2d0928ba2fd5eedb96369ac7e6 Mon Sep 17 00:00:00 2001 From: alanchen <40684202+chenliang15405@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:42:12 +0800 Subject: [PATCH] feat: add dbgpt app start cli (#2997) --- docs/docs/getting-started/cli-quickstart.md | 426 ++++++++++++++++++ docs/docs/getting-started/index.md | 9 + .../dbgpt-acc-auto/pyproject.toml | 2 +- .../src/dbgpt_acc_auto/_version.py | 2 +- .../dbgpt-acc-flash-attn/pyproject.toml | 2 +- .../src/dbgpt_acc_flash_attn/_version.py | 2 +- packages/dbgpt-app/pyproject.toml | 42 +- .../dbgpt_app/_builtin_examples/__init__.py | 7 + .../src/dbgpt_app/_builtin_skills/__init__.py | 6 + packages/dbgpt-app/src/dbgpt_app/_cli.py | 166 ++++++- packages/dbgpt-app/src/dbgpt_app/_version.py | 2 +- packages/dbgpt-app/src/dbgpt_app/base.py | 27 +- .../dbgpt-app/src/dbgpt_app/dbgpt_server.py | 43 +- .../initialization/skills_provisioning.py | 68 +++ .../initialization/workspace_provisioning.py | 41 ++ .../openapi/api_v1/agentic_data_api.py | 4 +- .../dbgpt_app/openapi/api_v1/examples_api.py | 97 ++-- .../tests/test_workspace_provisioning.py | 47 ++ packages/dbgpt-client/pyproject.toml | 2 +- .../dbgpt-client/src/dbgpt_client/_version.py | 2 +- packages/dbgpt-core/pyproject.toml | 2 +- packages/dbgpt-core/src/dbgpt/_version.py | 2 +- .../src/dbgpt/agent/skill/manage.py | 9 +- packages/dbgpt-core/src/dbgpt/cli/_config.py | 322 +++++++++++++ .../dbgpt-core/src/dbgpt/cli/_profile_cmd.py | 95 ++++ .../dbgpt-core/src/dbgpt/cli/_profiles.py | 203 +++++++++ packages/dbgpt-core/src/dbgpt/cli/_wizard.py | 325 +++++++++++++ .../dbgpt-core/src/dbgpt/cli/cli_scripts.py | 122 ++++- .../src/dbgpt/cli/tests/__init__.py | 0 .../src/dbgpt/cli/tests/conftest.py | 24 + .../src/dbgpt/cli/tests/test_cli_scripts.py | 65 +++ .../src/dbgpt/cli/tests/test_config.py | 381 ++++++++++++++++ .../src/dbgpt/cli/tests/test_integration.py | 170 +++++++ .../src/dbgpt/cli/tests/test_profile_cmd.py | 126 ++++++ .../src/dbgpt/cli/tests/test_profiles.py | 175 +++++++ .../src/dbgpt/cli/tests/test_smoke.py | 43 ++ .../src/dbgpt/cli/tests/test_wizard.py | 251 +++++++++++ .../src/dbgpt/configs/model_config.py | 55 ++- .../src/dbgpt/configs/tests/__init__.py | 0 .../dbgpt/configs/tests/test_model_config.py | 134 ++++++ .../dbgpt/model/proxy/tests/test_minimax.py | 6 - .../src/dbgpt/util/_db_migration_utils.py | 4 +- .../src/dbgpt/util/console/console.py | 158 ++++++- .../util/tests/test_db_migration_utils.py | 30 ++ packages/dbgpt-ext/pyproject.toml | 2 +- packages/dbgpt-ext/src/dbgpt_ext/_version.py | 2 +- packages/dbgpt-sandbox/pyproject.toml | 2 +- packages/dbgpt-serve/pyproject.toml | 2 +- .../dbgpt-serve/src/dbgpt_serve/_version.py | 2 +- pilot/examples/Walmart_Sales.db | Bin 0 -> 430080 bytes pyproject.toml | 2 +- 51 files changed, 3616 insertions(+), 95 deletions(-) create mode 100644 docs/docs/getting-started/cli-quickstart.md create mode 100644 docs/docs/getting-started/index.md create mode 100644 packages/dbgpt-app/src/dbgpt_app/_builtin_examples/__init__.py create mode 100644 packages/dbgpt-app/src/dbgpt_app/_builtin_skills/__init__.py create mode 100644 packages/dbgpt-app/src/dbgpt_app/initialization/skills_provisioning.py create mode 100644 packages/dbgpt-app/src/dbgpt_app/initialization/workspace_provisioning.py create mode 100644 packages/dbgpt-app/src/dbgpt_app/tests/test_workspace_provisioning.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/_config.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/_profile_cmd.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/_profiles.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/_wizard.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/__init__.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/conftest.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_cli_scripts.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_config.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_integration.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_profile_cmd.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_profiles.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_smoke.py create mode 100644 packages/dbgpt-core/src/dbgpt/cli/tests/test_wizard.py create mode 100644 packages/dbgpt-core/src/dbgpt/configs/tests/__init__.py create mode 100644 packages/dbgpt-core/src/dbgpt/configs/tests/test_model_config.py create mode 100644 packages/dbgpt-core/src/dbgpt/util/tests/test_db_migration_utils.py create mode 100644 pilot/examples/Walmart_Sales.db diff --git a/docs/docs/getting-started/cli-quickstart.md b/docs/docs/getting-started/cli-quickstart.md new file mode 100644 index 000000000..8cf8a3cc6 --- /dev/null +++ b/docs/docs/getting-started/cli-quickstart.md @@ -0,0 +1,426 @@ +--- +sidebar_position: 1 +--- + +# CLI Quick Start + +Install DB-GPT from PyPI and start it with a single command — no source checkout required. + +:::tip Prerequisites +- Python **3.10** or later +- [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager (recommended) or pip +::: + +## 1. Install + +```bash +# Recommended: use uv +uv pip install dbgpt-app + +# Or with pip +pip install dbgpt-app +``` + +:::info What's included +The default installation includes the **core framework** (CLI, FastAPI, SQLAlchemy, Agent), +**OpenAI-compatible LLM support** (also works with Kimi, Qwen, MiniMax, Z.AI), +**DashScope / Tongyi** support, **RAG document parsing**, and **ChromaDB** vector store. + +Need additional providers or data sources? See [Optional Modules](#optional-modules). +::: + +After installation the `dbgpt` command is available in your terminal. + +## 2. Start DB-GPT + +```bash +dbgpt start +``` + +That's it! On first run DB-GPT will launch an **interactive setup wizard** that helps you: + +1. Choose an LLM provider (OpenAI, Kimi, Qwen, MiniMax, Z.AI, or a custom endpoint) +2. Enter your API key (or use an environment variable) +3. Confirm the model names and API base URL + +Once complete, a TOML configuration file is written to `~/.dbgpt/configs/.toml` and the web server starts automatically. Open your browser at **http://localhost:5670**. + +### What the startup looks like + +``` + ____ ____ ____ ____ _____ + | _ \| __ ) / ___| _ \_ _| + | | | | _ \ ____| | _| |_) || | + | |_| | |_) |____| |_| | __/ | | + |____/|____/ \____|_| |_| + + 🚀 DB-GPT Quick Start + + +- - - - - - - - - - - - - - - - - - - - - - - -+ + : Profile: openai : + : Config: /Users/you/.dbgpt/configs/openai.toml: + : Workspace: /Users/you/.dbgpt/workspace : + +- - - - - - - - - - - - - - - - - - - - - - - -+ +``` + +--- + +## 3. Command Reference + +### Overview + +``` +dbgpt [OPTIONS] COMMAND [ARGS]... + +Options: + --log-level TEXT Log level (default: warn) + --version Show version and exit + --help Show help message + +Commands: + start Start the DB-GPT server + stop Stop a running server + setup Configure LLM provider (interactive wizard or CI mode) + profile Manage configuration profiles + knowledge Knowledge base operations + model Manage model serving + db Database management and migration + ... +``` + +--- + +### `dbgpt start` + +Start the DB-GPT web server. Running `dbgpt start` without a subcommand is equivalent to `dbgpt start web`. + +#### Subcommands + +| Subcommand | Description | +|---|---| +| `web` (or `webserver`) | Start the web server (default) | +| `none` | API-only mode — *planned for a future release* | +| `controller` | Start the model controller | +| `worker` | Start a model worker | +| `apiserver` | Start the API server | + +#### `dbgpt start web` Options + +| Option | Short | Type | Default | Description | +|---|---|---|---|---| +| `--config` | `-c` | PATH | *auto* | Path to a TOML config file. If omitted, uses the active profile or launches the setup wizard. | +| `--profile` | `-p` | TEXT | *active* | Provider profile name (`openai`, `kimi`, `qwen`, `minimax`, `glm`, `custom`). Overrides the active profile. | +| `--yes` | `-y` | FLAG | false | Non-interactive mode: skip the wizard and use defaults / environment variables. Ideal for CI/CD. | +| `--api-key` | | TEXT | *env* | API key for the chosen provider. Can also be set via the provider's own environment variable. | +| `--daemon` | `-d` | FLAG | false | Run as a background daemon. Stop with `dbgpt stop webserver`. | + +#### Examples + +```bash +# Interactive (first run) — wizard will guide you +dbgpt start + +# Use an existing profile +dbgpt start web --profile openai + +# Non-interactive with explicit API key +dbgpt start web --profile kimi --api-key sk-xxx --yes + +# Use a specific config file +dbgpt start web --config /path/to/my-config.toml + +# Run as a daemon +dbgpt start web --daemon +``` + +#### Config Resolution Priority + +When the web server starts, the configuration file is resolved in this order: + +1. **`--config` flag** — if specified, use this file directly +2. **`--profile` flag** — look up `~/.dbgpt/configs/.toml` +3. **Active profile** — read from `~/.dbgpt/config.toml` +4. **Setup wizard** — if nothing is configured yet, launch the interactive wizard + +--- + +### `dbgpt stop` + +Stop running DB-GPT server processes. + +```bash +# Stop the web server +dbgpt stop webserver + +# Stop the web server on a specific port +dbgpt stop webserver --port 5670 + +# Stop all servers +dbgpt stop all +``` + +--- + +### `dbgpt setup` + +Configure the LLM provider interactively, or in non-interactive / CI mode. This command writes a TOML config to `~/.dbgpt/configs/.toml` and marks it as the active profile. + +#### Options + +| Option | Short | Type | Default | Description | +|---|---|---|---|---| +| `--profile` | `-p` | TEXT | *interactive* | Provider profile to configure. If omitted, an interactive menu is shown. | +| `--yes` | `-y` | FLAG | false | Non-interactive mode: skip the wizard and use defaults. | +| `--api-key` | | TEXT | *env* | API key. Also reads `DBGPT_API_KEY` env var. | +| `--show` | | FLAG | false | Show the current active profile and config path, then exit. | + +#### Examples + +```bash +# Interactive wizard +dbgpt setup + +# Non-interactive: use OpenAI with env key +export OPENAI_API_KEY=sk-xxx +dbgpt setup --profile openai --yes + +# Non-interactive with explicit key +dbgpt setup --profile kimi --api-key sk-xxx + +# Show current configuration +dbgpt setup --show +``` + +--- + +### `dbgpt profile` + +Manage multiple configuration profiles. Each profile is a TOML file under `~/.dbgpt/configs/`. + +#### Subcommands + +| Subcommand | Description | +|---|---| +| `list` | List all profiles. The active one is marked with `*`. | +| `show ` | Display the TOML content of a profile. | +| `create ` | Create (or reconfigure) a profile using the setup wizard. | +| `switch ` | Set a profile as the active default. | +| `delete ` | Delete a profile configuration file. | + +#### Examples + +```bash +# List all profiles +dbgpt profile list +# openai ← no asterisk +# * kimi ← active + +# Show profile content +dbgpt profile show openai + +# Create a new profile +dbgpt profile create qwen + +# Switch active profile +dbgpt profile switch openai + +# Delete a profile +dbgpt profile delete minimax +dbgpt profile delete minimax --yes # skip confirmation +``` + +--- + +## 4. Supported Providers + +The setup wizard and `--profile` flag support the following providers: + +| Profile Name | Display Name | LLM Model | Embedding Model | API Key Env Var | +|---|---|---|---|---| +| `openai` | OpenAI | gpt-4o | text-embedding-3-small | `OPENAI_API_KEY` | +| `kimi` | Kimi | kimi-k2 | text-embedding-v3 | `MOONSHOT_API_KEY` (+ `DASHSCOPE_API_KEY` for embeddings) | +| `qwen` | Qwen | qwen-plus | text-embedding-v3 | `DASHSCOPE_API_KEY` | +| `minimax` | MiniMax | abab6.5s-chat | embo-01 | `MINIMAX_API_KEY` | +| `glm` | Z.AI | glm-4-plus | embedding-3 | `ZHIPUAI_API_KEY` | +| `custom` | Custom | gpt-4o | text-embedding-3-small | `OPENAI_API_KEY` | + +:::info +The **Custom** profile lets you connect to any OpenAI-compatible API endpoint. During the wizard you'll be asked for the API base URL. +::: + +--- + +## 5. Directory Structure + +After first run, DB-GPT creates the following structure under your home directory: + +``` +~/.dbgpt/ +├── config.toml # Records the active profile name +├── configs/ +│ ├── openai.toml # Profile: OpenAI +│ ├── kimi.toml # Profile: Kimi +│ └── ... # One file per profile +└── workspace/ + └── pilot/ # Runtime workspace (databases, data files, etc.) + ├── meta_data/ + │ └── dbgpt.db # SQLite metadata database + └── data/ # Vector store data +``` + +### Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `DBGPT_HOME` | `~/.dbgpt` | Override the DB-GPT home directory | +| `OPENAI_API_KEY` | — | OpenAI API key (used by `openai` and `custom` profiles) | +| `MOONSHOT_API_KEY` | — | Kimi / Moonshot API key | +| `DASHSCOPE_API_KEY` | — | Qwen / DashScope API key (also used for Kimi embeddings) | +| `MINIMAX_API_KEY` | — | MiniMax API key | +| `ZHIPUAI_API_KEY` | — | Z.AI / Zhipu API key | +| `DBGPT_API_KEY` | — | Generic API key (fallback for `--api-key` flag) | +| `DBGPT_LANG` | `en` | UI language (`en` or `zh`) | + +--- + +## 6. Common Workflows + +### First-time setup + +```bash +pip install dbgpt-app +dbgpt start +# Follow the wizard → choose provider → enter API key → server starts +``` + +### Switch between providers + +```bash +# Create a Kimi profile +dbgpt profile create kimi + +# Switch to it +dbgpt profile switch kimi + +# Start with the new profile +dbgpt start +``` + +### CI/CD deployment + +```bash +export OPENAI_API_KEY=sk-xxx +dbgpt setup --profile openai --yes +dbgpt start web --daemon +``` + +### Custom endpoint (e.g. Azure OpenAI, local vLLM) + +```bash +dbgpt setup --profile custom +# Wizard will ask for: +# - API base URL (e.g. http://localhost:8000/v1) +# - API key +# - Model names +``` + +--- + +## 7. Optional Modules + +The core framework is included by default when you `pip install dbgpt-app`. Use extras to add LLM providers, vector stores, data sources, and more. + +### LLM Providers + +| Extra | Provider | Key packages | +|-------|----------|-------------| +| `proxy_openai` | OpenAI, Kimi, Qwen, MiniMax, Z.AI, any OpenAI-compatible API | `openai`, `tiktoken` | +| `proxy_ollama` | Ollama (local models) | `ollama` | +| `proxy_zhipuai` | Zhipu AI (GLM) | `openai` | +| `proxy_tongyi` | Tongyi Qianwen | `openai`, `dashscope` | +| `proxy_qianfan` | Baidu Qianfan | `qianfan` | +| `proxy_anthropic` | Anthropic Claude | `anthropic` | + +### Vector Stores + +| Extra | Storage | Key packages | +|-------|---------|-------------| +| `storage_chromadb` | ChromaDB | `chromadb`, `onnxruntime` | +| `storage_milvus` | Milvus | `pymilvus` | +| `storage_weaviate` | Weaviate | `weaviate-client` | +| `storage_elasticsearch` | Elasticsearch | `elasticsearch` | +| `storage_obvector` | OBVector | `pyobvector` | + +### Knowledge & RAG + +| Extra | What it adds | Key packages | +|-------|-------------|-------------| +| `rag` | Document parsing (PDF, DOCX, PPTX, Markdown, HTML) | `spacy`, `pypdf`, `python-docx`, `python-pptx` | +| `graph_rag` | Graph-based RAG with TuGraph/Neo4j | `networkx`, `neo4j` | + +### Data Sources + +| Extra | Database | Key packages | +|-------|----------|-------------| +| `datasource_mysql` | MySQL | `mysqlclient` | +| `datasource_postgres` | PostgreSQL | `psycopg2-binary` | +| `datasource_clickhouse` | ClickHouse | `clickhouse-connect` | +| `datasource_oracle` | Oracle | `oracledb` | +| `datasource_mssql` | SQL Server | `pymssql` | +| `datasource_spark` | Apache Spark | `pyspark` | +| `datasource_hive` | Hive | `pyhive` | +| `datasource_vertica` | Vertica | `vertica-python` | + +### Example: combine multiple extras + +```bash +# OpenAI + ChromaDB + RAG + MySQL +pip install "dbgpt-app[proxy_openai,storage_chromadb,rag,datasource_mysql]" +``` + +:::tip Minimal install +If you only need the core framework without any LLM or storage: +```bash +pip install dbgpt-app +``` +This gives you the CLI, FastAPI server, and agent framework — but you'll need to add at least one LLM provider extra to actually use it. +::: + +--- + +## 8. Troubleshooting + +### Port already in use + +```bash +# Stop the existing server +dbgpt stop webserver --port 5670 + +# Or choose a different port by editing the config file +# [service.web] +# port = 5671 +``` + +### "No config file found" error + +This means no profile has been set up yet. Run: + +```bash +dbgpt setup +``` + +### Changing your API key + +Re-run the setup wizard for the same profile — it will overwrite the existing config: + +```bash +dbgpt setup --profile openai +# Or simply edit ~/.dbgpt/configs/openai.toml directly +``` + +### View current configuration + +```bash +dbgpt setup --show +dbgpt profile show openai +``` diff --git a/docs/docs/getting-started/index.md b/docs/docs/getting-started/index.md new file mode 100644 index 000000000..b70763d6a --- /dev/null +++ b/docs/docs/getting-started/index.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 0 +--- + +# Getting Started + +Welcome to DB-GPT! This section will help you get up and running quickly. + +- **[CLI Quick Start](./cli-quickstart)** — Install DB-GPT via pip and start it with a single command. Includes interactive setup wizard, profile management, and all CLI options. diff --git a/packages/dbgpt-accelerator/dbgpt-acc-auto/pyproject.toml b/packages/dbgpt-accelerator/dbgpt-acc-auto/pyproject.toml index f2c8d2e57..042cfdfe8 100644 --- a/packages/dbgpt-accelerator/dbgpt-acc-auto/pyproject.toml +++ b/packages/dbgpt-accelerator/dbgpt-acc-auto/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-acc-auto" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "Add your description here" authors = [ { name = "csunny", email = "cfqcsunny@gmail.com" } diff --git a/packages/dbgpt-accelerator/dbgpt-acc-auto/src/dbgpt_acc_auto/_version.py b/packages/dbgpt-accelerator/dbgpt-acc-auto/src/dbgpt_acc_auto/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-accelerator/dbgpt-acc-auto/src/dbgpt_acc_auto/_version.py +++ b/packages/dbgpt-accelerator/dbgpt-acc-auto/src/dbgpt_acc_auto/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/pyproject.toml b/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/pyproject.toml index 1d65fbd17..3b5d239ff 100644 --- a/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/pyproject.toml +++ b/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/pyproject.toml @@ -2,7 +2,7 @@ # https://github.com/astral-sh/uv/issues/2252#issuecomment-2624150395 [project] name = "dbgpt-acc-flash-attn" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "Add your description here" readme = "README.md" requires-python = ">=3.10" diff --git a/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/src/dbgpt_acc_flash_attn/_version.py b/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/src/dbgpt_acc_flash_attn/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/src/dbgpt_acc_flash_attn/_version.py +++ b/packages/dbgpt-accelerator/dbgpt-acc-flash-attn/src/dbgpt_acc_flash_attn/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/packages/dbgpt-app/pyproject.toml b/packages/dbgpt-app/pyproject.toml index 3cbe88298..cb391fb0f 100644 --- a/packages/dbgpt-app/pyproject.toml +++ b/packages/dbgpt-app/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-app" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "Add your description here" authors = [ { name = "csunny", email = "cfqcsunny@gmail.com" } @@ -11,8 +11,8 @@ requires-python = ">= 3.10" dependencies = [ "dbgpt-acc-auto", - "dbgpt", - "dbgpt-ext", + "dbgpt[client,cli,agent,simple_framework,framework,code,proxy_openai,proxy_tongyi,proxy_zhipuai]", + "dbgpt-ext[rag,storage_chromadb]", "dbgpt-serve", "dbgpt-client", "dbgpt-sandbox", @@ -64,3 +64,39 @@ exclude = [ "src/dbgpt_app/**/examples/*" ] +[tool.hatch.build.targets.sdist.force-include] +# Builtin skills +"../../skills/csv-data-analysis" = "skills/csv-data-analysis" +"../../skills/skill-creator" = "skills/skill-creator" +"../../skills/financial-report-analyzer" = "skills/financial-report-analyzer" +"../../skills/walmart-sales-analyzer" = "skills/walmart-sales-analyzer" +"../../skills/agent-browser" = "skills/agent-browser" +# Builtin example files +"../../docker/examples/excel/Walmart_Sales.csv" = "examples/excel/Walmart_Sales.csv" +"../../docker/examples/fin_report/pdf/2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" = "examples/fin_report/pdf/2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" +# Pilot workspace template files (source of truth: pilot/) +"../../pilot/meta_data/alembic.ini" = "pilot_tpl/meta_data/alembic.ini" +"../../pilot/meta_data/alembic/README" = "pilot_tpl/meta_data/alembic/README" +"../../pilot/meta_data/alembic/env.py" = "pilot_tpl/meta_data/alembic/env.py" +"../../pilot/meta_data/alembic/script.py.mako" = "pilot_tpl/meta_data/alembic/script.py.mako" +"../../pilot/benchmark_meta_data/2025_07_27_public_500_standard_benchmark_question_list.xlsx" = "pilot_tpl/benchmark_meta_data/2025_07_27_public_500_standard_benchmark_question_list.xlsx" +"../../pilot/examples/Walmart_Sales.db" = "pilot_tpl/examples/Walmart_Sales.db" + +[tool.hatch.build.targets.wheel.force-include] +# Builtin skills +"skills/csv-data-analysis" = "dbgpt_app/_builtin_skills/csv-data-analysis" +"skills/skill-creator" = "dbgpt_app/_builtin_skills/skill-creator" +"skills/financial-report-analyzer" = "dbgpt_app/_builtin_skills/financial-report-analyzer" +"skills/walmart-sales-analyzer" = "dbgpt_app/_builtin_skills/walmart-sales-analyzer" +"skills/agent-browser" = "dbgpt_app/_builtin_skills/agent-browser" +# Builtin example files +"examples/excel/Walmart_Sales.csv" = "dbgpt_app/_builtin_examples/excel/Walmart_Sales.csv" +"examples/fin_report/pdf/2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" = "dbgpt_app/_builtin_examples/fin_report/pdf/2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" +# Pilot workspace template files (provisioned to ~/.dbgpt/workspace/pilot/ on first startup) +"pilot_tpl/meta_data/alembic.ini" = "dbgpt_app/pilot_template/meta_data/alembic.ini" +"pilot_tpl/meta_data/alembic/README" = "dbgpt_app/pilot_template/meta_data/alembic/README" +"pilot_tpl/meta_data/alembic/env.py" = "dbgpt_app/pilot_template/meta_data/alembic/env.py" +"pilot_tpl/meta_data/alembic/script.py.mako" = "dbgpt_app/pilot_template/meta_data/alembic/script.py.mako" +"pilot_tpl/benchmark_meta_data/2025_07_27_public_500_standard_benchmark_question_list.xlsx" = "dbgpt_app/pilot_template/benchmark_meta_data/2025_07_27_public_500_standard_benchmark_question_list.xlsx" +"pilot_tpl/examples/Walmart_Sales.db" = "dbgpt_app/pilot_template/examples/Walmart_Sales.db" + diff --git a/packages/dbgpt-app/src/dbgpt_app/_builtin_examples/__init__.py b/packages/dbgpt-app/src/dbgpt_app/_builtin_examples/__init__.py new file mode 100644 index 000000000..91c0c1e6e --- /dev/null +++ b/packages/dbgpt-app/src/dbgpt_app/_builtin_examples/__init__.py @@ -0,0 +1,7 @@ +"""Builtin example files bundled with the dbgpt-app wheel. + +This package exists solely as an anchor so that code can locate the +bundled example data files via ``os.path.dirname(__file__)``. +The actual data files are injected by hatch ``force-include`` at build +time and are **not** present during source-code development. +""" diff --git a/packages/dbgpt-app/src/dbgpt_app/_builtin_skills/__init__.py b/packages/dbgpt-app/src/dbgpt_app/_builtin_skills/__init__.py new file mode 100644 index 000000000..9caa777c9 --- /dev/null +++ b/packages/dbgpt-app/src/dbgpt_app/_builtin_skills/__init__.py @@ -0,0 +1,6 @@ +"""Builtin skills bundled with dbgpt-app. + +These skills are copied into the user's skills directory (~/.dbgpt/skills/) +on first startup via ``ensure_builtin_skills()``. Do **not** modify files +in this directory directly -- they will be overwritten on package upgrade. +""" diff --git a/packages/dbgpt-app/src/dbgpt_app/_cli.py b/packages/dbgpt-app/src/dbgpt_app/_cli.py index dd47268e4..512517255 100644 --- a/packages/dbgpt-app/src/dbgpt_app/_cli.py +++ b/packages/dbgpt-app/src/dbgpt_app/_cli.py @@ -5,24 +5,168 @@ from typing import Any, Dict, Optional import click from dbgpt.configs.model_config import LOGDIR -from dbgpt.model.cli import add_start_server_options from dbgpt.util.command_utils import _run_current_with_daemon, _stop_service from dbgpt.util.i18n_utils import _ _GLOBAL_CONFIG: str = "" +_BANNER_ART = """\ + ____ ____ ____ ____ _____ + | _ \\| __ ) / ___| _ \\_ _| + | | | | _ \\ ____| | _| |_) || | + | |_| | |_) |____| |_| | __/ | | + |____/|____/ \\____|_| |_|\ +""" + + +def _print_banner() -> None: + """Print the DB-GPT ASCII art banner to the terminal.""" + from dbgpt.util.console.console import CliLogger + + _log = CliLogger() + _log.print(f"[bold green]{_BANNER_ART}[/bold green]") + _log.print("") + _log.print(" [dim]🚀 DB-GPT Quick Start[/dim]") + _log.print("") + + +def _add_webserver_start_options(func): + """Click options decorator for the webserver start command. + + Unlike the generic ``add_start_server_options``, ``--config`` here is + *optional* so that users can rely on ``--profile`` / wizard flow instead. + """ + + @click.option( + "-c", + "--config", + type=str, + required=False, + default=None, + help=_( + "Path to a TOML config file. If omitted, DB-GPT will use the active " + "profile from ~/.dbgpt/ or run the first-time setup wizard." + ), + ) + @click.option( + "-p", + "--profile", + type=str, + required=False, + default=None, + help=_( + "Name of the provider profile to use (openai / kimi / qwen / minimax / " + "deepseek / ollama). Overrides the active profile in ~/.dbgpt/config.toml." + ), + ) + @click.option( + "-y", + "--yes", + is_flag=True, + default=False, + help=_( + "Non-interactive mode: skip the setup wizard and use defaults / " + "environment variables. Useful for CI/CD and scripted installs." + ), + ) + @click.option( + "--api-key", + type=str, + required=False, + default=None, + envvar="DBGPT_API_KEY", + help=_( + "API key for the chosen provider. Can also be set via the provider's " + "own environment variable (e.g. OPENAI_API_KEY)." + ), + ) + @click.option( + "-d", + "--daemon", + is_flag=True, + help=_( + "Run in daemon mode. It will run in the background. If you want to stop" + " it, use `dbgpt stop` command" + ), + ) + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + @click.command(name="webserver") -@add_start_server_options -def start_webserver(config: str, **kwargs): - """Start webserver(dbgpt_server.py)""" - if kwargs["daemon"]: +@_add_webserver_start_options +def start_webserver( + config: Optional[str], + profile: Optional[str], + yes: bool, + api_key: Optional[str], + **kwargs, +): + """Start webserver (dbgpt_server.py). + + On first run (or when no config is found) DB-GPT will launch an + interactive setup wizard so you can choose your LLM provider and API key. + Use ``--yes`` to skip the wizard in non-interactive environments. + """ + # Print banner first — skip in daemon mode (output goes to a log file) + if not kwargs.get("daemon"): + _print_banner() + + if kwargs.get("daemon"): log_file = os.path.join(LOGDIR, "webserver_uvicorn.log") _run_current_with_daemon("WebServer", log_file) - else: - from dbgpt_app.dbgpt_server import run_webserver + return - run_webserver(config) + # Resolve (or create) a config file via the wizard if needed + try: + from dbgpt.cli._wizard import maybe_run_wizard + + resolved_config = maybe_run_wizard( + profile=profile, + config=config, + yes=yes, + api_key=api_key, + ) + except ImportError: + # Graceful fallback: if wizard module is somehow unavailable, require --config + if not config: + raise click.UsageError( + "No config file found. Please pass --config or run `dbgpt setup`." + ) + resolved_config = config + + from pathlib import Path + + from dbgpt.cli._config import dbgpt_home + from dbgpt.util.console.console import CliLogger + + _log = CliLogger() + _profile = Path(resolved_config).stem + _workspace = dbgpt_home() / "workspace" + _log.print("") + _info_lines = [ + ("Profile: ", str(_profile)), + ("Config: ", str(resolved_config)), + ("Workspace: ", str(_workspace)), + ] + _max_len = max(len(f" {lbl}{val}") for lbl, val in _info_lines) + _inner_w = _max_len + 2 # 1 space padding each side + _dash_line = "- " * ((_inner_w + 1) // 2) + _dash_line = _dash_line[:_inner_w] # trim to exact width + _log.print(f" +{_dash_line}+", highlight=False) + for _lbl, _val in _info_lines: + _content = f" {_lbl}[bold]{_val}[/bold]" + _pad = _max_len - len(f" {_lbl}{_val}") + _log.print(f" : {_content}{' ' * _pad} :", highlight=False) + _log.print(f" +{_dash_line}+", highlight=False) + _log.print("") + + from dbgpt_app.dbgpt_server import run_webserver + + run_webserver(resolved_config) @click.command(name="webserver") @@ -312,6 +456,12 @@ def _get_migration_config( default_meta_data_path = _initialize_db( db_url, "sqlite", db_name, db_engine_args, try_to_create_db=True ) + from dbgpt_app.initialization.workspace_provisioning import _ensure_pilot_workspace + + # Provision pilot workspace template files for pip-installed users. + # dest_root is the parent of meta_data/ (e.g. ~/.dbgpt/workspace/pilot/) + pilot_root = os.path.dirname(default_meta_data_path) + _ensure_pilot_workspace(pilot_root) alembic_cfg = create_alembic_config( default_meta_data_path, db_manager.engine, diff --git a/packages/dbgpt-app/src/dbgpt_app/_version.py b/packages/dbgpt-app/src/dbgpt_app/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-app/src/dbgpt_app/_version.py +++ b/packages/dbgpt-app/src/dbgpt_app/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/packages/dbgpt-app/src/dbgpt_app/base.py b/packages/dbgpt-app/src/dbgpt_app/base.py index b7bc37404..973d01655 100644 --- a/packages/dbgpt-app/src/dbgpt_app/base.py +++ b/packages/dbgpt-app/src/dbgpt_app/base.py @@ -104,7 +104,32 @@ def _migration_db_storage( from dbgpt_app.initialization.db_model_initialization import _MODELS # noqa: F401 from dbgpt_ext.datasource.rdbms.conn_sqlite import SQLiteConnectorParameters - default_meta_data_path = os.path.join(PILOT_PATH, "meta_data") + # Derive meta_data path from the resolved db path when available. + # For pip-installed users, db_params.path is an absolute path like + # ~/.dbgpt/workspace/pilot/meta_data/dbgpt.db (already resolved by + # _initialize_db_storage), so we use its parent directory. For source-code + # developers with relative paths, we fall back to PILOT_PATH/meta_data. + if ( + isinstance(db_params, SQLiteConnectorParameters) + and hasattr(db_params, "path") + and db_params.path + and os.path.isabs(db_params.path) + ): + default_meta_data_path = os.path.dirname(db_params.path) + else: + default_meta_data_path = os.path.join(PILOT_PATH, "meta_data") + from dbgpt_app.initialization.workspace_provisioning import _ensure_pilot_workspace + + # Provision pilot workspace template files for pip-installed users. + # dest_root is the parent of meta_data/ (e.g. ~/.dbgpt/workspace/pilot/) + pilot_root = os.path.dirname(default_meta_data_path) + _ensure_pilot_workspace(pilot_root) + + # Provision builtin skills for pip-installed users. + from dbgpt.configs.model_config import SKILLS_DIR + from dbgpt_app.initialization.skills_provisioning import ensure_builtin_skills + + ensure_builtin_skills(SKILLS_DIR) if not disable_alembic_upgrade: from dbgpt.storage.metadata.db_manager import db from dbgpt.util._db_migration_utils import _ddl_init_and_upgrade diff --git a/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py b/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py index 5df23bb6f..09ae592e5 100644 --- a/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py +++ b/packages/dbgpt-app/src/dbgpt_app/dbgpt_server.py @@ -89,10 +89,11 @@ def mount_routers(app: FastAPI): def mount_static_files(app: FastAPI, param: ApplicationConfig): + package_dir = os.path.dirname(os.path.abspath(__file__)) if param.service.web.new_web_ui: - static_file_path = os.path.join(ROOT_PATH, "src", "dbgpt_app/static/web") + static_file_path = os.path.join(package_dir, "static", "web") else: - static_file_path = os.path.join(ROOT_PATH, "src", "dbgpt_app/static/old_web") + static_file_path = os.path.join(package_dir, "static", "old_web") os.makedirs(STATIC_MESSAGE_IMG_PATH, exist_ok=True) app.mount( @@ -172,25 +173,37 @@ def initialize_app(param: ApplicationConfig, args: List[str] = None): # Register default data sources try: - from dbgpt.configs.model_config import ROOT_PATH + from dbgpt.configs.model_config import PILOT_PATH, ROOT_PATH from dbgpt_serve.datasource.manages.connect_config_db import ConnectConfigDao dao = ConnectConfigDao() db_name = "Walmart_Sales" if not dao.get_by_names(db_name): - db_absolute_path = os.path.join( - ROOT_PATH, "docker/examples/dashboard/Walmart_Sales.db" - ) - dao.add_file_db( - db_name=db_name, - db_type="sqlite", - db_path=db_absolute_path, - comment="Default Walmart Sales example database", - ) - logger.info( - f"Successfully registered default data source: " - f"{db_name} at {db_absolute_path}" + candidate_paths = [ + os.path.join(PILOT_PATH, "examples", "Walmart_Sales.db"), + os.path.join( + ROOT_PATH, "docker", "examples", "dashboard", "Walmart_Sales.db" + ), + ] + db_absolute_path = next( + (p for p in candidate_paths if os.path.isfile(p)), None ) + if db_absolute_path is None: + logger.info( + f"Skipping default data source '%s': file not found in any " + f"{db_name} at {candidate_paths}" + ) + else: + dao.add_file_db( + db_name=db_name, + db_type="sqlite", + db_path=db_absolute_path, + comment="Default Walmart Sales example database", + ) + logger.info( + f"Successfully registered default data source: " + f"{db_name} at {db_absolute_path}" + ) except Exception as e: logger.error(f"Failed to register default data sources: {str(e)}") diff --git a/packages/dbgpt-app/src/dbgpt_app/initialization/skills_provisioning.py b/packages/dbgpt-app/src/dbgpt_app/initialization/skills_provisioning.py new file mode 100644 index 000000000..4444fda11 --- /dev/null +++ b/packages/dbgpt-app/src/dbgpt_app/initialization/skills_provisioning.py @@ -0,0 +1,68 @@ +"""Skills provisioning module for dbgpt-app pip package users. + +Copies builtin skill templates from the installed package to the user's +skills directory on first startup. Existing skills are never overwritten. +""" + +import logging +import os +import shutil + +logger = logging.getLogger(__name__) + + +def ensure_builtin_skills(skills_dir: str) -> None: + """Idempotently seed builtin skills into *skills_dir*. + + On first run after ``pip install dbgpt-app``, the builtin skill + templates bundled inside the wheel (``dbgpt_app/_builtin_skills/``) + are copied to *skills_dir*. Skills that already exist in the + destination are **never** overwritten so that user modifications are + preserved. + + This function is a no-op when: + * ``dbgpt_app._builtin_skills`` cannot be imported (e.g. running + from a source checkout where the force-include hasn't been + triggered). + * The builtin skills directory inside the package is empty. + + Args: + skills_dir: Absolute path to the target skills directory, + e.g. ``~/.dbgpt/skills/``. + """ + try: + import dbgpt_app._builtin_skills as _bs + + builtin_root = os.path.dirname(_bs.__file__) + except (ImportError, AttributeError): + logger.debug("dbgpt_app._builtin_skills not available, skipping seed.") + return + + if not os.path.isdir(builtin_root): + return + + os.makedirs(skills_dir, exist_ok=True) + + for entry in os.listdir(builtin_root): + # Skip Python artifacts and hidden files + if entry.startswith(("_", ".")) or entry == "__pycache__": + continue + + src = os.path.join(builtin_root, entry) + dst = os.path.join(skills_dir, entry) + + # Never overwrite existing skills (user may have modified them) + if os.path.exists(dst): + logger.debug("Builtin skill already exists, skipping: %s", dst) + continue + + if os.path.isdir(src): + shutil.copytree(src, dst) + logger.info("Provisioned builtin skill: %s", entry) + else: + shutil.copy2(src, dst) + logger.info("Provisioned builtin skill file: %s", entry) + + # Ensure user/ subdirectory exists for uploaded/imported skills + user_dir = os.path.join(skills_dir, "user") + os.makedirs(user_dir, exist_ok=True) diff --git a/packages/dbgpt-app/src/dbgpt_app/initialization/workspace_provisioning.py b/packages/dbgpt-app/src/dbgpt_app/initialization/workspace_provisioning.py new file mode 100644 index 000000000..eeaf68f0e --- /dev/null +++ b/packages/dbgpt-app/src/dbgpt_app/initialization/workspace_provisioning.py @@ -0,0 +1,41 @@ +"""Workspace provisioning module for dbgpt-app pip package users. + +Copies pilot template files to the user's workspace directory on first startup. +""" + +import logging +import os +import shutil + +logger = logging.getLogger(__name__) + + +def _ensure_pilot_workspace(dest_root: str) -> None: + """Idempotently copy pilot workspace template files to dest_root. + + This function is safe to call multiple times — it will never overwrite + existing files. On first run, it provisions the full pilot/ directory + structure including alembic config and benchmark data. + + Args: + dest_root (str): The destination root directory (parent of meta_data/). + Example: ~/.dbgpt/workspace/pilot/ + """ + template_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "pilot_template", + ) + for src_dir, dirs, files in os.walk(template_dir): + dirs[:] = [d for d in dirs if d not in ("__pycache__", "versions")] + for filename in files: + if filename in (".DS_Store",) or filename.endswith((".pyc",)): + continue + src_file = os.path.join(src_dir, filename) + rel_path = os.path.relpath(src_file, template_dir) + dest_file = os.path.join(dest_root, rel_path) + if not os.path.exists(dest_file): + os.makedirs(os.path.dirname(dest_file), exist_ok=True) + shutil.copy2(src_file, dest_file) + logger.info("Provisioned: %s", dest_file) + else: + logger.debug("Skipped (exists): %s", dest_file) diff --git a/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py b/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py index 339f2de45..c3895ae71 100644 --- a/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py +++ b/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py @@ -19,7 +19,7 @@ from dbgpt._private.pydantic import BaseModel as _BaseModel from dbgpt.agent.resource.tool.base import tool from dbgpt.agent.skill.manage import get_skill_manager from dbgpt.component import ComponentType -from dbgpt.configs.model_config import resolve_root_path +from dbgpt.configs.model_config import SKILLS_DIR, resolve_root_path from dbgpt.core import PromptTemplate from dbgpt.model.cluster import WorkerManagerFactory from dbgpt_app.openapi.api_view_model import ( @@ -38,7 +38,7 @@ if TYPE_CHECKING: REACT_AGENT_MEMORY_CACHE: Dict[str, "GptsMemory"] = {} -DEFAULT_SKILLS_DIR = resolve_root_path("skills") or "skills" +DEFAULT_SKILLS_DIR = SKILLS_DIR AUTO_DATA_MARKER_PATTERN = re.compile( r"###([A-Z0-9_]+)_START###\s*(.*?)\s*###\1_END###", re.DOTALL ) diff --git a/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/examples_api.py b/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/examples_api.py index 1404292db..3844e7242 100644 --- a/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/examples_api.py +++ b/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/examples_api.py @@ -1,6 +1,19 @@ +"""API endpoints for bundled example files. + +Provides a single ``POST /v1/examples/use`` endpoint that copies a bundled +example file to the user's upload directory so it can be used in +conversations. + +Example files are resolved in the following order: + +1. ``docker/examples/`` under the source-code project root (dev mode). +2. ``dbgpt_app/_builtin_examples/`` inside the installed wheel (PyPI mode). +""" + import logging import os import shutil +from typing import Optional from fastapi import APIRouter, Body, Depends @@ -12,32 +25,78 @@ router = APIRouter() CFG = Config() logger = logging.getLogger(__name__) -# Map of example IDs to their file paths (relative to project root) +# Map of example IDs to their file info. +# - ``source_path``: path relative to source-repo root (``docker/examples/…``). +# - ``builtin_path``: path relative to ``_builtin_examples/`` inside the wheel. +# - ``name``: the user-visible filename. EXAMPLE_FILES = { "walmart_sales": { - "path": "docker/examples/excel/Walmart_Sales.csv", + "source_path": "docker/examples/excel/Walmart_Sales.csv", + "builtin_path": "excel/Walmart_Sales.csv", "name": "Walmart_Sales.csv", }, "csv_visual_report": { - "path": "docker/examples/excel/Walmart_Sales.csv", + "source_path": "docker/examples/excel/Walmart_Sales.csv", + "builtin_path": "excel/Walmart_Sales.csv", "name": "Walmart_Sales.csv", }, "fin_report": { - "path": ( + "source_path": ( "docker/examples/fin_report/pdf/" "2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" ), + "builtin_path": ( + "fin_report/pdf/" + "2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" + ), "name": ( "2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf" ), }, "create_sql_skill": { - "path": "docker/examples/txt/sql_skill.txt", + "source_path": "docker/examples/txt/sql_skill.txt", + "builtin_path": "txt/sql_skill.txt", "name": "sql_skill.txt", }, } +def _resolve_example_source(example: dict) -> Optional[str]: + """Return the absolute path to an example file, or *None* if not found. + + Resolution order: + 1. ``docker/examples/…`` under ``SYSTEM_APP.work_dir`` or cwd (source-code + development mode). + 2. ``_builtin_examples/…`` inside the installed ``dbgpt_app`` package + (PyPI install mode). + """ + # --- 1. Source-code / work_dir mode --- + base_dir = os.getcwd() + if ( + CFG.SYSTEM_APP + and hasattr(CFG.SYSTEM_APP, "work_dir") + and CFG.SYSTEM_APP.work_dir + ): + base_dir = CFG.SYSTEM_APP.work_dir + + candidate = os.path.join(base_dir, example["source_path"]) + if os.path.isfile(candidate): + return candidate + + # --- 2. Builtin examples bundled in the wheel --- + try: + import dbgpt_app._builtin_examples as _be + + builtin_root = os.path.dirname(_be.__file__) + candidate = os.path.join(builtin_root, example["builtin_path"]) + if os.path.isfile(candidate): + return candidate + except (ImportError, AttributeError): + pass + + return None + + @router.post("/v1/examples/use", response_model=Result[str]) async def use_example_file( example_id: str = Body(..., embed=True), @@ -51,7 +110,11 @@ async def use_example_file( example = EXAMPLE_FILES[example_id] user_id = user_token.user_id or "default" - # Determine base directory + source_path = _resolve_example_source(example) + if source_path is None: + return Result.failed(msg=f"Example file not found: {example['name']}") + + # Determine upload base directory (same convention as python_upload_api) base_dir = os.getcwd() if ( CFG.SYSTEM_APP @@ -60,28 +123,6 @@ async def use_example_file( ): base_dir = CFG.SYSTEM_APP.work_dir - # Source file - try base_dir first, then project root - source_path = os.path.join(base_dir, example["path"]) - if not os.path.exists(source_path): - project_root = os.path.dirname( - os.path.dirname( - os.path.dirname( - os.path.dirname( - os.path.dirname( - os.path.dirname( - os.path.dirname(os.path.abspath(__file__)) - ) - ) - ) - ) - ) - ) - source_path = os.path.join(project_root, example["path"]) - - if not os.path.exists(source_path): - return Result.failed(msg=f"Example file not found: {example['name']}") - - # Target directory - same as python_upload_api upload_dir = os.path.join(base_dir, "python_uploads", user_id) os.makedirs(upload_dir, exist_ok=True) diff --git a/packages/dbgpt-app/src/dbgpt_app/tests/test_workspace_provisioning.py b/packages/dbgpt-app/src/dbgpt_app/tests/test_workspace_provisioning.py new file mode 100644 index 000000000..3b724714a --- /dev/null +++ b/packages/dbgpt-app/src/dbgpt_app/tests/test_workspace_provisioning.py @@ -0,0 +1,47 @@ +"""Tests for workspace_provisioning module.""" + +import os + +from dbgpt_app.initialization.workspace_provisioning import _ensure_pilot_workspace + + +def test_ensure_pilot_workspace_copies_alembic_ini(tmp_path): + """Test that alembic.ini is copied to dest_root/meta_data/alembic.ini""" + _ensure_pilot_workspace(str(tmp_path)) + assert os.path.exists(tmp_path / "meta_data" / "alembic.ini") + + +def test_ensure_pilot_workspace_copies_alembic_env_py(tmp_path): + """Test that alembic/env.py is copied""" + _ensure_pilot_workspace(str(tmp_path)) + assert os.path.exists(tmp_path / "meta_data" / "alembic" / "env.py") + + +def test_ensure_pilot_workspace_copies_alembic_script_mako(tmp_path): + """Test that alembic/script.py.mako is copied""" + _ensure_pilot_workspace(str(tmp_path)) + assert os.path.exists(tmp_path / "meta_data" / "alembic" / "script.py.mako") + + +def test_ensure_pilot_workspace_copies_benchmark_xlsx(tmp_path): + """Test that benchmark xlsx is copied""" + _ensure_pilot_workspace(str(tmp_path)) + xlsx_files = list((tmp_path / "benchmark_meta_data").glob("*.xlsx")) + assert len(xlsx_files) == 1 + + +def test_ensure_pilot_workspace_idempotent_no_overwrite(tmp_path): + """Test that calling twice does not overwrite existing files""" + _ensure_pilot_workspace(str(tmp_path)) + ini_path = tmp_path / "meta_data" / "alembic.ini" + # write custom content to simulate user modification + ini_path.write_text("custom content") + _ensure_pilot_workspace(str(tmp_path)) # call again + assert ini_path.read_text() == "custom content" # must not be overwritten + + +def test_ensure_pilot_workspace_creates_missing_directories(tmp_path): + """Test that missing destination directories are created automatically""" + dest = tmp_path / "deep" / "nested" / "pilot" + _ensure_pilot_workspace(str(dest)) + assert os.path.exists(dest / "meta_data" / "alembic.ini") diff --git a/packages/dbgpt-client/pyproject.toml b/packages/dbgpt-client/pyproject.toml index 62ee940ee..4dacb4bc0 100644 --- a/packages/dbgpt-client/pyproject.toml +++ b/packages/dbgpt-client/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-client" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "Add your description here" authors = [ { name = "csunny", email = "cfqcsunny@gmail.com" } diff --git a/packages/dbgpt-client/src/dbgpt_client/_version.py b/packages/dbgpt-client/src/dbgpt_client/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-client/src/dbgpt_client/_version.py +++ b/packages/dbgpt-client/src/dbgpt_client/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/packages/dbgpt-core/pyproject.toml b/packages/dbgpt-core/pyproject.toml index 31b79b230..f9b8c1316 100644 --- a/packages/dbgpt-core/pyproject.toml +++ b/packages/dbgpt-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt" -version = "0.8.0rc1" +version = "0.8.0rc6" description = """DB-GPT is an experimental open-source project that uses localized GPT \ large models to interact with your data and environment. With this solution, you can be\ assured that there is no risk of data leakage, and your data is 100% private and secure.\ diff --git a/packages/dbgpt-core/src/dbgpt/_version.py b/packages/dbgpt-core/src/dbgpt/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-core/src/dbgpt/_version.py +++ b/packages/dbgpt-core/src/dbgpt/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/packages/dbgpt-core/src/dbgpt/agent/skill/manage.py b/packages/dbgpt-core/src/dbgpt/agent/skill/manage.py index 76c106b96..6e6f44f3a 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/skill/manage.py +++ b/packages/dbgpt-core/src/dbgpt/agent/skill/manage.py @@ -523,14 +523,9 @@ class SkillManager(BaseComponent): if hasattr(metadata, "path"): return metadata.path - skills_dir = os.environ.get("DBGPT_SKILLS_DIR") - if not skills_dir: - from dbgpt.configs.model_config import resolve_root_path + from dbgpt.configs.model_config import SKILLS_DIR - skills_dir = resolve_root_path("skills") - - if not skills_dir: - skills_dir = "skills" + skills_dir = SKILLS_DIR # Search candidate subdirectories: direct, user/, claude/, project/, etc. subdirs = ["", "user", "claude", "project"] diff --git a/packages/dbgpt-core/src/dbgpt/cli/_config.py b/packages/dbgpt-core/src/dbgpt/cli/_config.py new file mode 100644 index 000000000..97da19a86 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/_config.py @@ -0,0 +1,322 @@ +"""User-level configuration management for DB-GPT CLI. + +Manages ``~/.dbgpt/configs/.toml`` — one flat TOML file per +profile — and a small ``~/.dbgpt/config.toml`` that records which profile +is active by default. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Optional + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +_DBGPT_HOME = Path(os.environ.get("DBGPT_HOME", str(Path.home() / ".dbgpt"))) +_CONFIGS_DIR = _DBGPT_HOME / "configs" +_ACTIVE_CONFIG = _DBGPT_HOME / "config.toml" + + +def dbgpt_home() -> Path: + """Return ``~/.dbgpt``, creating it if necessary.""" + _DBGPT_HOME.mkdir(parents=True, exist_ok=True) + return _DBGPT_HOME + + +def configs_dir() -> Path: + """Return ``~/.dbgpt/configs/``, creating it if necessary.""" + _CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + return _CONFIGS_DIR + + +def profile_config_path(profile_name: str) -> Path: + """Return the path for a profile TOML, e.g. ``~/.dbgpt/configs/openai.toml``.""" + return configs_dir() / f"{profile_name}.toml" + + +def active_config_path() -> Path: + """Return ``~/.dbgpt/config.toml``.""" + return _ACTIVE_CONFIG + + +# --------------------------------------------------------------------------- +# Active-profile record +# --------------------------------------------------------------------------- + + +def read_active_profile() -> Optional[str]: + """Return the name of the active profile from ``~/.dbgpt/config.toml``. + + Returns: + Optional[str]: Profile name, or *None* if not yet configured. + """ + path = active_config_path() + if not path.exists(): + return None + try: + import tomlkit # type: ignore[import] + + data = tomlkit.loads(path.read_text(encoding="utf-8")) + return data.get("default", {}).get("profile") or None + except Exception: + return None + + +def write_active_profile(profile_name: str) -> None: + """Persist the active profile name to ``~/.dbgpt/config.toml``. + + Args: + profile_name (str): The profile to activate. + """ + import tomlkit # type: ignore[import] + + path = active_config_path() + dbgpt_home() # ensure directory exists + + if path.exists(): + try: + data = tomlkit.loads(path.read_text(encoding="utf-8")) + except Exception: + data = tomlkit.document() + else: + data = tomlkit.document() + + if "default" not in data: + data["default"] = tomlkit.table() + data["default"]["profile"] = profile_name # type: ignore[index] + path.write_text(tomlkit.dumps(data), encoding="utf-8") + + +# --------------------------------------------------------------------------- +# Profile TOML generation +# --------------------------------------------------------------------------- + + +def _escape_toml_string(value: str) -> str: + """Escape backslashes and double quotes for TOML basic strings.""" + return value.replace("\\", "\\\\").replace('"', '\\"') + + +def _render_profile_toml( + spec: "ProfileSpec", # noqa: F821 + api_key: Optional[str], + llm_model: Optional[str] = None, + embedding_model: Optional[str] = None, + api_base: Optional[str] = None, + embedding_api_key: Optional[str] = None, +) -> str: + """Render a complete TOML config for the given profile. + + The generated file uses a literal API key when *api_key* is provided; + otherwise it uses the ``${env:VAR}`` interpolation syntax so the server + reads the key from the environment at runtime. + + Args: + spec: A :class:`~dbgpt.cli._profiles.ProfileSpec` instance. + api_key (Optional[str]): Literal API key value, or *None*. + llm_model (Optional[str]): Override for the LLM model name. If *None*, + uses ``spec.llm_model``. + embedding_model (Optional[str]): Override for the embedding model name. + If *None*, uses ``spec.embedding_model``. + api_base (Optional[str]): Override for the LLM API base URL. If *None*, + uses ``spec.llm_api_base``. + embedding_api_key (Optional[str]): Literal embedding API key, or + *None*. When *None* and ``spec.embedding_env_var`` is set (and + differs from ``spec.env_var``), the generated TOML will reference + that separate env var instead of the LLM key. + + Returns: + str: TOML content as a string. + """ + from dbgpt.cli._profiles import ProfileSpec # noqa: F401 (type-only) + + if api_key: + api_key_value = _escape_toml_string(api_key) + elif spec.env_var: + api_key_value = f"${{env:{spec.env_var}:-sk-xxx}}" + else: + api_key_value = "" + + emb_env_var = spec.embedding_env_var or spec.env_var + if embedding_api_key: + emb_key_value = _escape_toml_string(embedding_api_key) + elif api_key and not spec.embedding_env_var: + emb_key_value = _escape_toml_string(api_key) + elif emb_env_var: + emb_key_value = f"${{env:{emb_env_var}:-sk-xxx}}" + else: + emb_key_value = "" + + if api_base: + embedding_api_url = f"{api_base.rstrip('/')}/embeddings" + else: + embedding_api_url = spec.embedding_api_url + + data_dir = str(dbgpt_home() / "workspace" / "pilot" / "meta_data" / "dbgpt.db") + vector_dir = str(dbgpt_home() / "workspace" / "pilot" / "data") + + lines = [ + f"# DB-GPT configuration — profile: {spec.name}", + "# Generated by `dbgpt setup`", + "", + "[system]", + "# Load language from environment variable(It is set by the hook)", + 'language = "${env:DBGPT_LANG:-en}"', + "api_keys = []", + 'encrypt_key = "your_secret_key"', + "", + "# Server Configurations", + "[service.web]", + 'host = "0.0.0.0"', + "port = 5670", + "", + "[service.web.database]", + 'type = "sqlite"', + f'path = "{data_dir}"', + "", + "[rag.storage]", + "[rag.storage.vector]", + 'type = "chroma"', + f'persist_path = "{vector_dir}"', + "", + "# Model Configurations", + "[models]", + "[[models.llms]]", + ] + + use_env = getattr(spec, "use_env_interpolation", False) and not api_key + + if use_env: + lines.append(f'name = "${{env:LLM_MODEL_NAME:-{spec.llm_model}}}"') + lines.append(f'provider = "${{env:LLM_MODEL_PROVIDER:-{spec.llm_provider}}}"') + effective_api_base = api_base or spec.llm_api_base + if effective_api_base: + lines.append(f'api_base = "${{env:OPENAI_API_BASE:-{effective_api_base}}}"') + lines.append(f'api_key = "{api_key_value}"') + lines.append("") + lines.append("[[models.embeddings]]") + lines.append(f'name = "${{env:EMBEDDING_MODEL_NAME:-{spec.embedding_model}}}"') + lines.append( + f'provider = "${{env:EMBEDDING_MODEL_PROVIDER:-{spec.embedding_provider}}}"' + ) + lines.append( + f'api_url = "${{env:EMBEDDING_MODEL_API_URL:-{embedding_api_url}}}"' + ) + lines.append(f'api_key = "{emb_key_value}"') + else: + lines.append(f'name = "{llm_model or spec.llm_model}"') + lines.append(f'provider = "{spec.llm_provider}"') + + effective_api_base = api_base or spec.llm_api_base + if effective_api_base: + lines.append(f'api_base = "{effective_api_base}"') + + lines.append(f'api_key = "{api_key_value}"') + lines.append("") + lines.append("[[models.embeddings]]") + lines.append(f'name = "{embedding_model or spec.embedding_model}"') + lines.append(f'provider = "{spec.embedding_provider}"') + lines.append(f'api_url = "{embedding_api_url}"') + lines.append(f'api_key = "{emb_key_value}"') + + # Append any provider-specific extras + for extra in spec.extra_toml_lines: + lines.append(extra) + + return "\n".join(lines) + "\n" + + +# --------------------------------------------------------------------------- +# Public write API +# --------------------------------------------------------------------------- + + +def write_profile_config( + profile_name: str, + api_key: Optional[str] = None, + activate: bool = True, + llm_model: Optional[str] = None, + embedding_model: Optional[str] = None, + api_base: Optional[str] = None, + embedding_api_key: Optional[str] = None, +) -> Path: + """Write (or overwrite) the TOML config for *profile_name*. + + Args: + profile_name (str): One of the supported profile names. + api_key (Optional[str]): Literal API key. If *None*, env-var + interpolation is used instead. + activate (bool): Also update ``~/.dbgpt/config.toml`` to make this + profile the active default. + llm_model (Optional[str]): Override LLM model name. Uses spec default + if *None*. + embedding_model (Optional[str]): Override embedding model name. Uses + spec default if *None*. + api_base (Optional[str]): Override LLM API base URL. Uses spec default + if *None*. + embedding_api_key (Optional[str]): Literal embedding API key. When + *None* and the profile has a separate ``embedding_env_var``, the + generated TOML will reference that env var. + + Returns: + Path: Path to the written config file. + """ + from dbgpt.cli._profiles import get_profile + + spec = get_profile(profile_name) + content = _render_profile_toml( + spec, + api_key, + llm_model=llm_model, + embedding_model=embedding_model, + api_base=api_base, + embedding_api_key=embedding_api_key, + ) + path = profile_config_path(profile_name) + path.write_text(content, encoding="utf-8") + + if activate: + write_active_profile(profile_name) + + return path + + +def resolve_config_path( + profile: Optional[str] = None, + config: Optional[str] = None, +) -> Optional[str]: + """Resolve which config file to use, in priority order. + + Priority: + 1. Explicit ``--config`` flag → use as-is. + 2. Explicit ``--profile`` flag → look up ``~/.dbgpt/configs/.toml``. + 3. Active profile from ``~/.dbgpt/config.toml``. + 4. Return *None* (caller should run the setup wizard). + + Args: + profile (Optional[str]): Value of ``--profile`` CLI flag. + config (Optional[str]): Value of ``--config`` CLI flag. + + Returns: + Optional[str]: Absolute path to the config file, or *None*. + """ + if config: + return config + + if profile: + path = profile_config_path(profile) + if path.exists(): + return str(path) + return None # profile specified but not yet configured + + # Fall back to whatever is active + active = read_active_profile() + if active: + path = profile_config_path(active) + if path.exists(): + return str(path) + + return None diff --git a/packages/dbgpt-core/src/dbgpt/cli/_profile_cmd.py b/packages/dbgpt-core/src/dbgpt/cli/_profile_cmd.py new file mode 100644 index 000000000..aa4014183 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/_profile_cmd.py @@ -0,0 +1,95 @@ +"""Profile management subcommands: list, show, create, switch, delete.""" + +import click + + +@click.group() +def profile(): + """Manage DB-GPT configuration profiles.""" + pass + + +@profile.command(name="list") +def profile_list(): + """List all configured profiles.""" + from dbgpt.cli._config import configs_dir, read_active_profile + + configs = configs_dir() + active = read_active_profile() + toml_files = sorted(configs.glob("*.toml")) + + if not toml_files: + click.echo("No profiles configured. Run: dbgpt setup") + return + + for f in toml_files: + name = f.stem + marker = "* " if name == active else " " + click.echo(f"{marker}{name}") + + +@profile.command() +@click.argument("name") +def show(name): + """Show the TOML configuration for a profile.""" + from dbgpt.cli._config import profile_config_path + + path = profile_config_path(name) + if not path.exists(): + raise click.ClickException( + f"Profile '{name}' not found. Run: dbgpt profile create {name}" + ) + click.echo(path.read_text(encoding="utf-8")) + + +@profile.command() +@click.argument("name") +def create(name): + """Create or reconfigure a profile (runs the setup wizard).""" + from dbgpt.cli._wizard import run_setup_wizard + + run_setup_wizard(pre_selected_profile=name) + + +@profile.command() +@click.argument("name") +def switch(name): + """Set a profile as the active default.""" + from dbgpt.cli._config import profile_config_path, write_active_profile + + path = profile_config_path(name) + if not path.exists(): + click.echo( + f"Error: Profile '{name}' not found. Run: dbgpt profile create {name}", + err=False, + ) + raise SystemExit(1) + write_active_profile(name) + click.echo(f"Switched active profile to: {name}") + + +@profile.command() +@click.argument("name") +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.") +def delete(name, yes): + """Delete a profile configuration file.""" + from dbgpt.cli._config import ( + profile_config_path, + read_active_profile, + write_active_profile, + ) + + path = profile_config_path(name) + if not path.exists(): + raise click.ClickException(f"Profile '{name}' not found.") + + if not yes: + click.confirm(f"Delete profile '{name}'?", abort=True) + + path.unlink() + + # Clear active pointer if this was the active profile + if read_active_profile() == name: + write_active_profile("") # clear by writing empty string + + click.echo(f"Deleted profile: {name}") diff --git a/packages/dbgpt-core/src/dbgpt/cli/_profiles.py b/packages/dbgpt-core/src/dbgpt/cli/_profiles.py new file mode 100644 index 000000000..3646d802b --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/_profiles.py @@ -0,0 +1,203 @@ +"""Profile definitions and API key resolution for DB-GPT CLI. + +Each profile corresponds to a supported LLM provider and contains the +information needed to generate a TOML configuration file and resolve +API credentials from environment variables. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Dict, List, Optional + + +@dataclass +class ProfileSpec: + """Specification for a single LLM provider profile.""" + + name: str + """Internal identifier, e.g. 'openai'.""" + + label: str + """Human-readable display name, e.g. 'OpenAI (GPT-4o)'.""" + + env_var: str + """Primary environment variable that holds the API key.""" + + llm_model: str + """Default LLM model name.""" + + llm_provider: str + """DB-GPT provider string, e.g. 'proxy/openai'.""" + + llm_api_base: str + """Base URL for the LLM API.""" + + embedding_model: str + """Default embedding model name.""" + + embedding_provider: str + """DB-GPT provider string for embeddings.""" + + embedding_api_url: str + """URL for the embedding API endpoint.""" + + needs_api_key: bool = True + """Whether this profile requires an API key.""" + + use_env_interpolation: bool = False + """When True, model fields use ``${env:VAR:-default}`` syntax in generated TOML. + + Set to True only for the *default* profile so that ``default.toml`` mirrors + ``configs/dbgpt-proxy-openai.toml`` exactly — allowing runtime override via + environment variables without re-running the setup wizard. + """ + + extra_toml_lines: List[str] = field(default_factory=list) + """Extra TOML lines appended verbatim to the generated config.""" + + embedding_env_var: Optional[str] = None + """Environment variable for the embedding API key. + + When *None* (the default), the same ``env_var`` used for the LLM key is + reused for embeddings. Set this to a different name when the embedding + endpoint requires a separate API key (e.g. Kimi uses MOONSHOT_API_KEY for + LLM but DASHSCOPE_API_KEY for embeddings via DashScope/Tongyi). + """ + + def env_key(self) -> Optional[str]: + """Return the API key from the environment, or None.""" + return os.environ.get(self.env_var) + + def embedding_env_key(self) -> Optional[str]: + """Return the embedding API key from the environment, or None. + + Falls back to the primary ``env_var`` when ``embedding_env_var`` is + not set. + """ + var = self.embedding_env_var or self.env_var + return os.environ.get(var) if var else None + + +# --------------------------------------------------------------------------- +# Supported profiles +# --------------------------------------------------------------------------- + +PROFILES: Dict[str, ProfileSpec] = { + "openai": ProfileSpec( + name="openai", + label="OpenAI (OpenAI or OpenAI API proxy)", + env_var="OPENAI_API_KEY", + llm_model="gpt-4o", + llm_provider="proxy/openai", + llm_api_base="https://api.openai.com/v1", + embedding_model="text-embedding-3-small", + embedding_provider="proxy/openai", + embedding_api_url="https://api.openai.com/v1/embeddings", + ), + "kimi": ProfileSpec( + name="kimi", + label="Kimi (Moonshot AI / kimi-k2)", + env_var="MOONSHOT_API_KEY", + llm_model="kimi-k2", + llm_provider="proxy/moonshot", + llm_api_base="https://api.moonshot.cn/v1", + embedding_model="text-embedding-v3", + embedding_provider="proxy/tongyi", + embedding_api_url="https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings", + embedding_env_var="DASHSCOPE_API_KEY", + ), + "qwen": ProfileSpec( + name="qwen", + label="Qwen (DashScope API)", + env_var="DASHSCOPE_API_KEY", + llm_model="qwen-plus", + llm_provider="proxy/tongyi", + llm_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", + embedding_model="text-embedding-v3", + embedding_provider="proxy/tongyi", + embedding_api_url="https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings", + ), + "minimax": ProfileSpec( + name="minimax", + label="MiniMax (abab series)", + env_var="MINIMAX_API_KEY", + llm_model="abab6.5s-chat", + llm_provider="proxy/openai", + llm_api_base="https://api.minimax.chat/v1", + embedding_model="embo-01", + embedding_provider="proxy/openai", + embedding_api_url="https://api.minimax.chat/v1/embeddings", + ), + "glm": ProfileSpec( + name="glm", + label="z.ai (zhipu.ai API)", + env_var="ZHIPUAI_API_KEY", + llm_model="glm-4-plus", + llm_provider="proxy/zhipu", + llm_api_base="https://open.bigmodel.cn/api/paas/v4", + embedding_model="embedding-3", + embedding_provider="proxy/zhipu", + embedding_api_url="https://open.bigmodel.cn/api/paas/v4/embeddings", + ), + "custom": ProfileSpec( + name="custom", + label="Custom Provider (Any OpenAI compatible endpoint)", + env_var="OPENAI_API_KEY", + llm_model="gpt-4o", + llm_provider="proxy/openai", + llm_api_base="https://api.openai.com/v1", + embedding_model="text-embedding-3-small", + embedding_provider="proxy/openai", + embedding_api_url="https://api.openai.com/v1/embeddings", + ), + "default": ProfileSpec( + name="default", + label="Skip for now (use OpenAI defaults)", + env_var="OPENAI_API_KEY", + llm_model="gpt-4o", + llm_provider="proxy/openai", + llm_api_base="https://api.openai.com/v1", + embedding_model="text-embedding-3-small", + embedding_provider="proxy/openai", + embedding_api_url="https://api.openai.com/v1/embeddings", + needs_api_key=False, + use_env_interpolation=True, + ), +} + +# Ordered list for display in the wizard +PROFILE_ORDER: List[str] = [ + "openai", + "kimi", + "qwen", + "minimax", + "glm", + "custom", + "default", +] + + +def get_profile(name: str) -> ProfileSpec: + """Return a ProfileSpec by name. + + Args: + name (str): Profile identifier (case-insensitive). + + Returns: + ProfileSpec: The matching profile spec. + + Raises: + ValueError: If the profile name is not recognised. + """ + key = name.lower() + if key not in PROFILES: + valid = ", ".join(PROFILE_ORDER) + raise ValueError(f"Unknown profile '{name}'. Valid profiles: {valid}") + return PROFILES[key] + + +def list_profiles() -> List[ProfileSpec]: + """Return profiles in canonical display order.""" + return [PROFILES[k] for k in PROFILE_ORDER] diff --git a/packages/dbgpt-core/src/dbgpt/cli/_wizard.py b/packages/dbgpt-core/src/dbgpt/cli/_wizard.py new file mode 100644 index 000000000..4888be68c --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/_wizard.py @@ -0,0 +1,325 @@ +"""First-run setup wizard for DB-GPT CLI. + +Provides :func:`run_setup_wizard` (interactive) and +:func:`run_setup_noninteractive` (``--yes`` / CI mode). + +Interactive flow:: + + Welcome to DB-GPT! 🎉 + + Which LLM provider would you like to use? + + ● OpenAI OpenAI or OpenAI API proxy + ○ Kimi Moonshot AI + ○ Qwen DashScope API + ○ MiniMax abab series + ○ Z.AI zhipu.ai API + ○ Custom Any OpenAI compatible endpoint + ○ Skip for now Use OpenAI defaults + + Enter your OPENAI_API_KEY: **** + ✔ Config saved → ~/.dbgpt/configs/openai.toml + +For profiles with a separate embedding key (e.g. Kimi uses MOONSHOT_API_KEY +for LLM but DASHSCOPE_API_KEY for embeddings), the wizard will prompt for +both keys. +""" + +from __future__ import annotations + +import os +from typing import Optional + +from dbgpt.cli._config import ( + resolve_config_path, + write_profile_config, +) +from dbgpt.cli._profiles import ProfileSpec, get_profile, list_profiles +from dbgpt.util.console.console import CliLogger + +_log = CliLogger() + +# --------------------------------------------------------------------------- +# Provider display metadata (description only) +# --------------------------------------------------------------------------- + +_PROVIDER_META = { + "openai": "OpenAI or OpenAI API proxy", + "kimi": "Moonshot AI", + "qwen": "DashScope API", + "minimax": "MiniMax API", + "glm": "zhipu.ai API", + "custom": "Any OpenAI compatible endpoint", + "default": "Use OpenAI defaults", +} + +_DISPLAY_NAMES = { + "openai": "OpenAI", + "kimi": "Kimi", + "qwen": "Qwen", + "minimax": "MiniMax", + "glm": "Z.AI", + "custom": "Custom", + "default": "Skip for now", +} + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def run_setup_wizard( + pre_selected_profile: Optional[str] = None, + pre_set_key: Optional[str] = None, +) -> str: + """Run the interactive first-time setup wizard. + + Args: + pre_selected_profile (Optional[str]): If supplied (e.g. via + ``--profile``), skip the provider selection step. + pre_set_key (Optional[str]): If supplied (e.g. via ``--api-key``), + skip the API-key prompt. + + Returns: + str: Absolute path to the written config file. + """ + _print_welcome() + + # ── step 1: choose profile ────────────────────────────────────────────── + if pre_selected_profile: + try: + spec = get_profile(pre_selected_profile) + except ValueError as exc: + _log.error(str(exc), exit_code=1) + return "" + else: + spec = _ask_profile() + + # ── step 2: API key ───────────────────────────────────────────────────── + api_key: Optional[str] = None + embedding_api_key: Optional[str] = None + if spec.needs_api_key: + api_key = _ask_api_key(spec, pre_set_key) + if spec.embedding_env_var and spec.embedding_env_var != spec.env_var: + embedding_api_key = _ask_embedding_api_key(spec) + + # ── step 2.5: api_base (openai + custom ask; others use spec default) ─── + api_base: Optional[str] = None + if spec.name in ("openai", "custom"): + api_base = _ask_api_base(spec) + + # ── step 3: model names (default profile skips this) ──────────────────── + llm_model: Optional[str] = None + embedding_model: Optional[str] = None + if spec.name != "default": + llm_model, embedding_model = _ask_model_names(spec) + + # ── step 4: write config ───────────────────────────────────────────────── + config_path = write_profile_config( + spec.name, + api_key=api_key, + activate=True, + llm_model=llm_model, + embedding_model=embedding_model, + api_base=api_base, + embedding_api_key=embedding_api_key, + ) + _log.success(f"✔ Config saved → {config_path}") + return str(config_path) + + +def run_setup_noninteractive( + profile_name: str = "openai", + api_key: Optional[str] = None, +) -> str: + """Create a config without prompting (``--yes`` / CI mode). + + Uses the literal *api_key* if provided; otherwise falls back to the + environment variable defined in the profile spec. + + Args: + profile_name (str): Profile to configure. + api_key (Optional[str]): Explicit API key; *None* means use env var. + + Returns: + str: Absolute path to the written config file. + """ + spec = get_profile(profile_name) + + # If no explicit key, try to read from environment + resolved_key = api_key or (spec.env_key() if spec.needs_api_key else None) + + config_path = write_profile_config(spec.name, api_key=resolved_key, activate=True) + _log.success(f"✔ Config saved → {config_path}") + return str(config_path) + + +def maybe_run_wizard( + profile: Optional[str], + config: Optional[str], + yes: bool, + api_key: Optional[str], +) -> str: + """Decide whether to run the wizard and return a ready config path. + + This is the single call-site used by ``start webserver`` to obtain a + config path, handling all first-run and re-configuration scenarios. + + Priority: + 1. ``--config`` path supplied → use it directly (skip wizard). + 2. Config already exists (via ``--profile`` or active default) → reuse. + 3. ``--yes`` → non-interactive setup. + 4. Interactive wizard. + + Args: + profile (Optional[str]): ``--profile`` CLI flag value. + config (Optional[str]): ``--config`` CLI flag value. + yes (bool): ``--yes`` / ``-y`` flag. + api_key (Optional[str]): ``--api-key`` flag value. + + Returns: + str: Absolute path to a usable config file. + """ + # 1. Explicit --config + if config: + return config + + # 2. Existing profile config + existing = resolve_config_path(profile=profile, config=None) + if existing: + return existing + + # 3. Non-interactive + if yes: + return run_setup_noninteractive( + profile_name=profile or "openai", + api_key=api_key, + ) + + # 4. Interactive wizard + return run_setup_wizard( + pre_selected_profile=profile, + pre_set_key=api_key, + ) + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _print_welcome() -> None: + _log.print("") + _log.print("[bold bright_blue]Welcome to DB-GPT! 🎉[/bold bright_blue]") + _log.print("") + _log.info( + "Let's set up your configuration. This only takes a moment.\n" + "You can re-run [bold]dbgpt setup[/bold] at any time to change settings." + ) + _log.print("") + + +def _build_provider_option(spec: ProfileSpec) -> tuple[str, str]: + """Return (display_name, description) for a provider.""" + display_name = _DISPLAY_NAMES.get(spec.name, spec.name.capitalize()) + desc = _PROVIDER_META.get(spec.name, spec.label) + return (display_name, desc) + + +def _ask_profile() -> ProfileSpec: + """Prompt the user to choose a provider with an arrow-key selector.""" + profiles = list_profiles() + + options = [_build_provider_option(spec) for spec in profiles] + + idx = _log.select("Which LLM provider would you like to use?", options) + return profiles[idx] + + +def _ask_api_key(spec: ProfileSpec, pre_set_key: Optional[str]) -> Optional[str]: + """Ask for an API key, honouring a pre-set value or env var. + + Returns *None* for providers that don't need a key (Ollama). + Returns an empty string if the user explicitly skips. + """ + if not spec.needs_api_key: + return None + + # If an explicit key was passed in (e.g. via --api-key flag), use it. + if pre_set_key: + _log.success(f"✔ Using supplied API key for {spec.label}") + return pre_set_key + + # Check environment + env_val = spec.env_key() + if env_val: + _log.success(f"✔ Found [bold]{spec.env_var}[/bold] in environment — using it.") + return env_val + + # Prompt the user + _log.print( + f"\nEnter your [bold]{spec.env_var}[/bold] " + f"(or press Enter to use env-var at runtime):" + ) + + import getpass + + try: + entered = getpass.getpass(prompt=" API key: ").strip() + except (EOFError, KeyboardInterrupt): + _log.warning("\nSkipping API key — you can set it via the environment later.") + return None + + if not entered: + _log.warning( + f"No key entered. The config will reference ${spec.env_var} at runtime." + ) + return None + + return entered + + +def _ask_model_names(spec: ProfileSpec) -> tuple[str, str]: + llm = _log.ask("LLM model name", default=spec.llm_model) + emb = _log.ask("Embedding model name", default=spec.embedding_model) + return (llm, emb) + + +def _ask_api_base(spec: ProfileSpec) -> str: + return _log.ask("API base URL", default=spec.llm_api_base) + + +def _ask_embedding_api_key(spec: ProfileSpec) -> Optional[str]: + emb_env_var = spec.embedding_env_var or spec.env_var + + env_val = os.environ.get(emb_env_var) if emb_env_var else None + if env_val: + _log.success( + f"✔ Found [bold]{emb_env_var}[/bold] in environment" + " — using it for embeddings." + ) + return env_val + + _log.print( + f"\nEnter your [bold]{emb_env_var}[/bold] for embeddings " + f"(or press Enter to use env-var at runtime):" + ) + + import getpass + + try: + entered = getpass.getpass(prompt=" Embedding API key: ").strip() + except (EOFError, KeyboardInterrupt): + _log.warning( + "\nSkipping embedding API key — you can set it via the environment later." + ) + return None + + if not entered: + _log.warning( + f"No key entered. The config will reference ${emb_env_var} at runtime." + ) + return None + + return entered diff --git a/packages/dbgpt-core/src/dbgpt/cli/cli_scripts.py b/packages/dbgpt-core/src/dbgpt/cli/cli_scripts.py index d66e9f551..750945135 100644 --- a/packages/dbgpt-core/src/dbgpt/cli/cli_scripts.py +++ b/packages/dbgpt-core/src/dbgpt/cli/cli_scripts.py @@ -34,10 +34,17 @@ def add_command_alias(command, name: str, hidden: bool = False, parent_group=Non parent_group.add_command(new_command, name=name) -@click.group() -def start(): +@click.group(invoke_without_command=True) +@click.pass_context +def start(ctx): """Start specific server.""" - pass + if ctx.invoked_subcommand is None: + # Try web first, then webserver as fallback + cmd = start.commands.get("web") or start.commands.get("webserver") + if cmd: + ctx.invoke(cmd) + else: + click.echo(ctx.get_help()) @click.group() @@ -93,6 +100,99 @@ def tool(): """DB-GPT Tools.""" +# --------------------------------------------------------------------------- +# dbgpt setup +# --------------------------------------------------------------------------- + + +@click.command(name="setup") +@click.option( + "-p", + "--profile", + type=str, + required=False, + default=None, + help=( + "Provider profile to configure: openai / kimi / qwen / minimax / " + "deepseek / ollama. If omitted, an interactive menu is shown." + ), +) +@click.option( + "-y", + "--yes", + is_flag=True, + default=False, + help="Non-interactive: skip wizard and use defaults / env variables.", +) +@click.option( + "--api-key", + type=str, + required=False, + default=None, + envvar="DBGPT_API_KEY", + help="API key for the chosen provider.", +) +@click.option( + "--show", + is_flag=True, + default=False, + help="Show the current active profile and config path, then exit.", +) +def setup_command(profile: str, yes: bool, api_key: str, show: bool): + """Configure DB-GPT's LLM provider and write ~/.dbgpt/configs/.toml. + + Run without arguments for an interactive wizard, or use --yes for + non-interactive / CI usage. + + \b + Examples: + dbgpt setup # interactive wizard + dbgpt setup --profile openai --yes # use OPENAI_API_KEY from env + dbgpt setup --profile kimi --api-key sk-xxx + dbgpt setup --show # print current config + """ + try: + from dbgpt.cli._config import ( + profile_config_path, + read_active_profile, + ) + from dbgpt.cli._wizard import run_setup_noninteractive, run_setup_wizard + from dbgpt.util.console.console import CliLogger + + cl = CliLogger() + + if show: + active = read_active_profile() + if active: + path = profile_config_path(active) + cl.info(f"Active profile : [bold]{active}[/bold]") + cl.info(f"Config file : {path}") + if not path.exists(): + cl.warning(" ⚠ Config file does not exist yet. Run `dbgpt setup`.") + else: + cl.warning("No profile configured yet. Run `dbgpt setup`.") + return + + if yes: + run_setup_noninteractive( + profile_name=profile or "openai", + api_key=api_key, + ) + else: + run_setup_wizard( + pre_selected_profile=profile, + pre_set_key=api_key, + ) + + except ImportError as e: + logger.warning(f"Setup wizard unavailable: {e}") + raise click.ClickException(str(e)) + + +# --------------------------------------------------------------------------- +# Stop all +# --------------------------------------------------------------------------- + stop_all_func_list = [] @@ -103,6 +203,17 @@ def stop_all(): stop_func() +@click.command(name="none") +def start_none(): + """Start DB-GPT in API-only mode (no web UI). [Planned]""" + click.echo( + "API-only mode (no web UI) is planned for a future release.\n" + "For now, use: dbgpt start web" + ) + + +start.add_command(start_none) + cli.add_command(start) cli.add_command(stop) # cli.add_command(install) @@ -113,6 +224,10 @@ cli.add_command(repo) cli.add_command(run) cli.add_command(net) cli.add_command(tool) +cli.add_command(setup_command) +from dbgpt.cli._profile_cmd import profile as profile_cmd # noqa: E402 + +cli.add_command(profile_cmd, name="profile") add_command_alias(stop_all, name="all", parent_group=stop) try: @@ -149,6 +264,7 @@ try: ) add_command_alias(start_webserver, name="webserver", parent_group=start) + add_command_alias(start_webserver, name="web", parent_group=start) add_command_alias(stop_webserver, name="webserver", parent_group=stop) # Add migration command add_command_alias(migration, name="migration", parent_group=db) diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/__init__.py b/packages/dbgpt-core/src/dbgpt/cli/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/conftest.py b/packages/dbgpt-core/src/dbgpt/cli/tests/conftest.py new file mode 100644 index 000000000..56164fc62 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/conftest.py @@ -0,0 +1,24 @@ +"""Shared fixtures for CLI tests.""" + +import click.testing +import pytest + + +@pytest.fixture() +def isolated_dbgpt_home(tmp_path, monkeypatch): + """Isolate DBGPT_HOME to a temp directory.""" + home = tmp_path / "dbgpt" + home.mkdir() + monkeypatch.setenv("DBGPT_HOME", str(home)) + import dbgpt.cli._config as _cfg + + monkeypatch.setattr(_cfg, "_DBGPT_HOME", home) + monkeypatch.setattr(_cfg, "_CONFIGS_DIR", home / "configs") + monkeypatch.setattr(_cfg, "_ACTIVE_CONFIG", home / "config.toml") + return home + + +@pytest.fixture() +def cli_runner(): + """Return a Click test runner.""" + return click.testing.CliRunner(mix_stderr=False) diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_cli_scripts.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_cli_scripts.py new file mode 100644 index 000000000..9ebcf832b --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_cli_scripts.py @@ -0,0 +1,65 @@ +"""Tests for cli_scripts.py — start group alias and bare-start behavior.""" + +from unittest.mock import patch + +from click.testing import CliRunner + +from dbgpt.cli.cli_scripts import cli + + +def test_start_web_alias_help_shows_options(): + """dbgpt start web --help exits 0 and shows --config and --profile.""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["start", "web", "--help"]) + assert result.exit_code == 0, result.output + assert "--config" in result.output or "--profile" in result.output + + +def test_start_webserver_still_works(): + """dbgpt start webserver --help exits 0 (regression).""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["start", "webserver", "--help"]) + assert result.exit_code == 0, result.output + + +def test_start_controller_unaffected(): + """dbgpt start controller --help still works after changes.""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["start", "controller", "--help"]) + assert result.exit_code == 0, result.output + + +def test_bare_start_invokes_web_or_shows_help(): + """bare dbgpt start (no subcommand) either invokes web or shows help — does NOT crash.""" # noqa: E501 + runner = CliRunner(mix_stderr=False) + with ( + patch("dbgpt.cli._wizard.maybe_run_wizard", return_value="/fake/config.toml"), + patch("dbgpt_app.dbgpt_server.run_webserver"), + ): + result = runner.invoke(cli, ["start"]) + # Must not return an error exit code from a crash + assert result.exit_code in (0, 1, 2), ( + f"Unexpected exit code: {result.exit_code}\n{result.output}" + ) + assert result.exception is None or "No such command" not in str(result.exception) + + +def test_start_none_exits_zero(cli_runner): + from dbgpt.cli.cli_scripts import cli + + result = cli_runner.invoke(cli, ["start", "none"]) + assert result.exit_code == 0 + + +def test_start_none_output_mentions_planned(cli_runner): + from dbgpt.cli.cli_scripts import cli + + result = cli_runner.invoke(cli, ["start", "none"]) + assert "planned" in result.output.lower() + + +def test_start_none_output_mentions_start_web(cli_runner): + from dbgpt.cli.cli_scripts import cli + + result = cli_runner.invoke(cli, ["start", "none"]) + assert "dbgpt start web" in result.output diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_config.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_config.py new file mode 100644 index 000000000..2c7205572 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_config.py @@ -0,0 +1,381 @@ +"""Tests for _config.py — TOML generation, path handling, etc.""" + +from dbgpt.cli._config import _render_profile_toml +from dbgpt.cli._profiles import get_profile + + +def test_escape_api_key_with_double_quote_generates_valid_toml(isolated_dbgpt_home): + """API key containing double quote should produce valid parseable TOML.""" + import tomlkit + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key='sk-abc"def') + data = tomlkit.loads(content) # should NOT raise + llms = data["models"]["llms"] + assert llms[0]["api_key"] == 'sk-abc"def' + + +def test_escape_api_key_with_backslash_generates_valid_toml(isolated_dbgpt_home): + """API key containing backslash should produce valid parseable TOML.""" + import tomlkit + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="sk-abc\\def") + data = tomlkit.loads(content) # should NOT raise + llms = data["models"]["llms"] + assert llms[0]["api_key"] == "sk-abc\\def" + + +def test_escape_env_var_placeholder_not_escaped(isolated_dbgpt_home): + """Env-var placeholder ${env:VAR:-default} should NOT be double-escaped.""" + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key=None) + assert "${env:OPENAI_API_KEY:-sk-xxx}" in content + assert "\\\\" not in content # no double backslash introduced + + +def test_escape_normal_api_key_unchanged(isolated_dbgpt_home): + """Normal API key (no special chars) should work exactly as before.""" + import tomlkit + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="sk-normal-key-123") + data = tomlkit.loads(content) + llms = data["models"]["llms"] + assert llms[0]["api_key"] == "sk-normal-key-123" + + +class TestDbgptHomeEnvVar: + def test_dbgpt_home_returns_custom_path(self, isolated_dbgpt_home): + from dbgpt.cli._config import dbgpt_home + + result = dbgpt_home() + assert result == isolated_dbgpt_home + + def test_configs_dir_under_custom_home(self, isolated_dbgpt_home): + from dbgpt.cli._config import configs_dir + + result = configs_dir() + assert result == isolated_dbgpt_home / "configs" + + def test_profile_config_path_under_custom_home(self, isolated_dbgpt_home): + from dbgpt.cli._config import profile_config_path + + result = profile_config_path("openai") + assert result == isolated_dbgpt_home / "configs" / "openai.toml" + + def test_active_config_path_under_custom_home(self, isolated_dbgpt_home): + from dbgpt.cli._config import active_config_path + + result = active_config_path() + assert result == isolated_dbgpt_home / "config.toml" + + def test_default_home_is_dotdbgpt(self, monkeypatch): + monkeypatch.delenv("DBGPT_HOME", raising=False) + from pathlib import Path + + import dbgpt.cli._config as _cfg + + expected = Path.home() / ".dbgpt" + monkeypatch.setattr(_cfg, "_DBGPT_HOME", expected) + monkeypatch.setattr(_cfg, "_CONFIGS_DIR", expected / "configs") + monkeypatch.setattr(_cfg, "_ACTIVE_CONFIG", expected / "config.toml") + from dbgpt.cli._config import dbgpt_home + + assert dbgpt_home() == expected + + +class TestWorkspacePaths: + def test_render_toml_data_path_contains_workspace(self, isolated_dbgpt_home): + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="test-key") + assert "workspace/pilot/meta_data/dbgpt.db" in content + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="test-key") + assert "workspace/pilot/data" in content + + def test_render_toml_uses_custom_home_in_data_path(self, isolated_dbgpt_home): + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="test-key") + # 路径中包含 isolated_dbgpt_home 的字符串 + assert str(isolated_dbgpt_home) in content + assert "workspace/pilot/meta_data/dbgpt.db" in content + + +class TestExtendedSignature: + def test_render_toml_with_llm_model_override_uses_override( + self, isolated_dbgpt_home + ): + """llm_model override replaces spec default in generated TOML.""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config( + "openai", api_key="test-key", llm_model="gpt-4-turbo" + ) + data = tomlkit.loads(path.read_text()) + assert data["models"]["llms"][0]["name"] == "gpt-4-turbo" + + def test_render_toml_with_embedding_model_override_uses_override( + self, isolated_dbgpt_home + ): + """embedding_model override replaces spec default in generated TOML.""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config( + "openai", api_key="test-key", embedding_model="ada-002" + ) + data = tomlkit.loads(path.read_text()) + assert data["models"]["embeddings"][0]["name"] == "ada-002" + + def test_render_toml_without_overrides_uses_spec_defaults( + self, isolated_dbgpt_home + ): + """No overrides → TOML uses spec defaults (regression guard).""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config("openai", api_key="test-key") + data = tomlkit.loads(path.read_text()) + assert data["models"]["llms"][0]["name"] == "gpt-4o" + assert data["models"]["embeddings"][0]["name"] == "text-embedding-3-small" + + def test_render_toml_with_api_base_override_uses_override( + self, isolated_dbgpt_home + ): + """api_base override replaces spec default in generated TOML.""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config( + "custom", api_key="test-key", api_base="https://my.api/v1" + ) + data = tomlkit.loads(path.read_text()) + assert data["models"]["llms"][0]["api_base"] == "https://my.api/v1" + + def test_render_toml_custom_api_base_derives_embedding_url( + self, isolated_dbgpt_home + ): + """When api_base is overridden, embedding api_url is derived from it.""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config( + "custom", api_key="test-key", api_base="https://my-proxy.com/v1" + ) + data = tomlkit.loads(path.read_text()) + assert ( + data["models"]["embeddings"][0]["api_url"] + == "https://my-proxy.com/v1/embeddings" + ) + + def test_render_toml_custom_api_base_trailing_slash_stripped( + self, isolated_dbgpt_home + ): + """Trailing slash in api_base should not produce double-slash.""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config( + "custom", api_key="test-key", api_base="https://my-proxy.com/v1/" + ) + data = tomlkit.loads(path.read_text()) + assert ( + data["models"]["embeddings"][0]["api_url"] + == "https://my-proxy.com/v1/embeddings" + ) + + def test_render_toml_without_api_base_uses_spec_embedding_url( + self, isolated_dbgpt_home + ): + """Without api_base override, embedding api_url uses spec default.""" + import tomlkit + + from dbgpt.cli._config import write_profile_config + + path = write_profile_config("openai", api_key="test-key") + data = tomlkit.loads(path.read_text()) + assert ( + data["models"]["embeddings"][0]["api_url"] + == "https://api.openai.com/v1/embeddings" + ) + + +class TestKimiEmbeddingEnvVar: + def test_kimi_embedding_uses_dashscope_env_var(self, isolated_dbgpt_home): + """Kimi profile should reference DASHSCOPE_API_KEY for embeddings.""" + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("kimi") + content = _render_profile_toml(spec, api_key=None) + assert "${env:DASHSCOPE_API_KEY:-sk-xxx}" in content + + def test_kimi_llm_uses_moonshot_env_var(self, isolated_dbgpt_home): + """Kimi profile LLM section should still reference MOONSHOT_API_KEY.""" + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("kimi") + content = _render_profile_toml(spec, api_key=None) + assert "${env:MOONSHOT_API_KEY:-sk-xxx}" in content + + def test_kimi_literal_embedding_key_overrides_env_var(self, isolated_dbgpt_home): + """When embedding_api_key is supplied, use it literally.""" + import tomlkit + + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("kimi") + content = _render_profile_toml(spec, api_key=None, embedding_api_key="ds-key") + data = tomlkit.loads(content) + assert data["models"]["embeddings"][0]["api_key"] == "ds-key" + + def test_kimi_embedding_api_url_is_dashscope(self, isolated_dbgpt_home): + """Kimi embedding api_url should point to DashScope.""" + import tomlkit + + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("kimi") + content = _render_profile_toml(spec, api_key=None) + data = tomlkit.loads(content) + assert data["models"]["embeddings"][0]["api_url"] == ( + "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings" + ) + + def test_openai_same_key_used_for_embeddings(self, isolated_dbgpt_home): + """OpenAI profile uses the same literal key for both LLM and embeddings.""" + import tomlkit + + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="sk-test") + data = tomlkit.loads(content) + assert data["models"]["llms"][0]["api_key"] == "sk-test" + assert data["models"]["embeddings"][0]["api_key"] == "sk-test" + + +class TestDefaultProfileConfig: + """Tests for config generation with the 'default' (formerly skip) profile.""" + + def test_default_profile_generates_env_var_placeholder(self, isolated_dbgpt_home): + """default profile should use env-var placeholder with default.""" + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("default") + content = _render_profile_toml(spec, api_key=None) + assert "${env:OPENAI_API_KEY:-sk-xxx}" in content + # Also verify env-var interpolation for model fields + assert "${env:LLM_MODEL_NAME:-gpt-4o}" in content + assert "${env:LLM_MODEL_PROVIDER:-proxy/openai}" in content + assert "${env:OPENAI_API_BASE:-https://api.openai.com/v1}" in content + assert "${env:EMBEDDING_MODEL_NAME:-text-embedding-3-small}" in content + assert "${env:EMBEDDING_MODEL_PROVIDER:-proxy/openai}" in content + assert ( + "${env:EMBEDDING_MODEL_API_URL:-https://api.openai.com/v1/embeddings}" + in content + ) + + def test_default_profile_generates_valid_toml(self, isolated_dbgpt_home): + """default profile should produce parseable TOML with env-var interpolation.""" + import tomlkit + + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("default") + content = _render_profile_toml(spec, api_key=None) + data = tomlkit.loads(content) + # Model fields use env-var interpolation syntax as raw strings + assert data["models"]["llms"][0]["name"] == "${env:LLM_MODEL_NAME:-gpt-4o}" + assert ( + data["models"]["llms"][0]["provider"] + == "${env:LLM_MODEL_PROVIDER:-proxy/openai}" + ) + assert ( + data["models"]["embeddings"][0]["name"] + == "${env:EMBEDDING_MODEL_NAME:-text-embedding-3-small}" + ) + + def test_default_profile_config_file_named_default(self, isolated_dbgpt_home): + """write_profile_config('default') should create default.toml.""" + from dbgpt.cli._config import profile_config_path, write_profile_config + + path = write_profile_config("default", api_key=None) + assert path == profile_config_path("default") + assert path.name == "default.toml" + assert path.exists() + + def test_default_profile_with_literal_api_key_uses_literal_not_env( + self, isolated_dbgpt_home + ): + """When api_key is provided for default profile, use literal values.""" + import tomlkit + + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("default") + content = _render_profile_toml(spec, api_key="sk-literal") + data = tomlkit.loads(content) + # With a literal api_key, use_env_interpolation should NOT activate + assert data["models"]["llms"][0]["name"] == "gpt-4o" + assert data["models"]["llms"][0]["provider"] == "proxy/openai" + assert data["models"]["llms"][0]["api_key"] == "sk-literal" + # No env-var syntax for model name/provider + assert "${env:LLM_MODEL_NAME" not in content + + def test_openai_profile_no_regression_with_api_key(self, isolated_dbgpt_home): + """openai profile with literal api_key must still use literal model values.""" + import tomlkit + + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key="sk-xxx") + data = tomlkit.loads(content) + assert data["models"]["llms"][0]["name"] == "gpt-4o" + assert data["models"]["llms"][0]["provider"] == "proxy/openai" + assert data["models"]["llms"][0]["api_base"] == "https://api.openai.com/v1" + assert data["models"]["llms"][0]["api_key"] == "sk-xxx" + # No env-var interpolation for openai profile + assert "${env:LLM_MODEL_NAME" not in content + + def test_openai_profile_no_regression_without_api_key(self, isolated_dbgpt_home): + """openai profile with api_key=None uses env-var for key, literal for model.""" + from dbgpt.cli._config import _render_profile_toml + from dbgpt.cli._profiles import get_profile + + spec = get_profile("openai") + content = _render_profile_toml(spec, api_key=None) + # api_key uses env-var syntax with default + assert "${env:OPENAI_API_KEY:-sk-xxx}" in content + # model name/provider/api_base remain literal + assert 'name = "gpt-4o"' in content + assert 'provider = "proxy/openai"' in content + assert 'api_base = "https://api.openai.com/v1"' in content + # No LLM_MODEL_NAME env-var for openai profile + assert "${env:LLM_MODEL_NAME" not in content diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_integration.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_integration.py new file mode 100644 index 000000000..5df755bcc --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_integration.py @@ -0,0 +1,170 @@ +"""Integration tests — full wizard→config→start flow.""" + +from __future__ import annotations + +from unittest.mock import patch + +import dbgpt.cli._config as _cfg +from dbgpt.cli._config import ( + read_active_profile, + resolve_config_path, + write_active_profile, + write_profile_config, +) +from dbgpt.cli._wizard import run_setup_noninteractive + +# All tests use `isolated_dbgpt_home` from conftest.py — never touches real ~/.dbgpt + + +# --------------------------------------------------------------------------- +# Test 1: First run triggers wizard and creates TOML +# --------------------------------------------------------------------------- + + +def test_first_run_triggers_wizard_creates_toml(isolated_dbgpt_home): + """First run: calling run_setup_wizard creates profile TOML + activates it.""" + from dbgpt.cli._wizard import run_setup_wizard + + # Ensure no config exists before wizard runs + assert not (_cfg._CONFIGS_DIR / "openai.toml").exists() + + with ( + patch("dbgpt.cli._wizard._ask_profile") as mock_ask_profile, + patch("dbgpt.cli._wizard._ask_api_key", return_value="sk-test"), + patch( + "dbgpt.cli._wizard._ask_model_names", + return_value=("gpt-4o", "text-embedding-3-small"), + ), + patch("dbgpt.cli._wizard._print_welcome"), + patch( + "dbgpt.cli._wizard._ask_api_base", + return_value="https://api.openai.com/v1", + ), + ): + from dbgpt.cli._profiles import get_profile + + mock_ask_profile.return_value = get_profile("openai") + + run_setup_wizard() + + # openai.toml must exist under the isolated configs dir + assert (_cfg._CONFIGS_DIR / "openai.toml").exists() + # Active profile must be set to "openai" + assert read_active_profile() == "openai" + + +# --------------------------------------------------------------------------- +# Test 2: Profile switch then start resolves correct config +# --------------------------------------------------------------------------- + + +def test_profile_switch_then_start_resolves_config(isolated_dbgpt_home): + """Switching active profile makes resolve_config_path return the new profile path.""" # noqa: E501 + # Create two TOML files + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "openai.toml").write_text("[models]\n", encoding="utf-8") + (_cfg._CONFIGS_DIR / "qwen.toml").write_text("[models]\n", encoding="utf-8") + + # Switch to qwen + write_active_profile("qwen") + + # resolve_config_path with no args should pick up qwen + result = resolve_config_path() + assert result is not None + assert "qwen.toml" in result + + +# --------------------------------------------------------------------------- +# Test 3: start web help shows config/profile options +# --------------------------------------------------------------------------- + + +def test_start_web_invokes_webserver_with_resolved_config(isolated_dbgpt_home): + """start web --help exits 0 and shows relevant option text.""" + import click.testing + + from dbgpt.cli.cli_scripts import cli + + runner = click.testing.CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["start", "web", "--help"]) + + # --help should always succeed (exit 0) and show options + assert result.exit_code == 0 + # Should show at least one of the expected flags + help_text = result.output + assert "--config" in help_text or "--profile" in help_text or "--yes" in help_text + + +# --------------------------------------------------------------------------- +# Test 4: CLI --profile flag overrides active profile +# --------------------------------------------------------------------------- + + +def test_cli_flag_overrides_active_profile(isolated_dbgpt_home): + """resolve_config_path(profile=...) returns that profile's path, not the active one.""" # noqa: E501 + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "openai.toml").write_text("[models]\n", encoding="utf-8") + (_cfg._CONFIGS_DIR / "qwen.toml").write_text("[models]\n", encoding="utf-8") + + # Active profile is openai + write_active_profile("openai") + + # But we pass profile="qwen" explicitly + result = resolve_config_path(profile="qwen") + assert result is not None + assert "qwen.toml" in result + assert "openai.toml" not in result + + +# --------------------------------------------------------------------------- +# Test 5: Explicit --config flag overrides everything +# --------------------------------------------------------------------------- + + +def test_explicit_config_flag_overrides_all(isolated_dbgpt_home): + """resolve_config_path(config=...) returns the exact path regardless of active profile.""" # noqa: E501 + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "openai.toml").write_text("[models]\n", encoding="utf-8") + write_active_profile("openai") + + custom_path = "/custom/path.toml" + result = resolve_config_path(config=custom_path) + assert result == custom_path + + +# --------------------------------------------------------------------------- +# Test 6: Non-interactive mode creates default profile +# --------------------------------------------------------------------------- + + +def test_noninteractive_mode_creates_default_profile(isolated_dbgpt_home): + """run_setup_noninteractive writes openai.toml with the provided API key.""" + # Confirm no config exists first + assert not (_cfg._CONFIGS_DIR / "openai.toml").exists() + + run_setup_noninteractive(profile_name="openai", api_key="sk-test") + + # Profile TOML must be created + toml_path = _cfg._CONFIGS_DIR / "openai.toml" + assert toml_path.exists() + content = toml_path.read_text(encoding="utf-8") + assert "sk-test" in content + + +# --------------------------------------------------------------------------- +# Test 7: Skip provider creates minimal config +# --------------------------------------------------------------------------- + + +def test_skip_provider_creates_minimal_config(isolated_dbgpt_home): + """write_profile_config('default', ...) creates default.toml and activates it.""" + # Confirm no config exists + assert not (_cfg._CONFIGS_DIR / "default.toml").exists() + + write_profile_config("default", api_key=None, activate=True) + + # default.toml must exist + toml_path = _cfg._CONFIGS_DIR / "default.toml" + assert toml_path.exists() + # Active profile must be "default" + assert read_active_profile() == "default" diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_profile_cmd.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_profile_cmd.py new file mode 100644 index 000000000..3d242d553 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_profile_cmd.py @@ -0,0 +1,126 @@ +"""Tests for dbgpt profile subcommands.""" + +from click.testing import CliRunner + +from dbgpt.cli.cli_scripts import cli + + +class TestProfileList: + def test_profile_list_with_no_configs_shows_empty_message( + self, isolated_dbgpt_home + ): + """profile list with no configs dir → 'No profiles configured'.""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "list"]) + assert result.exit_code == 0, result.output + assert ( + "no profile" in result.output.lower() + or "no config" in result.output.lower() + ) + + def test_profile_list_shows_all_profiles(self, isolated_dbgpt_home): + """profile list with 2 TOML files → both shown.""" + # Create fake TOML files + import dbgpt.cli._config as _cfg + + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "openai.toml").write_text("[models]\n") + (_cfg._CONFIGS_DIR / "kimi.toml").write_text("[models]\n") + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "list"]) + assert result.exit_code == 0, result.output + assert "openai" in result.output + assert "kimi" in result.output + + def test_profile_list_marks_active_with_asterisk(self, isolated_dbgpt_home): + """profile list marks active profile with *.""" + import dbgpt.cli._config as _cfg + + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "openai.toml").write_text("[models]\n") + (_cfg._CONFIGS_DIR / "kimi.toml").write_text("[models]\n") + _cfg._ACTIVE_CONFIG.write_text('[default]\nprofile = "openai"\n') + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "list"]) + assert result.exit_code == 0, result.output + # openai should have * marker + lines = result.output.splitlines() + openai_line = next((line for line in lines if "openai" in line), "") + assert "*" in openai_line, f"Expected * in openai line, got: {openai_line!r}" + + +class TestProfileShow: + def test_profile_show_prints_toml_content(self, isolated_dbgpt_home): + """profile show openai → prints TOML file content.""" + import dbgpt.cli._config as _cfg + + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "openai.toml").write_text("[models]\nllms = []\n") + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "show", "openai"]) + assert result.exit_code == 0, result.output + assert "[models]" in result.output + + def test_profile_show_nonexistent_exits_nonzero(self, isolated_dbgpt_home): + """profile show nonexistent → exit_code != 0.""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "show", "nonexistent"]) + assert result.exit_code != 0 + + +class TestProfileSwitch: + def test_profile_switch_updates_active(self, isolated_dbgpt_home): + """profile switch kimi → kimi becomes active.""" + import dbgpt.cli._config as _cfg + from dbgpt.cli._config import read_active_profile + + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + (_cfg._CONFIGS_DIR / "kimi.toml").write_text("[models]\n") + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "switch", "kimi"]) + assert result.exit_code == 0, result.output + assert read_active_profile() == "kimi" + + def test_profile_switch_nonexistent_exits_nonzero(self, isolated_dbgpt_home): + """profile switch nonexistent → exit_code != 0, suggests create.""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "switch", "nonexistent"]) + assert result.exit_code != 0 + assert ( + "create" in result.output.lower() or "nonexistent" in result.output.lower() + ) + + +class TestProfileDelete: + def test_profile_delete_with_yes_removes_file(self, isolated_dbgpt_home): + """profile delete openai --yes → openai.toml removed.""" + import dbgpt.cli._config as _cfg + + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + toml_path = _cfg._CONFIGS_DIR / "openai.toml" + toml_path.write_text("[models]\n") + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "delete", "openai", "--yes"]) + assert result.exit_code == 0, result.output + assert not toml_path.exists() + + def test_profile_delete_active_clears_active_pointer(self, isolated_dbgpt_home): + """profile delete active profile → clears active pointer.""" + import dbgpt.cli._config as _cfg + from dbgpt.cli._config import read_active_profile + + _cfg._CONFIGS_DIR.mkdir(parents=True, exist_ok=True) + toml_path = _cfg._CONFIGS_DIR / "openai.toml" + toml_path.write_text("[models]\n") + _cfg._ACTIVE_CONFIG.write_text('[default]\nprofile = "openai"\n') + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "delete", "openai", "--yes"]) + assert result.exit_code == 0, result.output + assert not toml_path.exists() + assert read_active_profile() is None + + def test_profile_delete_nonexistent_exits_nonzero(self, isolated_dbgpt_home): + """profile delete nonexistent --yes → exit_code != 0.""" + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["profile", "delete", "nonexistent", "--yes"]) + assert result.exit_code != 0 diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_profiles.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_profiles.py new file mode 100644 index 000000000..f25dbdecd --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_profiles.py @@ -0,0 +1,175 @@ +"""Tests for _profiles.py — GLM/Custom/Default profiles and label fixes.""" + +import pytest + +from dbgpt.cli._profiles import ProfileSpec, get_profile, list_profiles + + +class TestGlmProfile: + """Tests for the glm profile.""" + + def test_glm_profile_spec(self): + """get_profile('glm') should return a ProfileSpec with correct fields.""" + p = get_profile("glm") + assert isinstance(p, ProfileSpec) + assert p.name == "glm" + assert p.label == "z.ai (zhipu.ai API)" + assert p.env_var == "ZHIPUAI_API_KEY" + assert p.llm_model == "glm-4-plus" + assert p.llm_provider == "proxy/zhipu" + assert p.llm_api_base == "https://open.bigmodel.cn/api/paas/v4" + assert p.embedding_model == "embedding-3" + assert p.embedding_provider == "proxy/zhipu" + assert p.needs_api_key is True + + def test_glm_case_insensitive(self): + """get_profile('GLM') should work the same as get_profile('glm').""" + p_lower = get_profile("glm") + p_upper = get_profile("GLM") + assert p_lower.name == p_upper.name + assert p_lower.label == p_upper.label + assert p_lower.env_var == p_upper.env_var + + +class TestCustomProfile: + """Tests for the custom profile.""" + + def test_custom_profile_spec(self): + """get_profile('custom') should return a ProfileSpec with correct fields.""" + p = get_profile("custom") + assert isinstance(p, ProfileSpec) + assert p.name == "custom" + assert p.label == "Custom Provider (Any OpenAI compatible endpoint)" + assert p.env_var == "OPENAI_API_KEY" + assert p.llm_model == "gpt-4o" + assert p.llm_provider == "proxy/openai" + assert p.llm_api_base == "https://api.openai.com/v1" + assert p.needs_api_key is True + + +class TestDefaultProfile: + """Tests for the default (formerly skip) profile.""" + + def test_default_profile_spec(self): + """get_profile('default') returns a ProfileSpec with needs_api_key=False.""" + p = get_profile("default") + assert isinstance(p, ProfileSpec) + assert p.name == "default" + assert p.needs_api_key is False + + def test_default_profile_has_real_openai_values(self): + """default profile should have real OpenAI values for config generation.""" + p = get_profile("default") + assert p.env_var == "OPENAI_API_KEY" + assert p.llm_model == "gpt-4o" + assert p.llm_provider == "proxy/openai" + assert p.llm_api_base == "https://api.openai.com/v1" + assert p.embedding_model == "text-embedding-3-small" + assert p.embedding_provider == "proxy/openai" + + def test_default_profile_label(self): + """default profile label should mention 'Skip for now'.""" + p = get_profile("default") + assert "Skip for now" in p.label + + def test_skip_profile_no_longer_exists(self): + """'skip' is no longer a valid profile name.""" + with pytest.raises(ValueError): + get_profile("skip") + + +class TestProfileOrder: + """Tests for profile ordering.""" + + def test_profile_order_ends_with_new_profiles(self): + """list_profiles() should end with glm, custom, default.""" + profiles = list_profiles() + names = [p.name for p in profiles] + assert names[-3:] == ["glm", "custom", "default"] + + +class TestExistingLabels: + """Tests for fixed labels on existing profiles.""" + + def test_openai_label_fixed(self): + """openai profile label should be updated.""" + p = get_profile("openai") + assert p.label == "OpenAI (OpenAI or OpenAI API proxy)" + + def test_qwen_label_fixed(self): + """qwen profile label should be updated.""" + p = get_profile("qwen") + assert p.label == "Qwen (DashScope API)" + + +class TestErrorHandling: + """Tests for error handling.""" + + def test_unknown_profile_raises_value_error(self): + """get_profile with unknown name should raise ValueError.""" + with pytest.raises(ValueError): + get_profile("nonexistent") + + +class TestKimiEmbedding: + """Tests for Kimi embedding configuration using DashScope.""" + + def test_kimi_embedding_uses_tongyi_provider(self): + p = get_profile("kimi") + assert p.embedding_provider == "proxy/tongyi" + + def test_kimi_embedding_model_is_text_embedding_v3(self): + p = get_profile("kimi") + assert p.embedding_model == "text-embedding-v3" + + def test_kimi_embedding_api_url_is_dashscope(self): + p = get_profile("kimi") + assert ( + p.embedding_api_url + == "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings" + ) + + def test_kimi_has_separate_embedding_env_var(self): + p = get_profile("kimi") + assert p.embedding_env_var == "DASHSCOPE_API_KEY" + assert p.embedding_env_var != p.env_var + + def test_kimi_llm_still_uses_moonshot_env_var(self): + p = get_profile("kimi") + assert p.env_var == "MOONSHOT_API_KEY" + + +class TestMinimaxEmbedding: + """Tests for MiniMax embedding configuration.""" + + def test_minimax_embedding_model_is_embo_01(self): + p = get_profile("minimax") + assert p.embedding_model == "embo-01" + + def test_minimax_embedding_provider_is_openai(self): + p = get_profile("minimax") + assert p.embedding_provider == "proxy/openai" + + def test_minimax_embedding_api_url_is_minimax(self): + p = get_profile("minimax") + assert p.embedding_api_url == "https://api.minimax.chat/v1/embeddings" + + def test_minimax_no_separate_embedding_env_var(self): + p = get_profile("minimax") + assert p.embedding_env_var is None + + +class TestEmbeddingEnvVarDefault: + """Tests for the embedding_env_var default behaviour.""" + + def test_openai_has_no_separate_embedding_env_var(self): + p = get_profile("openai") + assert p.embedding_env_var is None + + def test_qwen_has_no_separate_embedding_env_var(self): + p = get_profile("qwen") + assert p.embedding_env_var is None + + def test_glm_has_no_separate_embedding_env_var(self): + p = get_profile("glm") + assert p.embedding_env_var is None diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_smoke.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_smoke.py new file mode 100644 index 000000000..f8ef130d1 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_smoke.py @@ -0,0 +1,43 @@ +"""Smoke tests for CLI test infrastructure — verifies fixture isolation.""" + +import os +from pathlib import Path + + +def test_isolated_dbgpt_home_is_not_real_home(isolated_dbgpt_home): + """Verify that isolated_dbgpt_home does NOT point to the real ~/.dbgpt.""" + real_home = Path.home() / ".dbgpt" + assert isolated_dbgpt_home != real_home, ( + f"isolated_dbgpt_home should not be the real home: {real_home}" + ) + + +def test_isolated_dbgpt_home_env_var(isolated_dbgpt_home): + """Verify DBGPT_HOME env var is set to the isolated path.""" + env_home = os.environ.get("DBGPT_HOME") + assert env_home is not None, "DBGPT_HOME env var should be set" + assert env_home == str(isolated_dbgpt_home), ( + f"DBGPT_HOME ({env_home}) should match isolated_dbgpt_home ({isolated_dbgpt_home})" # noqa: E501 + ) + + +def test_isolated_dbgpt_home_patches_config_module(isolated_dbgpt_home): + """Verify _config module constants are patched to isolated temp path.""" + import dbgpt.cli._config as _cfg + + assert _cfg._DBGPT_HOME == isolated_dbgpt_home, ( + f"_config._DBGPT_HOME ({_cfg._DBGPT_HOME}) should equal isolated home" + ) + assert _cfg._CONFIGS_DIR == isolated_dbgpt_home / "configs", ( + "_config._CONFIGS_DIR should be under isolated home" + ) + assert _cfg._ACTIVE_CONFIG == isolated_dbgpt_home / "config.toml", ( + "_config._ACTIVE_CONFIG should be under isolated home" + ) + + +def test_cli_runner_returns_click_runner(cli_runner): + """Verify cli_runner fixture returns a CliRunner instance.""" + import click.testing + + assert isinstance(cli_runner, click.testing.CliRunner) diff --git a/packages/dbgpt-core/src/dbgpt/cli/tests/test_wizard.py b/packages/dbgpt-core/src/dbgpt/cli/tests/test_wizard.py new file mode 100644 index 000000000..94077fbf2 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/cli/tests/test_wizard.py @@ -0,0 +1,251 @@ +"""Tests for _wizard.py — model config step and skip/default flow.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from dbgpt.cli._profiles import get_profile + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_log_ask(*return_values): + """Return a mock _log whose .ask() yields values in sequence.""" + mock_log = MagicMock() + mock_log.ask.side_effect = list(return_values) + return mock_log + + +# --------------------------------------------------------------------------- +# _ask_model_names +# --------------------------------------------------------------------------- + + +def test_ask_model_names_returns_defaults_on_enter(): + """User presses Enter → default values from spec returned.""" + from dbgpt.cli._wizard import _ask_model_names + + spec = get_profile("openai") + + mock_log = _make_log_ask("gpt-4o", "text-embedding-3-small") + with patch("dbgpt.cli._wizard._log", mock_log): + llm, emb = _ask_model_names(spec) + + assert llm == "gpt-4o" + assert emb == "text-embedding-3-small" + mock_log.ask.assert_any_call("LLM model name", default="gpt-4o") + mock_log.ask.assert_any_call( + "Embedding model name", default="text-embedding-3-small" + ) + + +def test_ask_model_names_returns_custom_values(): + """User types custom model names → returned as-is.""" + from dbgpt.cli._wizard import _ask_model_names + + spec = get_profile("openai") + + mock_log = _make_log_ask("my-custom-llm", "my-custom-emb") + with patch("dbgpt.cli._wizard._log", mock_log): + llm, emb = _ask_model_names(spec) + + assert llm == "my-custom-llm" + assert emb == "my-custom-emb" + + +# --------------------------------------------------------------------------- +# run_setup_wizard — model names integration +# --------------------------------------------------------------------------- + + +def test_run_setup_wizard_calls_ask_model_names(isolated_dbgpt_home): + """run_setup_wizard passes llm_model/embedding_model to write_profile_config.""" + from dbgpt.cli._wizard import run_setup_wizard + + with ( + patch("dbgpt.cli._wizard._ask_profile") as mock_ask_profile, + patch("dbgpt.cli._wizard._ask_api_key", return_value="sk-test"), + patch( + "dbgpt.cli._wizard._ask_api_base", return_value="https://api.openai.com/v1" + ), + patch( + "dbgpt.cli._wizard._ask_model_names", + return_value=("gpt-4o", "text-embedding-3-small"), + ) as mock_model, + patch( + "dbgpt.cli._wizard.write_profile_config", + return_value=isolated_dbgpt_home / "configs" / "openai.toml", + ) as mock_write, + patch("dbgpt.cli._wizard._print_welcome"), + ): + spec = get_profile("openai") + mock_ask_profile.return_value = spec + + run_setup_wizard() + + mock_model.assert_called_once_with(spec) + mock_write.assert_called_once_with( + "openai", + api_key="sk-test", + activate=True, + llm_model="gpt-4o", + embedding_model="text-embedding-3-small", + api_base="https://api.openai.com/v1", + embedding_api_key=None, + ) + + +# --------------------------------------------------------------------------- +# Default (formerly skip) flow +# --------------------------------------------------------------------------- + + +def test_default_profile_bypasses_all_prompts(isolated_dbgpt_home): + """default spec → _ask_api_key and _ask_model_names never called.""" + from dbgpt.cli._wizard import run_setup_wizard + + with ( + patch("dbgpt.cli._wizard._ask_profile") as mock_ask_profile, + patch("dbgpt.cli._wizard._ask_api_key") as mock_api_key, + patch("dbgpt.cli._wizard._ask_model_names") as mock_model, + patch( + "dbgpt.cli._wizard.write_profile_config", + return_value=isolated_dbgpt_home / "configs" / "default.toml", + ), + patch("dbgpt.cli._wizard._print_welcome"), + ): + mock_ask_profile.return_value = get_profile("default") + + run_setup_wizard() + + mock_api_key.assert_not_called() + mock_model.assert_not_called() + + +# --------------------------------------------------------------------------- +# run_setup_noninteractive — unchanged +# --------------------------------------------------------------------------- + + +def test_run_setup_noninteractive_unchanged(isolated_dbgpt_home): + """run_setup_noninteractive does NOT call _ask_model_names.""" + from dbgpt.cli._wizard import run_setup_noninteractive + + with ( + patch("dbgpt.cli._wizard._ask_model_names") as mock_model, + patch( + "dbgpt.cli._wizard.write_profile_config", + return_value=isolated_dbgpt_home / "configs" / "openai.toml", + ), + ): + run_setup_noninteractive("openai", "sk-xxx") + + mock_model.assert_not_called() + + +# --------------------------------------------------------------------------- +# Custom profile — api_base +# --------------------------------------------------------------------------- + + +def test_custom_profile_asks_api_base(isolated_dbgpt_home): + """custom spec → _ask_api_base is called and result passed to write_profile_config.""" # noqa: E501 + from dbgpt.cli._wizard import run_setup_wizard + + with ( + patch("dbgpt.cli._wizard._ask_profile") as mock_ask_profile, + patch("dbgpt.cli._wizard._ask_api_key", return_value="sk-custom"), + patch( + "dbgpt.cli._wizard._ask_api_base", return_value="https://my.proxy.com/v1" + ) as mock_api_base, + patch( + "dbgpt.cli._wizard._ask_model_names", + return_value=("gpt-4o", "text-embedding-3-small"), + ), + patch( + "dbgpt.cli._wizard.write_profile_config", + return_value=isolated_dbgpt_home / "configs" / "custom.toml", + ) as mock_write, + patch("dbgpt.cli._wizard._print_welcome"), + ): + spec = get_profile("custom") + mock_ask_profile.return_value = spec + + run_setup_wizard() + + mock_api_base.assert_called_once_with(spec) + mock_write.assert_called_once_with( + "custom", + api_key="sk-custom", + activate=True, + llm_model="gpt-4o", + embedding_model="text-embedding-3-small", + api_base="https://my.proxy.com/v1", + embedding_api_key=None, + ) + + +def test_openai_profile_asks_api_base(isolated_dbgpt_home): + """openai spec → _ask_api_base IS called (users often use proxies).""" + from dbgpt.cli._wizard import run_setup_wizard + + with ( + patch("dbgpt.cli._wizard._ask_profile") as mock_ask_profile, + patch("dbgpt.cli._wizard._ask_api_key", return_value="sk-openai"), + patch( + "dbgpt.cli._wizard._ask_api_base", + return_value="https://api.openai.com/v1", + ) as mock_api_base, + patch( + "dbgpt.cli._wizard._ask_model_names", + return_value=("gpt-4o", "text-embedding-3-small"), + ), + patch( + "dbgpt.cli._wizard.write_profile_config", + return_value=isolated_dbgpt_home / "configs" / "openai.toml", + ) as mock_write, + patch("dbgpt.cli._wizard._print_welcome"), + ): + spec = get_profile("openai") + mock_ask_profile.return_value = spec + + run_setup_wizard() + + mock_api_base.assert_called_once_with(spec) + mock_write.assert_called_once_with( + "openai", + api_key="sk-openai", + activate=True, + llm_model="gpt-4o", + embedding_model="text-embedding-3-small", + api_base="https://api.openai.com/v1", + embedding_api_key=None, + ) + + +def test_kimi_profile_does_not_ask_api_base(isolated_dbgpt_home): + """kimi spec → _ask_api_base is NOT called.""" + from dbgpt.cli._wizard import run_setup_wizard + + with ( + patch("dbgpt.cli._wizard._ask_profile") as mock_ask_profile, + patch("dbgpt.cli._wizard._ask_api_key", return_value="sk-test"), + patch("dbgpt.cli._wizard._ask_api_base") as mock_api_base, + patch( + "dbgpt.cli._wizard._ask_model_names", + return_value=("kimi-k2", "text-embedding-v3"), + ), + patch("dbgpt.cli._wizard._ask_embedding_api_key", return_value="ds-key"), + patch( + "dbgpt.cli._wizard.write_profile_config", + return_value=isolated_dbgpt_home / "configs" / "kimi.toml", + ), + patch("dbgpt.cli._wizard._print_welcome"), + ): + mock_ask_profile.return_value = get_profile("kimi") + + run_setup_wizard() + + mock_api_base.assert_not_called() diff --git a/packages/dbgpt-core/src/dbgpt/configs/model_config.py b/packages/dbgpt-core/src/dbgpt/configs/model_config.py index 869469440..c81c330ee 100644 --- a/packages/dbgpt-core/src/dbgpt/configs/model_config.py +++ b/packages/dbgpt-core/src/dbgpt/configs/model_config.py @@ -5,13 +5,34 @@ import os from functools import cache from typing import Optional -ROOT_PATH = os.path.dirname( - os.path.dirname( + +def _detect_root_path() -> str: + """Detect the root path of the DB-GPT installation. + + Determines whether running from a source checkout or a pip install, + and returns the appropriate root path. + + Returns: + str: The repo root directory for source installs, or + ``DBGPT_HOME/workspace`` (defaulting to ``~/.dbgpt/workspace``) + for pip installs. + """ + candidate = os.path.dirname( os.path.dirname( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + ) ) ) -) + if os.path.isfile(os.path.join(candidate, "pyproject.toml")): + return candidate + home = os.environ.get("DBGPT_HOME", os.path.expanduser("~/.dbgpt")) + return os.path.join(home, "workspace") + + +ROOT_PATH = _detect_root_path() MODEL_PATH = os.path.join(ROOT_PATH, "models") PILOT_PATH = os.path.join(ROOT_PATH, "pilot") LOGDIR = os.getenv("DBGPT_LOG_DIR", os.path.join(ROOT_PATH, "logs")) @@ -354,3 +375,29 @@ KNOWLEDGE_CACHE_ROOT_PATH = os.path.join( KNOWLEDGE_UPLOAD_ROOT_PATH, "_knowledge_cache_" ) BENCHMARK_DATA_ROOT_PATH = os.path.join(PILOT_PATH, "benchmark_meta_data") + + +def _detect_skills_dir() -> str: + """Detect the skills directory path. + + Priority: + 1. ``DBGPT_SKILLS_DIR`` environment variable (highest) + 2. ``{ROOT_PATH}/skills`` if it exists (source checkout) + 3. ``~/.dbgpt/skills`` (pip install mode) + + Returns: + str: Absolute path to the skills directory. + """ + env_dir = os.environ.get("DBGPT_SKILLS_DIR") + if env_dir: + return env_dir + + source_dir = os.path.join(ROOT_PATH, "skills") + if os.path.isdir(source_dir): + return source_dir + + home = os.environ.get("DBGPT_HOME", os.path.expanduser("~/.dbgpt")) + return os.path.join(home, "skills") + + +SKILLS_DIR = _detect_skills_dir() diff --git a/packages/dbgpt-core/src/dbgpt/configs/tests/__init__.py b/packages/dbgpt-core/src/dbgpt/configs/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/dbgpt-core/src/dbgpt/configs/tests/test_model_config.py b/packages/dbgpt-core/src/dbgpt/configs/tests/test_model_config.py new file mode 100644 index 000000000..04db43e87 --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/configs/tests/test_model_config.py @@ -0,0 +1,134 @@ +"""Tests for _detect_root_path() in model_config.py.""" + +import os + +# --------------------------------------------------------------------------- +# Helper: call _detect_root_path() with a controlled filesystem view +# --------------------------------------------------------------------------- + + +def _call_detect(monkeypatch, candidate: str, dbgpt_home: str | None = None) -> str: + """Invoke _detect_root_path() with the given candidate and env. + + Args: + monkeypatch: pytest monkeypatch fixture for isolation. + candidate: The fake "6-level dirname" that the function would compute + from ``__file__``. + dbgpt_home: If not None, set DBGPT_HOME env var to this value. + If None, remove DBGPT_HOME from the environment. + + Returns: + str: The return value of ``_detect_root_path()``. + """ + import dbgpt.configs.model_config as _mc + + # Patch os.path.abspath so that the dirname chain resolves to `candidate` + # The function calls os.path.dirname 6 times on os.path.abspath(__file__). + # We make abspath return a path deep enough that 6 dirname calls yield + # exactly `candidate`. + fake_deep_path = os.path.join(candidate, "a", "b", "c", "d", "e", "model_config.py") + + monkeypatch.setattr( + _mc.os.path, + "abspath", + lambda _path: fake_deep_path, + ) + + if dbgpt_home is None: + monkeypatch.delenv("DBGPT_HOME", raising=False) + else: + monkeypatch.setenv("DBGPT_HOME", dbgpt_home) + + return _mc._detect_root_path() + + +# --------------------------------------------------------------------------- +# Test 1: source install — pyproject.toml found at candidate +# --------------------------------------------------------------------------- + + +def test_source_install_returns_candidate(tmp_path, monkeypatch): + """Source install: pyproject.toml at candidate → ROOT_PATH == candidate. + + Creates a fake repo root (tmp_path) with a pyproject.toml sentinel and + verifies that _detect_root_path() returns that directory. + """ + candidate = str(tmp_path) + (tmp_path / "pyproject.toml").write_text("[tool]\n") + + result = _call_detect(monkeypatch, candidate) + + assert result == candidate + + +# --------------------------------------------------------------------------- +# Test 2: pip install default — no pyproject.toml, no DBGPT_HOME +# --------------------------------------------------------------------------- + + +def test_pip_install_default_returns_dot_dbgpt_workspace(tmp_path, monkeypatch): + """Pip install (default): no pyproject.toml, no DBGPT_HOME → ~/.dbgpt/workspace. + + Ensures the fallback path is ``~/.dbgpt/workspace`` when the candidate + directory does not contain a pyproject.toml and DBGPT_HOME is not set. + """ + # candidate has no pyproject.toml + candidate = str(tmp_path) + + result = _call_detect(monkeypatch, candidate, dbgpt_home=None) + + expected = os.path.join(os.path.expanduser("~/.dbgpt"), "workspace") + assert result == expected + + +# --------------------------------------------------------------------------- +# Test 3: pip install with custom DBGPT_HOME +# --------------------------------------------------------------------------- + + +def test_pip_install_custom_dbgpt_home_returns_workspace_under_home( + tmp_path, monkeypatch +): + """Pip install + DBGPT_HOME: no pyproject.toml + DBGPT_HOME → DBGPT_HOME/workspace. + + Verifies that when DBGPT_HOME is set to a custom path and pyproject.toml + does not exist at the candidate, the result is ``DBGPT_HOME/workspace``. + """ + candidate = str(tmp_path / "candidate") + os.makedirs(candidate, exist_ok=True) + # no pyproject.toml in candidate + + custom_home = str(tmp_path / "custom_home") + + result = _call_detect(monkeypatch, candidate, dbgpt_home=custom_home) + + assert result == os.path.join(custom_home, "workspace") + + +# --------------------------------------------------------------------------- +# Test 4: derived constants follow ROOT_PATH +# --------------------------------------------------------------------------- + + +def test_derived_constants_follow_root_path(tmp_path, monkeypatch): + """Derived constants PILOT_PATH and STATIC_MESSAGE_IMG_PATH follow ROOT_PATH. + + After _detect_root_path() would return a given path, PILOT_PATH and + STATIC_MESSAGE_IMG_PATH should be derived consistently from it. + Validates the relationship defined in model_config.py: + + PILOT_PATH = ROOT_PATH + "/pilot" + STATIC_MESSAGE_IMG_PATH = PILOT_PATH + "/message/img" + """ + candidate = str(tmp_path) + (tmp_path / "pyproject.toml").write_text("[tool]\n") + + root = _call_detect(monkeypatch, candidate) + + expected_pilot = os.path.join(root, "pilot") + expected_img = os.path.join(expected_pilot, "message/img") + + # Verify the relationships (not the module-level cached constants, which + # were evaluated at import time with the real filesystem). + assert os.path.join(root, "pilot") == expected_pilot + assert os.path.join(expected_pilot, "message/img") == expected_img diff --git a/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_minimax.py b/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_minimax.py index 57e8d8b58..1707d9955 100644 --- a/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_minimax.py +++ b/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_minimax.py @@ -3,8 +3,6 @@ import os from unittest.mock import patch -import pytest - from dbgpt.model.proxy.llms.minimax import ( _DEFAULT_MODEL, MiniMaxDeployModelParameters, @@ -38,28 +36,24 @@ class TestMiniMaxModelList: def test_model_list_contains_m27(self): # Import triggers registration from dbgpt.model.adapter.base import get_model_adapter - from dbgpt.model.proxy.llms.minimax import MiniMaxLLMClient # noqa: F811 adapter = get_model_adapter("proxy/minimax", "MiniMax-M2.7") assert adapter is not None def test_model_list_contains_m27_highspeed(self): from dbgpt.model.adapter.base import get_model_adapter - from dbgpt.model.proxy.llms.minimax import MiniMaxLLMClient # noqa: F811 adapter = get_model_adapter("proxy/minimax", "MiniMax-M2.7-highspeed") assert adapter is not None def test_model_list_contains_m25(self): from dbgpt.model.adapter.base import get_model_adapter - from dbgpt.model.proxy.llms.minimax import MiniMaxLLMClient # noqa: F811 adapter = get_model_adapter("proxy/minimax", "MiniMax-M2.5") assert adapter is not None def test_model_list_contains_m25_highspeed(self): from dbgpt.model.adapter.base import get_model_adapter - from dbgpt.model.proxy.llms.minimax import MiniMaxLLMClient # noqa: F811 adapter = get_model_adapter("proxy/minimax", "MiniMax-M2.5-highspeed") assert adapter is not None diff --git a/packages/dbgpt-core/src/dbgpt/util/_db_migration_utils.py b/packages/dbgpt-core/src/dbgpt/util/_db_migration_utils.py index 39428c454..167aecde7 100644 --- a/packages/dbgpt-core/src/dbgpt/util/_db_migration_utils.py +++ b/packages/dbgpt-core/src/dbgpt/util/_db_migration_utils.py @@ -35,14 +35,14 @@ def create_alembic_config( alembic_ini_path = alembic_ini_path or os.path.join( alembic_root_path, "alembic.ini" ) - alembic_cfg = AlembicConfig(alembic_ini_path) - alembic_cfg.set_main_option("sqlalchemy.url", str(engine.url)) script_location = script_location or os.path.join(alembic_root_path, "alembic") versions_dir = os.path.join(script_location, "versions") os.makedirs(script_location, exist_ok=True) os.makedirs(versions_dir, exist_ok=True) + alembic_cfg = AlembicConfig(alembic_ini_path) + alembic_cfg.set_main_option("sqlalchemy.url", str(engine.url)) alembic_cfg.set_main_option("script_location", script_location) alembic_cfg.attributes["target_metadata"] = base.metadata diff --git a/packages/dbgpt-core/src/dbgpt/util/console/console.py b/packages/dbgpt-core/src/dbgpt/util/console/console.py index 423c22d11..49486cc76 100644 --- a/packages/dbgpt-core/src/dbgpt/util/console/console.py +++ b/packages/dbgpt-core/src/dbgpt/util/console/console.py @@ -3,7 +3,7 @@ import dataclasses import sys from functools import lru_cache -from typing import Any +from typing import Any, Callable, List, Optional, Tuple from rich.console import Console from rich.markdown import Markdown @@ -40,6 +40,66 @@ def get_console(output: Output | None = None) -> Console: ) +# --------------------------------------------------------------------------- +# Terminal raw-mode helpers (stdlib only) +# --------------------------------------------------------------------------- + + +def _supports_raw_mode() -> bool: + """Return True if stdin supports raw mode (TTY and termios available).""" + if not sys.stdin.isatty(): + return False + try: + import termios # noqa: F401 + import tty # noqa: F401 + + return True + except ImportError: + return False + + +def _read_key() -> str: + """Read a single keypress from stdin in raw mode. + + Returns: + str: One of ``'up'``, ``'down'``, ``'enter'``, or the raw character. + + Raises: + KeyboardInterrupt: On Ctrl-C. + """ + import termios + import tty + + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + if ch == "\x1b": + ch2 = sys.stdin.read(1) + ch3 = sys.stdin.read(1) + if ch2 == "[": + if ch3 == "A": + return "up" + if ch3 == "B": + return "down" + return ch + if ch in ("\r", "\n"): + return "enter" + if ch == "\x03": + raise KeyboardInterrupt + return ch + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +def _move_up(console: Console, lines: int) -> None: + """Move terminal cursor up *lines* rows and clear those lines.""" + # ANSI: move up N lines then erase to end of screen + console.file.write(f"\x1b[{lines}A\x1b[0J") + console.file.flush() + + class CliLogger: def __init__(self, output: Output | None = None): self.console = get_console(output) @@ -70,3 +130,99 @@ class CliLogger: def ask(self, msg: str, **kwargs): return Prompt.ask(msg, **kwargs) + + def select( + self, + prompt: str, + options: List[Tuple[str, str]], + _read_key_fn: Optional[Callable[[], str]] = None, + ) -> int: + """Display an interactive arrow-key selector and return the chosen index. + + Args: + prompt (str): Header text displayed above the option list. + options (List[Tuple[str, str]]): Each tuple is (name, description). + ``name`` is the display name rendered in bold. + ``description`` is dim helper text shown after the name. + _read_key_fn (Optional[Callable[[], str]]): Override the key-reading + function (used for testing). Should return one of: ``'up'``, + ``'down'``, ``'enter'``, or a single character string. + + Returns: + int: Zero-based index of the selected option. + """ + read_key = _read_key_fn or _read_key + + # If raw-mode isn't available, fall back to numbered input. + if not _supports_raw_mode(): + return self._select_fallback(prompt, options) + + current = 0 + n = len(options) + + self.console.print(f"\n {prompt}\n") + + def _render(idx: int, move_up_lines: int = 0) -> None: + buf = "" + if move_up_lines > 0: + buf += f"\x1b[{move_up_lines}A\x1b[0J" + for i, (name, desc) in enumerate(options): + if i == idx: + marker = "\x1b[1;96m●\x1b[0m" + else: + marker = "\x1b[2m○\x1b[0m" + bold_name = f"\x1b[1m{name}\x1b[0m" + dim_desc = f"\x1b[2m{desc}\x1b[0m" + buf += f" {marker} {bold_name:<20}{dim_desc}\n" + self.console.file.write(buf) + self.console.file.flush() + + _render(current) + + while True: + try: + key = read_key() + except KeyboardInterrupt: + raise + + if key == "up": + current = (current - 1) % n + _render(current, move_up_lines=n) + elif key == "down": + current = (current + 1) % n + _render(current, move_up_lines=n) + elif key == "enter": + _render(current, move_up_lines=n) + self.console.print("") + return current + elif key.isdigit(): + num = int(key) + if 1 <= num <= n: + current = num - 1 + _render(current, move_up_lines=n) + self.console.print("") + return current + + def _select_fallback( + self, + prompt: str, + options: List[Tuple[str, str]], + ) -> int: + """Numbered fallback for non-TTY environments.""" + self.console.print(f"\n {prompt}\n") + for i, (name, desc) in enumerate(options, start=1): + self.console.print( + f" [[bold]{i}[/bold]] [bold]{name}[/bold] [dim]{desc}[/dim]" + ) + self.console.print("") + while True: + raw = Prompt.ask("Enter a number", default="1") + try: + choice = int(raw) + if 1 <= choice <= len(options): + return choice - 1 + except (ValueError, TypeError): + pass + self.console.print( + f"[warning]Please enter a number between 1 and {len(options)}.[/]" + ) diff --git a/packages/dbgpt-core/src/dbgpt/util/tests/test_db_migration_utils.py b/packages/dbgpt-core/src/dbgpt/util/tests/test_db_migration_utils.py new file mode 100644 index 000000000..65ff1c2fb --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/util/tests/test_db_migration_utils.py @@ -0,0 +1,30 @@ +"""Tests for _db_migration_utils create_alembic_config behaviour.""" + +from unittest.mock import MagicMock, patch + + +def test_create_alembic_config_creates_versions_dir(tmp_path): + """create_alembic_config must create script_location and versions/ directories.""" + from dbgpt.util._db_migration_utils import create_alembic_config + + mock_engine = MagicMock() + mock_engine.url = "sqlite:///test.db" + mock_base = MagicMock() + mock_base.metadata = MagicMock() + mock_session = MagicMock() + + alembic_root = str(tmp_path) + + with patch("dbgpt.util._db_migration_utils.AlembicConfig") as mock_alembic_cfg_cls: + mock_cfg = MagicMock() + mock_alembic_cfg_cls.return_value = mock_cfg + + result = create_alembic_config( + alembic_root, mock_engine, mock_base, mock_session + ) + + alembic_dir = tmp_path / "alembic" + versions_dir = alembic_dir / "versions" + assert alembic_dir.exists(), "script_location directory must be created" + assert versions_dir.exists(), "versions directory must be created" + assert result is mock_cfg diff --git a/packages/dbgpt-ext/pyproject.toml b/packages/dbgpt-ext/pyproject.toml index 9784d2545..45dec7983 100644 --- a/packages/dbgpt-ext/pyproject.toml +++ b/packages/dbgpt-ext/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-ext" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "Add your description here" authors = [ { name = "csunny", email = "cfqcsunny@gmail.com" } diff --git a/packages/dbgpt-ext/src/dbgpt_ext/_version.py b/packages/dbgpt-ext/src/dbgpt_ext/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-ext/src/dbgpt_ext/_version.py +++ b/packages/dbgpt-ext/src/dbgpt_ext/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/packages/dbgpt-sandbox/pyproject.toml b/packages/dbgpt-sandbox/pyproject.toml index 3f795948a..8d805ccdd 100644 --- a/packages/dbgpt-sandbox/pyproject.toml +++ b/packages/dbgpt-sandbox/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-sandbox" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "A secure sandbox execution environment for DB-GPT Agent" authors = [ { name = "csunny", email = "cfqcsunny@gmail.com" } diff --git a/packages/dbgpt-serve/pyproject.toml b/packages/dbgpt-serve/pyproject.toml index bcc003a6c..bf05f0f71 100644 --- a/packages/dbgpt-serve/pyproject.toml +++ b/packages/dbgpt-serve/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-serve" -version = "0.8.0rc1" +version = "0.8.0rc6" description = "Add your description here" authors = [ { name = "csunny", email = "cfqcsunny@gmail.com" } diff --git a/packages/dbgpt-serve/src/dbgpt_serve/_version.py b/packages/dbgpt-serve/src/dbgpt_serve/_version.py index f1b3ba841..34f3f57b6 100644 --- a/packages/dbgpt-serve/src/dbgpt_serve/_version.py +++ b/packages/dbgpt-serve/src/dbgpt_serve/_version.py @@ -1 +1 @@ -version = "0.8.0rc1" +version = "0.8.0rc6" diff --git a/pilot/examples/Walmart_Sales.db b/pilot/examples/Walmart_Sales.db new file mode 100644 index 0000000000000000000000000000000000000000..1d7e4bb72c5a63f512875c540cac26600b650d48 GIT binary patch literal 430080 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 04ed72180..c2bcef3e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbgpt-mono" -version = "0.8.0rc1" +version = "0.8.0rc6" description = """DB-GPT is an experimental open-source project that uses localized GPT \ large models to interact with your data and environment. With this solution, you can be\ assured that there is no risk of data leakage, and your data is 100% private and secure.\