feat: load editable prompts from markdown

This commit is contained in:
Azimkin
2026-09-19 16:25:19 +02:00
parent 110b9fee0b
commit 7272147270
10 changed files with 201 additions and 25 deletions

View File

@@ -11,6 +11,7 @@ from llm_trade_bot.config import (
BraveConfig,
BybitConfig,
DeepSeekConfig,
PromptsConfig,
RiskConfig,
ScannerConfig,
ScheduleConfig,
@@ -51,6 +52,7 @@ def make_config(database_path: Path) -> AppConfig:
BybitConfig("key", "secret"),
TelegramConfig(1, "hash", "token", frozenset({1})),
BraveConfig("key"),
PromptsConfig(database_path.parent / "prompts.md"),
ScheduleConfig(ZoneInfo("Europe/Warsaw"), (time(9), time(21))),
ScannerConfig(),
RiskConfig(),

View File

@@ -17,6 +17,8 @@ bot_token = "bot-token"
admin_user_ids = [42]
[brave]
api_key = "brave-key"
[prompts]
file_path = "prompts.md"
[schedule]
timezone = "Europe/Warsaw"
times = ["09:00", "21:00"]
@@ -31,26 +33,45 @@ max_leverage = 3
database_path = "state.db"
"""
PROMPTS = """# Prompts
## master_prompt
Master prompt.
## run_prompt
Run prompt.
"""
def write_config(tmp_path: Path, content: str = CONFIG) -> Path:
path = tmp_path / "config.toml"
path.write_text(content, encoding="utf-8")
(tmp_path / "prompts.md").write_text(PROMPTS, encoding="utf-8")
return path
def test_loads_and_resolves_database_path(tmp_path: Path):
path = tmp_path / "config.toml"
path.write_text(CONFIG, encoding="utf-8")
path = write_config(tmp_path)
config = load_config(path)
assert config.deepseek.model == "deepseek-flash"
assert config.telegram.admin_user_ids == frozenset({42})
assert config.storage.database_path == tmp_path / "state.db"
assert config.prompts.file_path == tmp_path / "prompts.md"
def test_rejects_risk_above_hard_limit(tmp_path: Path):
path = tmp_path / "config.toml"
path.write_text(CONFIG.replace("max_leverage = 3", "max_leverage = 4"), encoding="utf-8")
path = write_config(tmp_path, CONFIG.replace("max_leverage = 3", "max_leverage = 4"))
with pytest.raises(ConfigError, match="max_leverage"):
load_config(path)
def test_rejects_non_official_deepseek_endpoint(tmp_path: Path):
path = tmp_path / "config.toml"
changed = CONFIG.replace("api_key = \"deepseek-key\"", 'api_key = "deepseek-key"\nbase_url = "https://example.com"', 1)
path.write_text(changed, encoding="utf-8")
path = write_config(tmp_path, changed)
with pytest.raises(ConfigError, match="base_url"):
load_config(path)
def test_rejects_missing_prompts_file(tmp_path: Path):
path = tmp_path / "config.toml"
path.write_text(CONFIG, encoding="utf-8")
with pytest.raises(ConfigError, match="Prompts file not found"):
load_config(path)

40
tests/test_prompts.py Normal file
View File

@@ -0,0 +1,40 @@
from pathlib import Path
import pytest
from llm_trade_bot.prompts import PromptError, PromptStore
def test_loads_named_markdown_sections(tmp_path: Path):
path = tmp_path / "prompts.md"
path.write_text(
"# Prompts\n\n## master_prompt\nMaster text.\n\n### Nested heading\nMore.\n\n## run_prompt\nRun text.\n",
encoding="utf-8",
)
prompts = PromptStore(path).load()
assert prompts.master_prompt == "Master text.\n\n### Nested heading\nMore."
assert prompts.run_prompt == "Run text."
def test_reloads_file_for_each_snapshot(tmp_path: Path):
path = tmp_path / "prompts.md"
path.write_text("## master_prompt\nFirst\n## run_prompt\nRun", encoding="utf-8")
store = PromptStore(path)
assert store.load().master_prompt == "First"
path.write_text("## master_prompt\nSecond\n## run_prompt\nRun", encoding="utf-8")
assert store.load().master_prompt == "Second"
@pytest.mark.parametrize(
"content, message",
[
("# no sections", "level-two sections"),
("## master_prompt\nMaster", "run_prompt"),
("## master_prompt\nOne\n## master_prompt\nTwo\n## run_prompt\nRun", "Duplicate"),
],
)
def test_rejects_invalid_prompt_files(tmp_path: Path, content: str, message: str):
path = tmp_path / "prompts.md"
path.write_text(content, encoding="utf-8")
with pytest.raises(PromptError, match=message):
PromptStore(path).load()