feat(telegram): add per-chat/topic default agents (#109)
This commit is contained in:
@@ -1445,6 +1445,86 @@ async def test_run_main_loop_persists_topic_sessions_in_project_scope(
|
||||
assert stored == ResumeToken(engine=CODEX_ENGINE, value=resume_value)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_main_loop_auto_resumes_topic_default_engine(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
state_path = tmp_path / "takopi.toml"
|
||||
topic_path = resolve_state_path(state_path)
|
||||
store = TopicStateStore(topic_path)
|
||||
await store.set_session_resume(
|
||||
123, 77, ResumeToken(engine=CODEX_ENGINE, value="resume-codex")
|
||||
)
|
||||
await store.set_session_resume(
|
||||
123, 77, ResumeToken(engine=EngineId("claude"), value="resume-claude")
|
||||
)
|
||||
await store.set_default_engine(123, 77, "claude")
|
||||
|
||||
transport = _FakeTransport()
|
||||
bot = _FakeBot()
|
||||
codex_runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
|
||||
claude_runner = ScriptRunner([Return(answer="ok")], engine=EngineId("claude"))
|
||||
router = AutoRouter(
|
||||
entries=[
|
||||
RunnerEntry(engine=codex_runner.engine, runner=codex_runner),
|
||||
RunnerEntry(engine=claude_runner.engine, runner=claude_runner),
|
||||
],
|
||||
default_engine=codex_runner.engine,
|
||||
)
|
||||
projects = ProjectsConfig(
|
||||
projects={
|
||||
"proj": ProjectConfig(
|
||||
alias="proj",
|
||||
path=tmp_path,
|
||||
worktrees_dir=Path(".worktrees"),
|
||||
chat_id=123,
|
||||
)
|
||||
},
|
||||
default_project=None,
|
||||
chat_map={123: "proj"},
|
||||
)
|
||||
runtime = TransportRuntime(
|
||||
router=router,
|
||||
projects=projects,
|
||||
config_path=state_path,
|
||||
)
|
||||
cfg = TelegramBridgeConfig(
|
||||
bot=bot,
|
||||
runtime=runtime,
|
||||
chat_id=123,
|
||||
startup_msg="",
|
||||
exec_cfg=ExecBridgeConfig(
|
||||
transport=transport,
|
||||
presenter=MarkdownPresenter(),
|
||||
final_notify=True,
|
||||
),
|
||||
topics=TelegramTopicsSettings(
|
||||
enabled=True,
|
||||
scope="main",
|
||||
),
|
||||
)
|
||||
|
||||
async def poller(_cfg: TelegramBridgeConfig):
|
||||
yield TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=123,
|
||||
message_id=1,
|
||||
text="hello",
|
||||
reply_to_message_id=None,
|
||||
reply_to_text=None,
|
||||
sender_id=123,
|
||||
thread_id=77,
|
||||
)
|
||||
|
||||
await run_main_loop(cfg, poller)
|
||||
|
||||
assert codex_runner.calls == []
|
||||
assert len(claude_runner.calls) == 1
|
||||
assert claude_runner.calls[0][1] == ResumeToken(
|
||||
engine=EngineId("claude"), value="resume-claude"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_main_loop_auto_resumes_chat_sessions(tmp_path: Path) -> None:
|
||||
resume_value = "resume-123"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
|
||||
from takopi.telegram.chat_prefs import ChatPrefsStore
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_chat_prefs_store_roundtrip(tmp_path) -> None:
|
||||
path = tmp_path / "telegram_chat_prefs_state.json"
|
||||
store = ChatPrefsStore(path)
|
||||
await store.set_default_engine(123, "codex")
|
||||
await store.set_default_engine(123, "codex")
|
||||
await store.clear_default_engine(456)
|
||||
|
||||
assert await store.get_default_engine(123) == "codex"
|
||||
|
||||
store2 = ChatPrefsStore(path)
|
||||
assert await store2.get_default_engine(123) == "codex"
|
||||
|
||||
await store2.clear_default_engine(123)
|
||||
assert await store2.get_default_engine(123) is None
|
||||
@@ -0,0 +1,78 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from takopi.config import ProjectConfig, ProjectsConfig
|
||||
from takopi.context import RunContext
|
||||
from takopi.model import EngineId
|
||||
from takopi.router import AutoRouter, RunnerEntry
|
||||
from takopi.runners.mock import Return, ScriptRunner
|
||||
from takopi.telegram.chat_prefs import ChatPrefsStore
|
||||
from takopi.telegram.engine_defaults import resolve_engine_for_message
|
||||
from takopi.telegram.topic_state import TopicStateStore
|
||||
from takopi.transport_runtime import TransportRuntime
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_resolve_engine_for_message_sources(tmp_path) -> None:
|
||||
codex = ScriptRunner([Return(answer="ok")], engine=EngineId("codex"))
|
||||
pi = ScriptRunner([Return(answer="ok")], engine=EngineId("pi"))
|
||||
router = AutoRouter(
|
||||
entries=[
|
||||
RunnerEntry(engine=codex.engine, runner=codex),
|
||||
RunnerEntry(engine=pi.engine, runner=pi),
|
||||
],
|
||||
default_engine=codex.engine,
|
||||
)
|
||||
project = ProjectConfig(
|
||||
alias="proj",
|
||||
path=tmp_path,
|
||||
worktrees_dir=Path(".worktrees"),
|
||||
default_engine=pi.engine,
|
||||
)
|
||||
runtime = TransportRuntime(
|
||||
router=router,
|
||||
projects=ProjectsConfig(projects={"proj": project}, default_project=None),
|
||||
)
|
||||
chat_prefs = ChatPrefsStore(tmp_path / "telegram_chat_prefs_state.json")
|
||||
topic_store = TopicStateStore(tmp_path / "telegram_topics_state.json")
|
||||
await chat_prefs.set_default_engine(1, "pi")
|
||||
await topic_store.set_default_engine(1, 10, "codex")
|
||||
|
||||
resolved = await resolve_engine_for_message(
|
||||
runtime=runtime,
|
||||
context=RunContext(project="proj"),
|
||||
explicit_engine=EngineId("codex"),
|
||||
chat_id=1,
|
||||
topic_key=(1, 10),
|
||||
topic_store=topic_store,
|
||||
chat_prefs=chat_prefs,
|
||||
)
|
||||
assert resolved.source == "directive"
|
||||
assert resolved.engine == "codex"
|
||||
|
||||
await topic_store.clear_default_engine(1, 10)
|
||||
resolved = await resolve_engine_for_message(
|
||||
runtime=runtime,
|
||||
context=RunContext(project="proj"),
|
||||
explicit_engine=None,
|
||||
chat_id=1,
|
||||
topic_key=(1, 10),
|
||||
topic_store=topic_store,
|
||||
chat_prefs=chat_prefs,
|
||||
)
|
||||
assert resolved.source == "chat_default"
|
||||
assert resolved.engine == "pi"
|
||||
|
||||
await chat_prefs.clear_default_engine(1)
|
||||
resolved = await resolve_engine_for_message(
|
||||
runtime=runtime,
|
||||
context=RunContext(project="proj"),
|
||||
explicit_engine=None,
|
||||
chat_id=1,
|
||||
topic_key=(1, 10),
|
||||
topic_store=topic_store,
|
||||
chat_prefs=chat_prefs,
|
||||
)
|
||||
assert resolved.source == "project_default"
|
||||
assert resolved.engine == "pi"
|
||||
@@ -11,18 +11,21 @@ async def test_topic_state_store_roundtrip(tmp_path) -> None:
|
||||
store = TopicStateStore(path)
|
||||
context = RunContext(project="proj", branch="feat/topic")
|
||||
await store.set_context(1, 10, context)
|
||||
await store.set_default_engine(1, 10, "claude")
|
||||
await store.set_session_resume(1, 10, ResumeToken(engine="codex", value="abc123"))
|
||||
|
||||
snapshot = await store.get_thread(1, 10)
|
||||
assert snapshot is not None
|
||||
assert snapshot.context == context
|
||||
assert snapshot.sessions == {"codex": "abc123"}
|
||||
assert snapshot.default_engine == "claude"
|
||||
|
||||
store2 = TopicStateStore(path)
|
||||
snapshot2 = await store2.get_thread(1, 10)
|
||||
assert snapshot2 is not None
|
||||
assert snapshot2.context == context
|
||||
assert snapshot2.sessions == {"codex": "abc123"}
|
||||
assert snapshot2.default_engine == "claude"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -47,3 +50,7 @@ async def test_topic_state_store_clear_and_find(tmp_path) -> None:
|
||||
snapshot = await store.get_thread(2, 20)
|
||||
assert snapshot is not None
|
||||
assert snapshot.context is None
|
||||
await store.clear_default_engine(2, 20)
|
||||
snapshot = await store.get_thread(2, 20)
|
||||
assert snapshot is not None
|
||||
assert snapshot.default_engine is None
|
||||
|
||||
Reference in New Issue
Block a user