mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/cli: keep database url :memory:; default url None
Unset/empty url still maps to durable user-data chat.db. Explicit :memory: is not rewritten; CLI warns when not quiet. DatabaseConfig.url default is None; engine treats None as memory.
This commit is contained in:
@@ -97,7 +97,7 @@ Shared interactive I/O: `ask` / `choose` / `form` / `confirm` with pluggable bac
|
|||||||
|
|
||||||
Click app + readline REPL. Entry: `plyngent` / `python -m plyngent`.
|
Click app + readline REPL. Entry: `plyngent` / `python -m plyngent`.
|
||||||
|
|
||||||
- **`plyngent chat`**: provider/model (flags or interactive; Tab via readline in `prompting`); sessions store `provider_name`/`model` and restore on resume; SQLite via `[database]` (file DB under user data if unset/`:memory:`); workspace-bound; resume latest for cwd/`--workspace` by default (`--new` / `--session`). One-shot: `-p/--prompt` and non-TTY stdin; exit codes 0/1/2/3; `--yes` (YOLO on), `--stream/--no-stream`, `--quiet`. Root `--log-level`.
|
- **`plyngent chat`**: provider/model (flags or interactive; Tab via readline in `prompting`); sessions store `provider_name`/`model` and restore on resume; SQLite via `[database]` (file DB under user data when url unset; explicit `url = ":memory:"` kept + warn); workspace-bound; resume latest for cwd/`--workspace` by default (`--new` / `--session`). One-shot: `-p/--prompt` and non-TTY stdin; exit codes 0/1/2/3; `--yes` (YOLO on), `--stream/--no-stream`, `--quiet`. Root `--log-level`.
|
||||||
- Slash: Click group in `cli/slash.py` + `awaitlet` for async work; Tab completer from registry + ParamType `shell_complete`. Multiline `"""` … `"""`; `/edit` via `$EDITOR`. `/yolo on|off|once` for soft destructive confirms. `/model --persist` / `/models --persist` write model catalog entries into TOML. `/todos` for human show/push/pop/clear of the todo stack.
|
- Slash: Click group in `cli/slash.py` + `awaitlet` for async work; Tab completer from registry + ParamType `shell_complete`. Multiline `"""` … `"""`; `/edit` via `$EDITOR`. `/yolo on|off|once` for soft destructive confirms. `/model --persist` / `/models --persist` write model catalog entries into TOML. `/todos` for human show/push/pop/clear of the todo stack.
|
||||||
- Explicit `/resume` or `--session` from another workspace prompts: **keep** / **update** / **abort**.
|
- Explicit `/resume` or `--session` from another workspace prompts: **keep** / **update** / **abort**.
|
||||||
- Failed/cancelled turns: user kept; **committed tool rounds kept** (side effects not re-run on `/retry`); only unfinished assistant rolled back; Ctrl+C cancels; TTY confirms off-loop; auto-retry 10s/20s/30s then `/retry`.
|
- Failed/cancelled turns: user kept; **committed tool rounds kept** (side effects not re-run on `/retry`); only unfinished assistant rolled back; Ctrl+C cancels; TTY confirms off-loop; auto-retry 10s/20s/30s then `/retry`.
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ max_context_tokens = 200000
|
|||||||
|
|
||||||
Supported provider presets today: `openai` (default if `preset` is omitted; default models `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.4-nano` when `models` is omitted), `openai-compatible`, `deepseek` (OpenAI convention; default models `deepseek-v4-flash` and `deepseek-v4-pro` if `models` is omitted). Anthropic presets are modeled in config but not wired in the runtime client yet.
|
Supported provider presets today: `openai` (default if `preset` is omitted; default models `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.4-nano` when `models` is omitted), `openai-compatible`, `deepseek` (OpenAI convention; default models `deepseek-v4-flash` and `deepseek-v4-pro` if `models` is omitted). Anthropic presets are modeled in config but not wired in the runtime client yet.
|
||||||
|
|
||||||
If `[database]` is omitted (or SQLite `url` is empty/`":memory:"`), chat uses a durable file under the user data dir (e.g. `~/.local/share/plyngent/chat.db` on Linux).
|
If `[database]` is omitted (or SQLite `url` is unset/empty), chat uses a durable file under the user data dir (e.g. `~/.local/share/plyngent/chat.db` on Linux). Set `url = ":memory:"` for a true in-memory SQLite (CLI warns; no file; useful for tests).
|
||||||
|
|
||||||
## Chat
|
## Chat
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
# Copy to the path shown by: plyngent config path
|
# Copy to the path shown by: plyngent config path
|
||||||
# Then set access_key_or_token and adjust models.
|
# Then set access_key_or_token and adjust models.
|
||||||
|
|
||||||
# Optional. If omitted (or SQLite url is empty / ":memory:"), the CLI uses a
|
# Optional. If omitted (or SQLite url unset/empty), the CLI uses a durable file under
|
||||||
# durable file under the platform user data dir (e.g. ~/.local/share/plyngent/chat.db).
|
# the platform user data dir (e.g. ~/.local/share/plyngent/chat.db).
|
||||||
|
# url = ":memory:" → true in-memory SQLite (CLI warns; useful for tests).
|
||||||
# [database]
|
# [database]
|
||||||
# implementation = "sqlite"
|
# implementation = "sqlite"
|
||||||
# url = "/path/to/chat.db"
|
# url = "/path/to/chat.db"
|
||||||
|
# # url = ":memory:"
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
system_prompt = "You are a careful coding assistant. Prefer small, verified edits."
|
system_prompt = "You are a careful coding assistant. Prefer small, verified edits."
|
||||||
|
|||||||
+11
-2
@@ -82,12 +82,21 @@ def _warn_recoverable_providers(recoverable: Mapping[str, object]) -> None:
|
|||||||
|
|
||||||
def _database_config(store: ConfigStore, *, quiet: bool = False) -> DatabaseConfig:
|
def _database_config(store: ConfigStore, *, quiet: bool = False) -> DatabaseConfig:
|
||||||
raw = dict(store.database)
|
raw = dict(store.database)
|
||||||
# Prefer a durable file DB so sessions survive CLI restarts.
|
url = raw.get("url")
|
||||||
if raw.get("url") in {None, "", ":memory:"} and raw.get("implementation", "sqlite") == "sqlite":
|
impl = raw.get("implementation", "sqlite")
|
||||||
|
# Unset/empty → durable user-data chat.db so interactive sessions persist.
|
||||||
|
# Explicit ``:memory:`` is kept as-is (ephemeral; warn so it is not accidental).
|
||||||
|
if impl == "sqlite" and url in {None, ""}:
|
||||||
db_path = user_data_path("plyngent", ensure_exists=True) / _DEFAULT_DB_FILENAME
|
db_path = user_data_path("plyngent", ensure_exists=True) / _DEFAULT_DB_FILENAME
|
||||||
raw = {**raw, "implementation": "sqlite", "url": str(db_path)}
|
raw = {**raw, "implementation": "sqlite", "url": str(db_path)}
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho(f"using database: {db_path}", fg="bright_black", err=True)
|
click.secho(f"using database: {db_path}", fg="bright_black", err=True)
|
||||||
|
elif not quiet and impl == "sqlite" and url == ":memory:":
|
||||||
|
click.secho(
|
||||||
|
"warning: database url is :memory: — sessions are not persisted to disk",
|
||||||
|
fg="yellow",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
return msgspec.convert(raw, DatabaseConfig)
|
return msgspec.convert(raw, DatabaseConfig)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ _MINIMAL_CONFIG = """\
|
|||||||
# plyngent configuration
|
# plyngent configuration
|
||||||
# edit providers below
|
# edit providers below
|
||||||
|
|
||||||
# Optional: omit [database] to use ~/.local/share/plyngent/chat.db (Linux).
|
# Optional: omit [database] (or leave url unset) → ~/.local/share/plyngent/chat.db.
|
||||||
|
# url = ":memory:" keeps a true in-memory SQLite (CLI warns; nothing on disk).
|
||||||
# [database]
|
# [database]
|
||||||
# implementation = "sqlite"
|
# implementation = "sqlite"
|
||||||
# url = "/path/to/chat.db"
|
# url = "/path/to/chat.db"
|
||||||
|
# # url = ":memory:"
|
||||||
|
|
||||||
# [agent]
|
# [agent]
|
||||||
# system_prompt = "You are a careful coding assistant."
|
# system_prompt = "You are a careful coding assistant."
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ from msgspec import Struct, field
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(Struct, omit_defaults=True):
|
class DatabaseConfig(Struct, omit_defaults=True):
|
||||||
"""Database connection configuration."""
|
"""Database connection configuration.
|
||||||
|
|
||||||
|
``url`` is ``None`` when unset. The CLI fills a durable user-data
|
||||||
|
``chat.db`` only for unset/empty url. Explicit ``":memory:"`` is a true
|
||||||
|
in-memory SQLite (no rewrite).
|
||||||
|
"""
|
||||||
|
|
||||||
implementation: str = "sqlite"
|
implementation: str = "sqlite"
|
||||||
url: str = ":memory:"
|
url: str | None = None
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password: str | None = None
|
password: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def build_async_url(config: DatabaseConfig) -> str:
|
|||||||
raise UnsupportedDatabaseError(msg)
|
raise UnsupportedDatabaseError(msg)
|
||||||
|
|
||||||
url = config.url
|
url = config.url
|
||||||
if url in {":memory:", ""}:
|
if url is None or url in {":memory:", ""}:
|
||||||
return "sqlite+aiosqlite:///:memory:"
|
return "sqlite+aiosqlite:///:memory:"
|
||||||
if url.startswith("sqlite+aiosqlite://"):
|
if url.startswith("sqlite+aiosqlite://"):
|
||||||
return url
|
return url
|
||||||
|
|||||||
@@ -1,10 +1,102 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from plyngent.cli.app import main
|
from plyngent.cli.app import _database_config, main
|
||||||
|
from plyngent.config import load as load_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_config_memory_url_kept_with_warning(
|
||||||
|
tmp_path: Path,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
"""Explicit ``url = ":memory:"`` is not rewritten; CLI warns (when not quiet)."""
|
||||||
|
path = tmp_path / "mem.toml"
|
||||||
|
_ = path.write_text(
|
||||||
|
"""
|
||||||
|
[database]
|
||||||
|
implementation = "sqlite"
|
||||||
|
url = ":memory:"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store = load_config(path)
|
||||||
|
cfg = _database_config(store, quiet=False)
|
||||||
|
assert cfg.url == ":memory:"
|
||||||
|
assert cfg.implementation == "sqlite"
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert ":memory:" in err
|
||||||
|
assert "not persisted" in err.lower() or "warning" in err.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_config_memory_url_quiet_no_warn(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
path = tmp_path / "mem.toml"
|
||||||
|
_ = path.write_text(
|
||||||
|
"""
|
||||||
|
[database]
|
||||||
|
implementation = "sqlite"
|
||||||
|
url = ":memory:"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store = load_config(path)
|
||||||
|
cfg = _database_config(store, quiet=True)
|
||||||
|
assert cfg.url == ":memory:"
|
||||||
|
assert capsys.readouterr().err == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_config_unset_url_uses_user_data(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Unset/empty url → durable user-data chat.db."""
|
||||||
|
data_dir = tmp_path / "user-data"
|
||||||
|
_ = data_dir.mkdir()
|
||||||
|
|
||||||
|
def fake_user_data_path(*_args: object, **_kwargs: object) -> Path:
|
||||||
|
return data_dir
|
||||||
|
|
||||||
|
monkeypatch.setattr("plyngent.cli.app.user_data_path", fake_user_data_path)
|
||||||
|
path = tmp_path / "empty-url.toml"
|
||||||
|
_ = path.write_text(
|
||||||
|
"""
|
||||||
|
[database]
|
||||||
|
implementation = "sqlite"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store = load_config(path)
|
||||||
|
cfg = _database_config(store, quiet=True)
|
||||||
|
assert cfg.url == str(data_dir / "chat.db")
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_config_omitted_section_uses_user_data(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Omitted [database] → durable user-data chat.db (url default None)."""
|
||||||
|
data_dir = tmp_path / "user-data2"
|
||||||
|
_ = data_dir.mkdir()
|
||||||
|
|
||||||
|
def fake_user_data_path(*_args: object, **_kwargs: object) -> Path:
|
||||||
|
return data_dir
|
||||||
|
|
||||||
|
monkeypatch.setattr("plyngent.cli.app.user_data_path", fake_user_data_path)
|
||||||
|
path = tmp_path / "no-db.toml"
|
||||||
|
_ = path.write_text(
|
||||||
|
"""
|
||||||
|
[providers.local]
|
||||||
|
preset = "openai-compatible"
|
||||||
|
url = "http://127.0.0.1:1/v1"
|
||||||
|
access_key_or_token = "x"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store = load_config(path)
|
||||||
|
assert store.database.get("url") is None
|
||||||
|
cfg = _database_config(store, quiet=True)
|
||||||
|
assert cfg.url == str(data_dir / "chat.db")
|
||||||
|
|
||||||
|
|
||||||
def test_providers_list() -> None:
|
def test_providers_list() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user