fix(telegram): handle forwarded uploads and show overrides (#149)
This commit is contained in:
@@ -38,6 +38,7 @@ This line is parsed from replies and takes precedence over new directives.
|
||||
| `/agent` | Show/set the default agent for the current scope. |
|
||||
| `/model` | Show/set the model override for the current scope. |
|
||||
| `/reasoning` | Show/set the reasoning override for the current scope. |
|
||||
| `/trigger` | Show/set trigger mode (mentions-only vs all). |
|
||||
| `/file put <path>` | Upload a document into the repo/worktree (requires file transfer enabled). |
|
||||
| `/file get <path>` | Fetch a file or directory back into Telegram. |
|
||||
| `/topic <project> @branch` | Create/bind a topic (topics enabled). |
|
||||
|
||||
@@ -6,6 +6,7 @@ from ...context import RunContext
|
||||
from ...directives import DirectiveError
|
||||
from ..chat_prefs import ChatPrefsStore
|
||||
from ..engine_defaults import resolve_engine_for_message
|
||||
from ..engine_overrides import resolve_override_value
|
||||
from ..files import split_command_args
|
||||
from ..topic_state import TopicStateStore
|
||||
from ..topics import _topic_key
|
||||
@@ -89,6 +90,40 @@ async def _handle_agent_command(
|
||||
"global_default": "global default",
|
||||
}
|
||||
agent_line = f"agent: {selection.engine} ({source_labels[selection.source]})"
|
||||
topic_override = None
|
||||
if tkey is not None and topic_store is not None:
|
||||
topic_override = await topic_store.get_engine_override(
|
||||
tkey[0], tkey[1], selection.engine
|
||||
)
|
||||
chat_override = None
|
||||
if chat_prefs is not None:
|
||||
chat_override = await chat_prefs.get_engine_override(
|
||||
msg.chat_id, selection.engine
|
||||
)
|
||||
override_labels = {
|
||||
"topic_override": "topic override",
|
||||
"chat_default": "chat default",
|
||||
"default": "no override",
|
||||
}
|
||||
model_resolution = resolve_override_value(
|
||||
topic_override=topic_override,
|
||||
chat_override=chat_override,
|
||||
field="model",
|
||||
)
|
||||
reasoning_resolution = resolve_override_value(
|
||||
topic_override=topic_override,
|
||||
chat_override=chat_override,
|
||||
field="reasoning",
|
||||
)
|
||||
model_value = model_resolution.value or "default"
|
||||
model_line = (
|
||||
f"model: {model_value} ({override_labels[model_resolution.source]})"
|
||||
)
|
||||
reasoning_value = reasoning_resolution.value or "default"
|
||||
reasoning_line = (
|
||||
"reasoning: "
|
||||
f"{reasoning_value} ({override_labels[reasoning_resolution.source]})"
|
||||
)
|
||||
topic_default = selection.topic_default or "none"
|
||||
if tkey is None:
|
||||
topic_default = "none"
|
||||
@@ -110,7 +145,11 @@ async def _handle_agent_command(
|
||||
)
|
||||
available = ", ".join(cfg.runtime.engine_ids)
|
||||
available_line = f"available: {available}"
|
||||
await reply(text="\n\n".join([agent_line, defaults_line, available_line]))
|
||||
await reply(
|
||||
text="\n\n".join(
|
||||
[agent_line, model_line, reasoning_line, defaults_line, available_line]
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if action == "set":
|
||||
|
||||
@@ -8,7 +8,6 @@ import msgspec
|
||||
OverrideSource = Literal["topic_override", "chat_default", "default"]
|
||||
|
||||
REASONING_LEVELS: tuple[str, ...] = ("minimal", "low", "medium", "high", "xhigh")
|
||||
OPENCODE_REASONING_LEVELS: tuple[str, ...] = ("none", *REASONING_LEVELS)
|
||||
REASONING_SUPPORTED_ENGINES = frozenset({"codex"})
|
||||
|
||||
|
||||
@@ -98,8 +97,7 @@ def resolve_override_value(
|
||||
|
||||
|
||||
def allowed_reasoning_levels(engine: str) -> tuple[str, ...]:
|
||||
if engine == "opencode":
|
||||
return OPENCODE_REASONING_LEVELS
|
||||
_ = engine
|
||||
return REASONING_LEVELS
|
||||
|
||||
|
||||
|
||||
@@ -1283,7 +1283,13 @@ async def run_main_loop(
|
||||
reply = make_reply(cfg, msg)
|
||||
text = msg.text
|
||||
is_voice_transcribed = False
|
||||
if _is_forwarded(msg.raw):
|
||||
is_forward_candidate = (
|
||||
_is_forwarded(msg.raw)
|
||||
and msg.document is None
|
||||
and msg.voice is None
|
||||
and msg.media_group_id is None
|
||||
)
|
||||
if is_forward_candidate:
|
||||
_attach_forward(msg)
|
||||
continue
|
||||
forward_key = _forward_key(msg)
|
||||
|
||||
@@ -2372,6 +2372,84 @@ async def test_run_main_loop_ignores_forwarded_without_prompt() -> None:
|
||||
assert runner.calls == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_main_loop_forwarded_document_still_uploads(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
payload = b"hello"
|
||||
|
||||
class _UploadBot(_FakeBot):
|
||||
async def get_file(self, file_id: str) -> File | None:
|
||||
_ = file_id
|
||||
return File(file_path="files/hello.txt")
|
||||
|
||||
async def download_file(self, file_path: str) -> bytes | None:
|
||||
_ = file_path
|
||||
return payload
|
||||
|
||||
runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
|
||||
projects = ProjectsConfig(
|
||||
projects={
|
||||
"proj": ProjectConfig(
|
||||
alias="proj",
|
||||
path=tmp_path,
|
||||
worktrees_dir=Path(".worktrees"),
|
||||
)
|
||||
},
|
||||
default_project="proj",
|
||||
)
|
||||
runtime = TransportRuntime(router=_make_router(runner), projects=projects)
|
||||
transport = _FakeTransport()
|
||||
exec_cfg = ExecBridgeConfig(
|
||||
transport=transport,
|
||||
presenter=MarkdownPresenter(),
|
||||
final_notify=True,
|
||||
)
|
||||
cfg = TelegramBridgeConfig(
|
||||
bot=_UploadBot(),
|
||||
runtime=runtime,
|
||||
chat_id=123,
|
||||
startup_msg="",
|
||||
exec_cfg=exec_cfg,
|
||||
forward_coalesce_s=FAST_FORWARD_COALESCE_S,
|
||||
media_group_debounce_s=FAST_MEDIA_GROUP_DEBOUNCE_S,
|
||||
files=TelegramFilesSettings(
|
||||
enabled=True,
|
||||
auto_put=True,
|
||||
auto_put_mode="prompt",
|
||||
),
|
||||
)
|
||||
|
||||
async def poller(_cfg: TelegramBridgeConfig):
|
||||
yield TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=123,
|
||||
message_id=1,
|
||||
text="do thing",
|
||||
reply_to_message_id=None,
|
||||
reply_to_text=None,
|
||||
sender_id=123,
|
||||
chat_type="private",
|
||||
document=TelegramDocument(
|
||||
file_id="doc-1",
|
||||
file_name="hello.txt",
|
||||
mime_type="text/plain",
|
||||
file_size=len(payload),
|
||||
raw={"file_id": "doc-1"},
|
||||
),
|
||||
raw={"forward_origin": {"type": "user"}},
|
||||
)
|
||||
|
||||
await run_main_loop(cfg, poller)
|
||||
|
||||
saved_path = tmp_path / "incoming" / "hello.txt"
|
||||
assert saved_path.read_bytes() == payload
|
||||
assert runner.calls
|
||||
prompt_text, _ = runner.calls[0]
|
||||
assert prompt_text.startswith("do thing")
|
||||
assert "[uploaded file: incoming/hello.txt]" in prompt_text
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_main_loop_prompt_upload_auto_resumes_chat_sessions(
|
||||
tmp_path: Path,
|
||||
|
||||
Reference in New Issue
Block a user