feat: load editable prompts from markdown
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
40
tests/test_prompts.py
Normal 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()
|
||||
Reference in New Issue
Block a user