mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/memory+cli: remember session LLM; readline prompts
Store provider_name/model on sessions (SQLite migrate); restore on resume and persist on create//model//provider. Startup prefers session selection over re-asking. ask/choose and provider/model pick use readline with Tab completion when available.
This commit is contained in:
@@ -38,7 +38,11 @@ def test_session_export_json_roundtrip() -> None:
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
messages=messages,
|
||||
provider_name="local",
|
||||
model="tiny",
|
||||
)
|
||||
assert payload.get("provider") == "local"
|
||||
assert payload.get("model") == "tiny"
|
||||
assert isinstance(payload, dict)
|
||||
raw = encode_session_export_json(payload)
|
||||
assert "7" in raw
|
||||
|
||||
@@ -173,6 +173,23 @@ async def test_rename_slash(state: ReplState) -> None:
|
||||
assert row.name == "my-chat"
|
||||
|
||||
|
||||
async def test_model_switch_persists(state: ReplState) -> None:
|
||||
from plyngent.config.models import ModelConfig
|
||||
|
||||
state.provider.models = {
|
||||
"m1": ModelConfig(),
|
||||
"m2": ModelConfig(),
|
||||
}
|
||||
state.model = "m1"
|
||||
await state.persist_llm_selection()
|
||||
assert await handle_slash(state, "/model m2") is True
|
||||
assert state.model == "m2"
|
||||
assert state.session_id is not None
|
||||
row = await state.memory.get_session(state.session_id)
|
||||
assert row is not None
|
||||
assert row.model == "m2"
|
||||
|
||||
|
||||
async def test_delete_slash_confirm(
|
||||
state: ReplState,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
|
||||
@@ -38,11 +38,23 @@ def test_select_model_from_list() -> None:
|
||||
assert select_model(provider, preferred="m1") == "m1"
|
||||
|
||||
|
||||
def test_select_model_prompt_when_empty(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_select_model_prompt_when_empty() -> None:
|
||||
from plyngent.prompting import temporary_backend
|
||||
from tests.test_prompting import ScriptedBackend
|
||||
|
||||
provider = OpenAIProvider(access_key_or_token="sk")
|
||||
with temporary_backend(ScriptedBackend(["gpt-test"])):
|
||||
assert select_model(provider) == "gpt-test"
|
||||
|
||||
def _prompt(*_args: object, **_kwargs: object) -> str:
|
||||
return "gpt-test"
|
||||
|
||||
monkeypatch.setattr("click.prompt", _prompt)
|
||||
assert select_model(provider) == "gpt-test"
|
||||
def test_select_provider_interactive_choose() -> None:
|
||||
from plyngent.prompting import temporary_backend
|
||||
from tests.test_prompting import ScriptedBackend
|
||||
|
||||
providers = {
|
||||
"a": OpenAIProvider(access_key_or_token="sk"),
|
||||
"b": OpenAICompatibleProvider(access_key_or_token="sk", url="https://x/v1"),
|
||||
}
|
||||
with temporary_backend(ScriptedBackend(["2"])):
|
||||
name, _ = select_provider(providers)
|
||||
assert name == "b"
|
||||
|
||||
@@ -129,6 +129,24 @@ async def test_delete_session_cascades_messages(store: MemoryStore) -> None:
|
||||
assert await store.delete_session(session.sid) is False
|
||||
|
||||
|
||||
async def test_session_llm_remembered(store: MemoryStore) -> None:
|
||||
session = await store.create_session(
|
||||
name="llm",
|
||||
provider_name="deepseek",
|
||||
model="deepseek-v4-flash",
|
||||
)
|
||||
row = await store.get_session(session.sid)
|
||||
assert row is not None
|
||||
assert row.provider_name == "deepseek"
|
||||
assert row.model == "deepseek-v4-flash"
|
||||
updated = await store.update_session_llm(session.sid, model="deepseek-v4-pro")
|
||||
assert updated.provider_name == "deepseek"
|
||||
assert updated.model == "deepseek-v4-pro"
|
||||
again = await store.update_session_llm(session.sid, provider_name="other")
|
||||
assert again.provider_name == "other"
|
||||
assert again.model == "deepseek-v4-pro"
|
||||
|
||||
|
||||
async def test_update_session_workspace(store: MemoryStore, tmp_path: object) -> None:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -27,8 +27,14 @@ class ScriptedBackend:
|
||||
def is_interactive(self) -> bool:
|
||||
return self.interactive
|
||||
|
||||
def read_line(self, prompt: str, *, default: str | None = None) -> str:
|
||||
del prompt
|
||||
def read_line(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
default: str | None = None,
|
||||
completions: object = None,
|
||||
) -> str:
|
||||
del prompt, completions
|
||||
if self.lines:
|
||||
return self.lines.pop(0)
|
||||
if default is not None:
|
||||
|
||||
Reference in New Issue
Block a user