feat(telegram): make resume line optional (#100)
This commit is contained in:
@@ -84,6 +84,7 @@ Configuration (under `[transports.telegram]`):
|
|||||||
[transports.telegram.topics]
|
[transports.telegram.topics]
|
||||||
enabled = true
|
enabled = true
|
||||||
scope = "auto" # auto | main | projects | all
|
scope = "auto" # auto | main | projects | all
|
||||||
|
show_resume_line = true
|
||||||
```
|
```
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
@@ -95,6 +96,10 @@ Requirements:
|
|||||||
- `auto`: if any project chats are configured, uses `projects`; otherwise `main`.
|
- `auto`: if any project chats are configured, uses `projects`; otherwise `main`.
|
||||||
- The bot needs the **Manage Topics** permission in the relevant chat(s).
|
- The bot needs the **Manage Topics** permission in the relevant chat(s).
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
|
||||||
|
- `show_resume_line`: set `false` to hide the resume command line in topic threads.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
|
||||||
- `main`: `/topic <project> @branch` creates a topic in the main chat and binds it.
|
- `main`: `/topic <project> @branch` creates a topic in the main chat and binds it.
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ Topics bind Telegram forum threads to specific project/branch contexts. They als
|
|||||||
```toml
|
```toml
|
||||||
[transports.telegram.topics]
|
[transports.telegram.topics]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
# show_resume_line = false
|
||||||
```
|
```
|
||||||
|
|
||||||
Your bot needs **Manage Topics** permission in the group.
|
Your bot needs **Manage Topics** permission in the group.
|
||||||
@@ -269,6 +270,8 @@ Your bot needs **Manage Topics** permission in the group.
|
|||||||
If any `projects.<alias>.chat_id` are configured, topics are managed in those
|
If any `projects.<alias>.chat_id` are configured, topics are managed in those
|
||||||
project chats; otherwise topics are managed in the main chat.
|
project chats; otherwise topics are managed in the main chat.
|
||||||
|
|
||||||
|
Set `show_resume_line = false` to hide the resume command line in topic threads.
|
||||||
|
|
||||||
### Topic behavior
|
### Topic behavior
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -321,6 +324,7 @@ chat_id = -1001234567890
|
|||||||
|
|
||||||
[transports.telegram.topics]
|
[transports.telegram.topics]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
# show_resume_line = false
|
||||||
```
|
```
|
||||||
|
|
||||||
**Project chats:**
|
**Project chats:**
|
||||||
@@ -331,6 +335,7 @@ chat_id = 123456789 # main chat (private, for non-project messages)
|
|||||||
|
|
||||||
[transports.telegram.topics]
|
[transports.telegram.topics]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
# show_resume_line = false
|
||||||
|
|
||||||
[projects.takopi]
|
[projects.takopi]
|
||||||
path = "~/dev/takopi"
|
path = "~/dev/takopi"
|
||||||
@@ -455,6 +460,7 @@ deny_globs = [".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]
|
|||||||
[transports.telegram.topics]
|
[transports.telegram.topics]
|
||||||
enabled = true
|
enabled = true
|
||||||
scope = "auto"
|
scope = "auto"
|
||||||
|
show_resume_line = true
|
||||||
|
|
||||||
# Project definitions
|
# Project definitions
|
||||||
[projects.takopi]
|
[projects.takopi]
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class TelegramTopicsSettings(BaseModel):
|
|||||||
|
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
scope: Literal["auto", "main", "projects", "all"] = "auto"
|
scope: Literal["auto", "main", "projects", "all"] = "auto"
|
||||||
|
show_resume_line: bool = True
|
||||||
|
|
||||||
|
|
||||||
class TelegramFilesSettings(BaseModel):
|
class TelegramFilesSettings(BaseModel):
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable, Sequence
|
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ from ..directives import DirectiveError
|
|||||||
from ..ids import RESERVED_COMMAND_IDS, is_valid_id
|
from ..ids import RESERVED_COMMAND_IDS, is_valid_id
|
||||||
from ..logging import bind_run_context, clear_context, get_logger
|
from ..logging import bind_run_context, clear_context, get_logger
|
||||||
from ..markdown import MarkdownParts
|
from ..markdown import MarkdownParts
|
||||||
from ..model import EngineId, ResumeToken
|
from ..model import EngineId, ResumeToken, TakopiEvent
|
||||||
from ..plugins import COMMAND_GROUP, list_entrypoints
|
from ..plugins import COMMAND_GROUP, list_entrypoints
|
||||||
from ..progress import ProgressTracker
|
from ..progress import ProgressTracker
|
||||||
from ..router import RunnerUnavailableError
|
from ..router import RunnerUnavailableError
|
||||||
@@ -67,6 +68,7 @@ from .topics import (
|
|||||||
_maybe_update_topic_context,
|
_maybe_update_topic_context,
|
||||||
_topic_key,
|
_topic_key,
|
||||||
_topic_title,
|
_topic_title,
|
||||||
|
_topics_chat_allowed,
|
||||||
_topics_chat_project,
|
_topics_chat_project,
|
||||||
_topics_command_error,
|
_topics_command_error,
|
||||||
)
|
)
|
||||||
@@ -255,6 +257,29 @@ class _SavedFilePutGroup:
|
|||||||
failed: list[_FilePutResult]
|
failed: list[_FilePutResult]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class _ResumeLineProxy:
|
||||||
|
runner: Runner
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engine(self) -> str:
|
||||||
|
return self.runner.engine
|
||||||
|
|
||||||
|
def is_resume_line(self, line: str) -> bool:
|
||||||
|
return self.runner.is_resume_line(line)
|
||||||
|
|
||||||
|
def format_resume(self, _: ResumeToken) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def extract_resume(self, text: str | None) -> ResumeToken | None:
|
||||||
|
return self.runner.extract_resume(text)
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self, prompt: str, resume: ResumeToken | None
|
||||||
|
) -> AsyncIterator[TakopiEvent]:
|
||||||
|
return self.runner.run(prompt, resume)
|
||||||
|
|
||||||
|
|
||||||
def resolve_file_put_paths(
|
def resolve_file_put_paths(
|
||||||
plan: _FilePutPlan,
|
plan: _FilePutPlan,
|
||||||
*,
|
*,
|
||||||
@@ -1239,6 +1264,7 @@ async def _run_engine(
|
|||||||
| None = None,
|
| None = None,
|
||||||
engine_override: EngineId | None = None,
|
engine_override: EngineId | None = None,
|
||||||
thread_id: int | None = None,
|
thread_id: int | None = None,
|
||||||
|
show_resume_line: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
reply = partial(
|
reply = partial(
|
||||||
send_plain,
|
send_plain,
|
||||||
@@ -1256,6 +1282,9 @@ async def _run_engine(
|
|||||||
except RunnerUnavailableError as exc:
|
except RunnerUnavailableError as exc:
|
||||||
await reply(text=f"error:\n{exc}")
|
await reply(text=f"error:\n{exc}")
|
||||||
return
|
return
|
||||||
|
runner: Runner = entry.runner
|
||||||
|
if thread_id is not None and not show_resume_line:
|
||||||
|
runner = cast(Runner, _ResumeLineProxy(runner))
|
||||||
if not entry.available:
|
if not entry.available:
|
||||||
reason = entry.issue or "engine unavailable"
|
reason = entry.issue or "engine unavailable"
|
||||||
await _send_runner_unavailable(
|
await _send_runner_unavailable(
|
||||||
@@ -1263,7 +1292,7 @@ async def _run_engine(
|
|||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
user_msg_id=user_msg_id,
|
user_msg_id=user_msg_id,
|
||||||
resume_token=resume_token,
|
resume_token=resume_token,
|
||||||
runner=entry.runner,
|
runner=runner,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
)
|
)
|
||||||
@@ -1278,7 +1307,7 @@ async def _run_engine(
|
|||||||
run_fields = {
|
run_fields = {
|
||||||
"chat_id": chat_id,
|
"chat_id": chat_id,
|
||||||
"user_msg_id": user_msg_id,
|
"user_msg_id": user_msg_id,
|
||||||
"engine": entry.runner.engine,
|
"engine": runner.engine,
|
||||||
"resume": resume_token.value if resume_token else None,
|
"resume": resume_token.value if resume_token else None,
|
||||||
}
|
}
|
||||||
if context is not None:
|
if context is not None:
|
||||||
@@ -1297,7 +1326,7 @@ async def _run_engine(
|
|||||||
)
|
)
|
||||||
await handle_message(
|
await handle_message(
|
||||||
exec_cfg,
|
exec_cfg,
|
||||||
runner=entry.runner,
|
runner=runner,
|
||||||
incoming=incoming,
|
incoming=incoming,
|
||||||
resume_token=resume_token,
|
resume_token=resume_token,
|
||||||
context=context,
|
context=context,
|
||||||
@@ -1366,6 +1395,7 @@ class _TelegramCommandExecutor(CommandExecutor):
|
|||||||
chat_id: int,
|
chat_id: int,
|
||||||
user_msg_id: int,
|
user_msg_id: int,
|
||||||
thread_id: int | None,
|
thread_id: int | None,
|
||||||
|
show_resume_line: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._exec_cfg = exec_cfg
|
self._exec_cfg = exec_cfg
|
||||||
self._runtime = runtime
|
self._runtime = runtime
|
||||||
@@ -1374,6 +1404,7 @@ class _TelegramCommandExecutor(CommandExecutor):
|
|||||||
self._chat_id = chat_id
|
self._chat_id = chat_id
|
||||||
self._user_msg_id = user_msg_id
|
self._user_msg_id = user_msg_id
|
||||||
self._thread_id = thread_id
|
self._thread_id = thread_id
|
||||||
|
self._show_resume_line = show_resume_line
|
||||||
self._reply_ref = MessageRef(
|
self._reply_ref = MessageRef(
|
||||||
channel_id=chat_id,
|
channel_id=chat_id,
|
||||||
message_id=user_msg_id,
|
message_id=user_msg_id,
|
||||||
@@ -1443,6 +1474,7 @@ class _TelegramCommandExecutor(CommandExecutor):
|
|||||||
on_thread_known=None,
|
on_thread_known=None,
|
||||||
engine_override=engine,
|
engine_override=engine,
|
||||||
thread_id=self._thread_id,
|
thread_id=self._thread_id,
|
||||||
|
show_resume_line=self._show_resume_line,
|
||||||
)
|
)
|
||||||
return RunResult(engine=engine, message=capture.last_message)
|
return RunResult(engine=engine, message=capture.last_message)
|
||||||
await _run_engine(
|
await _run_engine(
|
||||||
@@ -1458,6 +1490,7 @@ class _TelegramCommandExecutor(CommandExecutor):
|
|||||||
on_thread_known=self._scheduler.note_thread_known,
|
on_thread_known=self._scheduler.note_thread_known,
|
||||||
engine_override=engine,
|
engine_override=engine,
|
||||||
thread_id=self._thread_id,
|
thread_id=self._thread_id,
|
||||||
|
show_resume_line=self._show_resume_line,
|
||||||
)
|
)
|
||||||
return RunResult(engine=engine, message=None)
|
return RunResult(engine=engine, message=None)
|
||||||
|
|
||||||
@@ -1504,6 +1537,8 @@ async def _dispatch_command(
|
|||||||
if msg.reply_to_message_id is not None
|
if msg.reply_to_message_id is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
topic_thread = msg.thread_id is not None and _topics_chat_allowed(cfg, msg.chat_id)
|
||||||
|
show_resume_line = cfg.topics.show_resume_line if topic_thread else True
|
||||||
executor = _TelegramCommandExecutor(
|
executor = _TelegramCommandExecutor(
|
||||||
exec_cfg=cfg.exec_cfg,
|
exec_cfg=cfg.exec_cfg,
|
||||||
runtime=cfg.runtime,
|
runtime=cfg.runtime,
|
||||||
@@ -1512,6 +1547,7 @@ async def _dispatch_command(
|
|||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
user_msg_id=user_msg_id,
|
user_msg_id=user_msg_id,
|
||||||
thread_id=msg.thread_id,
|
thread_id=msg.thread_id,
|
||||||
|
show_resume_line=show_resume_line,
|
||||||
)
|
)
|
||||||
message_ref = MessageRef(
|
message_ref = MessageRef(
|
||||||
channel_id=chat_id, message_id=user_msg_id, thread_id=msg.thread_id
|
channel_id=chat_id, message_id=user_msg_id, thread_id=msg.thread_id
|
||||||
|
|||||||
@@ -444,6 +444,9 @@ async def run_main_loop(
|
|||||||
)
|
)
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
show_resume_line = (
|
||||||
|
cfg.topics.show_resume_line if topic_key is not None else True
|
||||||
|
)
|
||||||
await _run_engine(
|
await _run_engine(
|
||||||
exec_cfg=cfg.exec_cfg,
|
exec_cfg=cfg.exec_cfg,
|
||||||
runtime=cfg.runtime,
|
runtime=cfg.runtime,
|
||||||
@@ -459,6 +462,7 @@ async def run_main_loop(
|
|||||||
),
|
),
|
||||||
engine_override=engine_override,
|
engine_override=engine_override,
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
|
show_resume_line=show_resume_line,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run_thread_job(job: ThreadJob) -> None:
|
async def run_thread_job(job: ThreadJob) -> None:
|
||||||
|
|||||||
@@ -1261,6 +1261,44 @@ async def test_send_with_resume_reports_when_missing() -> None:
|
|||||||
assert "resume token" in transport.send_calls[-1]["message"].text.lower()
|
assert "resume token" in transport.send_calls[-1]["message"].text.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_run_engine_hides_resume_line_in_topics() -> None:
|
||||||
|
transport = telegram_commands._CaptureTransport()
|
||||||
|
runner = ScriptRunner(
|
||||||
|
[Return(answer="ok")],
|
||||||
|
engine=CODEX_ENGINE,
|
||||||
|
resume_value="resume-123",
|
||||||
|
)
|
||||||
|
exec_cfg = ExecBridgeConfig(
|
||||||
|
transport=transport,
|
||||||
|
presenter=MarkdownPresenter(),
|
||||||
|
final_notify=True,
|
||||||
|
)
|
||||||
|
runtime = TransportRuntime(
|
||||||
|
router=_make_router(runner),
|
||||||
|
projects=_empty_projects(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await telegram_commands._run_engine(
|
||||||
|
exec_cfg=exec_cfg,
|
||||||
|
runtime=runtime,
|
||||||
|
running_tasks={},
|
||||||
|
chat_id=123,
|
||||||
|
user_msg_id=1,
|
||||||
|
text="hello",
|
||||||
|
resume_token=None,
|
||||||
|
context=None,
|
||||||
|
reply_ref=None,
|
||||||
|
on_thread_known=None,
|
||||||
|
engine_override=None,
|
||||||
|
thread_id=77,
|
||||||
|
show_resume_line=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert transport.last_message is not None
|
||||||
|
assert "resume-123" not in transport.last_message.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_run_main_loop_routes_reply_to_running_resume() -> None:
|
async def test_run_main_loop_routes_reply_to_running_resume() -> None:
|
||||||
progress_ready = anyio.Event()
|
progress_ready = anyio.Event()
|
||||||
|
|||||||
Reference in New Issue
Block a user