refactor(telegram): every user-facing string lives in TelegramTexts with English defaults
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Telegram frontend (§3.8): General = master, topic = branch, drafts, inbox/outbox."""
|
||||
"""Telegram frontend: General = master, topic = branch, drafts, inbox/outbox."""
|
||||
|
||||
from beaver_gateway.frontends.telegram.frontend import Attachments, TelegramFrontend
|
||||
from beaver_gateway.frontends.telegram.texts import TelegramTexts
|
||||
|
||||
__all__ = ["Attachments", "TelegramFrontend"]
|
||||
__all__ = ["Attachments", "TelegramFrontend", "TelegramTexts"]
|
||||
|
||||
@@ -43,7 +43,7 @@ class Draft:
|
||||
thread_id: int | None,
|
||||
turn_id: str,
|
||||
interval: float = 0.7,
|
||||
status: str = "⏳ думаю",
|
||||
status: str = "⏳ thinking",
|
||||
) -> None:
|
||||
self._bot = bot
|
||||
self._chat_id = chat_id
|
||||
|
||||
@@ -42,6 +42,7 @@ from beaver_gateway.frontends.telegram.drafts import Draft
|
||||
from beaver_gateway.frontends.telegram.inbox import Inbox
|
||||
from beaver_gateway.frontends.telegram.outbox import Outbox
|
||||
from beaver_gateway.frontends.telegram.render import chunks, status_label
|
||||
from beaver_gateway.frontends.telegram.texts import TelegramTexts
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver_gateway.conversations.kinds import Kind
|
||||
@@ -56,17 +57,7 @@ _log = logging.getLogger("beaver_gateway.frontends.telegram")
|
||||
|
||||
FRONTEND = "telegram"
|
||||
_DONE = "done"
|
||||
_COMMAND_HELP = (
|
||||
("merge", "слить ветку в мастер"),
|
||||
("new", "новая ветка: в General - новый топик, в топике - заново на нём"),
|
||||
("chat", "открыть глубокий чат в vault: /chat тема"),
|
||||
("status", "что с этим разговором"), # noqa: RUF001
|
||||
("help", "команды"),
|
||||
)
|
||||
_COMMANDS = (*(c for c, _ in _COMMAND_HELP), "start")
|
||||
_HELP = "General - мастер, любой другой топик - ветка.\n" + "\n".join(
|
||||
f"/{c} - {d}" for c, d in _COMMAND_HELP
|
||||
)
|
||||
_COMMANDS = ("merge", "new", "chat", "status", "help", "start")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -131,8 +122,10 @@ class TelegramFrontend(Frontend):
|
||||
queued_reaction: str = "👀",
|
||||
poll_timeout: int = 30,
|
||||
outbox_backoff: float = 2.0,
|
||||
texts: TelegramTexts | None = None,
|
||||
) -> None:
|
||||
self._token = token
|
||||
self.texts = texts or TelegramTexts()
|
||||
self.user_id = user_id
|
||||
self.chat_id = chat_id if chat_id is not None else user_id
|
||||
self.master_agent = master_agent
|
||||
@@ -184,7 +177,7 @@ class TelegramFrontend(Frontend):
|
||||
getattr(me, "has_topics_enabled", None),
|
||||
)
|
||||
await self.bot.set_my_commands(
|
||||
[BotCommand(command=c, description=d) for c, d in _COMMAND_HELP]
|
||||
[BotCommand(command=c, description=d) for c, d in self.texts.commands]
|
||||
)
|
||||
try:
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
@@ -205,7 +198,7 @@ class TelegramFrontend(Frontend):
|
||||
if conv.kind != "branch":
|
||||
return None
|
||||
topic = await self.bot.create_forum_topic(
|
||||
self.chat_id, name=(conv.title or "ветка")[:128]
|
||||
self.chat_id, name=(conv.title or self.texts.branch)[:128]
|
||||
)
|
||||
self._topic_names[topic.message_thread_id] = topic.name
|
||||
return await self.conversations.bind(
|
||||
@@ -215,12 +208,13 @@ class TelegramFrontend(Frontend):
|
||||
async def mark_closed(self, conv: Conversation) -> bool:
|
||||
return await self.mark_topic(conv)
|
||||
|
||||
async def mark_topic(self, conv: Conversation, prefix: str = "✅ ") -> bool:
|
||||
async def mark_topic(self, conv: Conversation, prefix: str | None = None) -> bool:
|
||||
"""Rename the topic; closeForumTopic does not exist in private chats."""
|
||||
prefix = self.texts.closed_prefix if prefix is None else prefix
|
||||
target = await self._target_of(conv)
|
||||
if target is None or target[1] is None:
|
||||
return False
|
||||
name = self._topic_names.get(target[1]) or conv.title or "ветка"
|
||||
name = self._topic_names.get(target[1]) or conv.title or self.texts.branch
|
||||
if name.startswith(prefix):
|
||||
return True
|
||||
await self.bot.edit_forum_topic(
|
||||
@@ -353,7 +347,9 @@ class TelegramFrontend(Frontend):
|
||||
kind="branch",
|
||||
seed="morning",
|
||||
parent=master,
|
||||
title=(title or self._topic_names.get(thread_id) or "ветка")[:128],
|
||||
title=(title or self._topic_names.get(thread_id) or self.texts.branch)[
|
||||
:128
|
||||
],
|
||||
text=text,
|
||||
origin=FRONTEND,
|
||||
binding=(FRONTEND, self._ext(thread_id)),
|
||||
@@ -423,7 +419,9 @@ class TelegramFrontend(Frontend):
|
||||
return
|
||||
pending = self.conversations.pending_question(conv.external_id)
|
||||
if pending is not None and self.conversations.answer(pending[0], text):
|
||||
await self._close_ask(pending[0], f"✍️ {text}")
|
||||
await self._close_ask(
|
||||
pending[0], self.texts.question_typed.format(text=text)
|
||||
)
|
||||
return
|
||||
item = await self.conversations.post(conv, text, origin=FRONTEND)
|
||||
if conv.running_turn or conv.pending_question:
|
||||
@@ -474,52 +472,50 @@ class TelegramFrontend(Frontend):
|
||||
name: str | None = None
|
||||
kind = ""
|
||||
size: int | None = None
|
||||
texts = self.texts
|
||||
if message.sticker:
|
||||
return "[вложение: стикер - не поддерживается]"
|
||||
return texts.attachment_sticker
|
||||
if message.animation:
|
||||
return "[вложение: анимация - не поддерживается]"
|
||||
return texts.attachment_animation
|
||||
if message.photo:
|
||||
photo = message.photo[-1]
|
||||
file_id, kind, size = photo.file_id, "фото", photo.file_size
|
||||
file_id, kind, size = photo.file_id, "photo", photo.file_size
|
||||
name = f"{photo.file_unique_id}.jpg"
|
||||
elif message.document:
|
||||
doc = message.document
|
||||
file_id, kind, size = doc.file_id, "файл", doc.file_size
|
||||
file_id, kind, size = doc.file_id, "document", doc.file_size
|
||||
name = doc.file_name or f"{doc.file_unique_id}.bin"
|
||||
elif message.voice:
|
||||
file_id, kind, size = (
|
||||
message.voice.file_id,
|
||||
"голосовое",
|
||||
"voice",
|
||||
message.voice.file_size,
|
||||
)
|
||||
name = f"{message.voice.file_unique_id}.ogg"
|
||||
elif message.audio:
|
||||
file_id, kind, size = (
|
||||
message.audio.file_id,
|
||||
"аудио",
|
||||
"audio",
|
||||
message.audio.file_size,
|
||||
)
|
||||
name = message.audio.file_name or f"{message.audio.file_unique_id}.mp3"
|
||||
elif message.video:
|
||||
file_id, kind, size = (
|
||||
message.video.file_id,
|
||||
"видео",
|
||||
"video",
|
||||
message.video.file_size,
|
||||
)
|
||||
name = message.video.file_name or f"{message.video.file_unique_id}.mp4"
|
||||
elif message.video_note:
|
||||
file_id, kind, size = (
|
||||
message.video_note.file_id,
|
||||
"кружок",
|
||||
message.video_note.file_size,
|
||||
)
|
||||
name = f"{message.video_note.file_unique_id}.mp4"
|
||||
note = message.video_note
|
||||
file_id, kind, size = note.file_id, "video_note", note.file_size
|
||||
name = f"{note.file_unique_id}.mp4"
|
||||
if file_id is None or name is None:
|
||||
return None
|
||||
kind = texts.attachment_kinds.get(kind, kind)
|
||||
if size and size > 20 * 1024 * 1024:
|
||||
return (
|
||||
f"[вложение: {kind} {name}, {size // 1024 // 1024} МБ - "
|
||||
"больше 20 МБ, Telegram не отдаёт ботам]"
|
||||
return texts.attachment_too_big.format(
|
||||
kind=kind, name=name, mb=size // 1024 // 1024
|
||||
)
|
||||
now = time.time()
|
||||
folder = self.attachments.root / self.attachments.day(now)
|
||||
@@ -533,15 +529,12 @@ class TelegramFrontend(Frontend):
|
||||
path.chmod(0o644)
|
||||
except (TelegramAPIError, OSError) as exc:
|
||||
_log.warning("attachment download failed: %s", exc)
|
||||
return f"[вложение: {kind} {name} - не скачалось: {exc}]"
|
||||
shown = f", {size // 1024} КБ" if size else ""
|
||||
return texts.attachment_failed.format(kind=kind, name=name, error=exc)
|
||||
shown = texts.attachment_size.format(kb=size // 1024) if size else ""
|
||||
keep = self.attachments.keep_days
|
||||
if keep is None:
|
||||
return f"[вложение: {kind} {path}{shown}]"
|
||||
return (
|
||||
f"[вложение: {kind} {path}{shown}; хранится {keep} дн., "
|
||||
"перенеси в vault, если нужно]"
|
||||
)
|
||||
return texts.attachment.format(kind=kind, path=path, size=shown)
|
||||
return texts.attachment_kept.format(kind=kind, path=path, size=shown, days=keep)
|
||||
|
||||
async def _sweep_loop(self, interval: float = 6 * 3600) -> None:
|
||||
while True:
|
||||
@@ -575,8 +568,9 @@ class TelegramFrontend(Frontend):
|
||||
)
|
||||
|
||||
async def _run_command(self, command: str, args: str, thread_id: int | None) -> str:
|
||||
texts = self.texts
|
||||
if command in ("start", "help"):
|
||||
return _HELP
|
||||
return texts.help
|
||||
is_master = (
|
||||
thread_id is None or self._ext(thread_id) == await self._master_ext()
|
||||
)
|
||||
@@ -585,9 +579,9 @@ class TelegramFrontend(Frontend):
|
||||
return await self._status(conv)
|
||||
if command == "merge":
|
||||
if conv is None or conv.kind != "branch":
|
||||
return "сливать нечего: это не открытая ветка"
|
||||
return texts.merge_nothing
|
||||
self._spawn_task(self._merge(conv))
|
||||
return "🔀 сливаю в мастер"
|
||||
return texts.merging
|
||||
if command == "new":
|
||||
if is_master or thread_id is None:
|
||||
child = await self.conversations.spawn(
|
||||
@@ -597,20 +591,22 @@ class TelegramFrontend(Frontend):
|
||||
title=args or None,
|
||||
origin=FRONTEND,
|
||||
)
|
||||
return f"🌿 ветка «{child.title or child.external_id}» - в новом топике"
|
||||
return texts.new_branch_topic.format(
|
||||
title=child.title or child.external_id
|
||||
)
|
||||
if conv is not None:
|
||||
await self.conversations.set_status(conv, "closed")
|
||||
await self._branch(thread_id, title=args or None, text=None)
|
||||
return "🌿 новая ветка на этом топике"
|
||||
return texts.new_branch_here
|
||||
if command == "chat":
|
||||
if not args:
|
||||
return "/chat <тема>"
|
||||
return texts.chat_usage
|
||||
try:
|
||||
deep = await self.conversations.spawn(
|
||||
kind="deep", seed="clean", title=args, origin=FRONTEND
|
||||
)
|
||||
except (ValueError, LookupError) as exc:
|
||||
return f"не вышло: {exc}"
|
||||
return texts.chat_failed.format(error=exc)
|
||||
where = next(
|
||||
(
|
||||
b.external_id
|
||||
@@ -619,12 +615,13 @@ class TelegramFrontend(Frontend):
|
||||
),
|
||||
deep.external_id,
|
||||
)
|
||||
return f"💬 глубокий чат: {where}"
|
||||
return _HELP
|
||||
return texts.chat_opened.format(where=where)
|
||||
return texts.help
|
||||
|
||||
async def _status(self, conv: Conversation | None) -> str:
|
||||
texts = self.texts
|
||||
if conv is None:
|
||||
return "этот топик ни к чему не привязан - напиши, и откроется ветка"
|
||||
return texts.status_unbound
|
||||
info = await self.conversations.describe(conv)
|
||||
queued = sum(
|
||||
1
|
||||
@@ -635,16 +632,22 @@ class TelegramFrontend(Frontend):
|
||||
)
|
||||
lines = [
|
||||
f"{conv.kind} · {conv.status} · {conv.agent_name}",
|
||||
f"сессия: {'живая' if info['live'] else 'нет'}"
|
||||
f" · тёрн: {'идёт' if conv.running_turn else 'нет'}"
|
||||
f" · в очереди: {queued}",
|
||||
texts.status_line.format(
|
||||
live=texts.status_live if info["live"] else texts.status_dead,
|
||||
turn=texts.status_running if conv.running_turn else texts.status_idle,
|
||||
queued=queued,
|
||||
),
|
||||
]
|
||||
if conv.pending_question:
|
||||
lines.append("❓ ждёт ответа на вопрос")
|
||||
lines.append(texts.status_question)
|
||||
if conv.kind == "master":
|
||||
pool = self.conversations.pool
|
||||
lines.append(f"пул: {len(pool)} сессий, rss {pool.rss() // (1 << 20)} МБ")
|
||||
lines.append(f"outbox: {await self.outbox.pending()} в очереди")
|
||||
lines.append(
|
||||
texts.status_pool.format(sessions=len(pool), mb=pool.rss() // (1 << 20))
|
||||
)
|
||||
lines.append(
|
||||
texts.status_outbox.format(pending=await self.outbox.pending())
|
||||
)
|
||||
lines.append(f"id: {conv.external_id}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -653,7 +656,7 @@ class TelegramFrontend(Frontend):
|
||||
await self.conversations.merge(conv)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_log.exception("merge of %s failed", conv.external_id)
|
||||
await self._deliver(conv, f"⚠️ слив не удался: {exc}")
|
||||
await self._deliver(conv, self.texts.merge_failed.format(error=exc))
|
||||
|
||||
def _spawn_task(self, coro: Any) -> None:
|
||||
task = asyncio.create_task(coro)
|
||||
@@ -698,7 +701,7 @@ class TelegramFrontend(Frontend):
|
||||
draft = self._drafts.get(key)
|
||||
if draft is not None and event.get("parent_tool_use_id") is None:
|
||||
draft.set_status(
|
||||
f"⏳ {status_label(str(event['name']), event.get('input'))}"
|
||||
self._tool_status(event["name"], event.get("input"))
|
||||
)
|
||||
case "turn.end":
|
||||
draft = self._drafts.get(key)
|
||||
@@ -708,9 +711,9 @@ class TelegramFrontend(Frontend):
|
||||
origin = event.get("origin")
|
||||
await self._deliver(
|
||||
conv,
|
||||
"⚠️ тёрн упал, смотри логи gateway"
|
||||
self.texts.turn_failed
|
||||
if origin == "user"
|
||||
else f"⚠️ фоновый тёрн ({origin}) упал, смотри логи gateway",
|
||||
else self.texts.background_failed.format(origin=origin),
|
||||
turn_id=event.get("turn_id"),
|
||||
key=f"{event.get('turn_id')}:error",
|
||||
)
|
||||
@@ -724,14 +727,17 @@ class TelegramFrontend(Frontend):
|
||||
await self._ask(conv, event, target)
|
||||
case "question.answered":
|
||||
await self._close_ask(
|
||||
str(event["question_id"]), f"✅ {event.get('answer') or ''}"
|
||||
str(event["question_id"]),
|
||||
self.texts.question_answered.format(
|
||||
answer=event.get("answer") or ""
|
||||
),
|
||||
)
|
||||
case "question.timeout":
|
||||
await self._close_ask(
|
||||
str(event["question_id"]), "⌛ время вышло - ответь текстом"
|
||||
str(event["question_id"]), self.texts.question_timeout
|
||||
)
|
||||
case "conversation.merged":
|
||||
await self._deliver(conv, "✅ слито в мастер", key=f"{key}:merged")
|
||||
await self._deliver(conv, self.texts.merged, key=f"{key}:merged")
|
||||
|
||||
async def _on_delivery_failed(self, event: Event) -> None:
|
||||
row = event.get("conversation_row")
|
||||
@@ -778,7 +784,7 @@ class TelegramFrontend(Frontend):
|
||||
if user_text:
|
||||
await self._deliver(
|
||||
conv,
|
||||
f"📝 из панели:\n{user_text}",
|
||||
self.texts.mirrored.format(text=user_text),
|
||||
turn_id=turn_id,
|
||||
key=f"{turn_id}:mirror",
|
||||
)
|
||||
@@ -798,6 +804,7 @@ class TelegramFrontend(Frontend):
|
||||
thread_id=target[1],
|
||||
turn_id=str(event.get("turn_id") or key),
|
||||
interval=self.draft_interval,
|
||||
status=self.texts.waiting,
|
||||
)
|
||||
self._drafts[key] = draft
|
||||
draft.start()
|
||||
@@ -811,16 +818,23 @@ class TelegramFrontend(Frontend):
|
||||
if kind == "content_block_delta":
|
||||
delta = raw.get("delta") or {}
|
||||
if delta.get("type") == "text_delta":
|
||||
draft.set_status("✍️ пишу")
|
||||
draft.set_status(self.texts.writing)
|
||||
draft.append(str(delta.get("text") or ""))
|
||||
elif delta.get("type") == "thinking_delta":
|
||||
draft.set_status("🤔 думаю")
|
||||
draft.set_status(self.texts.thinking)
|
||||
elif kind == "content_block_start":
|
||||
block = raw.get("content_block") or {}
|
||||
if block.get("type") == "tool_use":
|
||||
draft.set_status(
|
||||
f"⏳ {status_label(str(block.get('name') or ''), None)}"
|
||||
)
|
||||
draft.set_status(self._tool_status(block.get("name") or "", None))
|
||||
|
||||
def _tool_status(self, name: Any, tool_input: Any) -> str:
|
||||
label = status_label(str(name), tool_input, self.texts.tool_labels)
|
||||
return self.texts.tool.format(label=label)
|
||||
|
||||
def _keyboard(
|
||||
self, question_id: str, qi: int, question: dict[str, Any], picked: list[str]
|
||||
) -> InlineKeyboardMarkup:
|
||||
return _keyboard(question_id, qi, question, picked, done=self.texts.done_button)
|
||||
|
||||
async def _close_draft(self, key: str) -> None:
|
||||
draft = self._drafts.pop(key, None)
|
||||
@@ -850,7 +864,7 @@ class TelegramFrontend(Frontend):
|
||||
_question_html(question),
|
||||
message_thread_id=target[1],
|
||||
parse_mode="HTML",
|
||||
reply_markup=_keyboard(question_id, qi, question, []),
|
||||
reply_markup=self._keyboard(question_id, qi, question, []),
|
||||
)
|
||||
except TelegramAPIError:
|
||||
_log.exception("question %s could not be sent", question_id)
|
||||
@@ -866,7 +880,7 @@ class TelegramFrontend(Frontend):
|
||||
_, question_id, qi_raw, choice = parts
|
||||
ask = self._asks.get(question_id)
|
||||
if ask is None or not qi_raw.isdigit():
|
||||
await self._callback_reply(query, "вопрос уже закрыт")
|
||||
await self._callback_reply(query, self.texts.question_closed)
|
||||
await self._strip_keyboard(query)
|
||||
return
|
||||
qi = int(qi_raw)
|
||||
@@ -901,14 +915,14 @@ class TelegramFrontend(Frontend):
|
||||
await self.bot.edit_message_reply_markup(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
reply_markup=_keyboard(question_id, qi, question, picked),
|
||||
reply_markup=self._keyboard(question_id, qi, question, picked),
|
||||
)
|
||||
await self._callback_reply(query, None)
|
||||
if len(ask.done) == len(ask.questions):
|
||||
answer = _answer_text(ask)
|
||||
self._asks.pop(question_id, None)
|
||||
if not self.conversations.answer(question_id, answer):
|
||||
await self._edit_asks(ask, "⌛ время вышло - ответь текстом")
|
||||
await self._edit_asks(ask, self.texts.question_timeout)
|
||||
|
||||
async def _strip_keyboard(self, query: CallbackQuery) -> None:
|
||||
message = query.message if isinstance(query.message, Message) else None
|
||||
@@ -932,7 +946,7 @@ class TelegramFrontend(Frontend):
|
||||
|
||||
async def _edit_asks(self, ask: _Ask, note: str) -> None:
|
||||
for qi, message_id in enumerate(ask.messages):
|
||||
if qi in ask.done and not note.startswith("⌛"):
|
||||
if qi in ask.done and note != self.texts.question_timeout:
|
||||
continue
|
||||
with contextlib.suppress(TelegramAPIError):
|
||||
await self.bot.edit_message_text(
|
||||
@@ -955,7 +969,7 @@ def _question_html(question: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def _keyboard(
|
||||
question_id: str, qi: int, question: dict[str, Any], picked: list[str]
|
||||
question_id: str, qi: int, question: dict[str, Any], picked: list[str], *, done: str
|
||||
) -> InlineKeyboardMarkup:
|
||||
rows = [
|
||||
[
|
||||
@@ -971,7 +985,7 @@ def _keyboard(
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="✅ готово", callback_data=f"q:{question_id}:{qi}:{_DONE}"
|
||||
text=done, callback_data=f"q:{question_id}:{qi}:{_DONE}"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -7,8 +7,10 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from beaver_gateway.frontends.telegram.texts import TOOL_LABELS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
|
||||
__all__ = ["LIMIT", "chunks", "status_label", "to_html", "to_html_tail"]
|
||||
|
||||
@@ -37,27 +39,6 @@ _ITALIC_U = re.compile(r"(?<![\w_])_(?!\s)(.+?)(?<!\s)_(?![\w_])")
|
||||
_STRIKE = re.compile(r"~~(.+?)~~")
|
||||
_SPOILER = re.compile(r"\|\|(.+?)\|\|")
|
||||
|
||||
_LABELS: dict[str, str] = {
|
||||
"Read": "читаю vault",
|
||||
"Glob": "ищу файлы",
|
||||
"Grep": "ищу в vault",
|
||||
"Edit": "правлю файл",
|
||||
"Write": "пишу файл",
|
||||
"MultiEdit": "правлю файлы",
|
||||
"Bash": "выполняю команду",
|
||||
"WebSearch": "ищу в сети",
|
||||
"WebFetch": "читаю страницу",
|
||||
"Task": "запустил сабагента",
|
||||
"Agent": "запустил сабагента",
|
||||
"AskUserQuestion": "спрашиваю",
|
||||
"TodoWrite": "планирую",
|
||||
"Skill": "открываю скилл",
|
||||
"mcp__gateway__spawn": "открываю разговор",
|
||||
"mcp__gateway__read_conversation": "читаю разговор",
|
||||
"mcp__gateway__say": "говорю",
|
||||
"mcp__gateway__schedule": "ставлю напоминание",
|
||||
"mcp__gateway__inject": "передаю в другой разговор",
|
||||
}
|
||||
|
||||
_Kind = Literal["fence", "quote", "table", "text"]
|
||||
|
||||
@@ -100,8 +81,12 @@ def chunks(text: str, limit: int = LIMIT) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def status_label(name: str, tool_input: dict[str, Any] | None = None) -> str:
|
||||
label = _LABELS.get(name)
|
||||
def status_label(
|
||||
name: str,
|
||||
tool_input: dict[str, Any] | None = None,
|
||||
labels: Mapping[str, str] | None = None,
|
||||
) -> str:
|
||||
label = (labels if labels is not None else TOOL_LABELS).get(name)
|
||||
if label is not None:
|
||||
return label
|
||||
if name.startswith("mcp__"):
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Every string the Telegram frontend shows the user; English defaults, overridable."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
__all__ = ["TOOL_LABELS", "TelegramTexts"]
|
||||
|
||||
TOOL_LABELS: dict[str, str] = {
|
||||
"Read": "reading",
|
||||
"Glob": "looking for files",
|
||||
"Grep": "searching",
|
||||
"Edit": "editing a file",
|
||||
"Write": "writing a file",
|
||||
"MultiEdit": "editing files",
|
||||
"Bash": "running a command",
|
||||
"WebSearch": "searching the web",
|
||||
"WebFetch": "reading a page",
|
||||
"Task": "started a subagent",
|
||||
"Agent": "started a subagent",
|
||||
"AskUserQuestion": "asking",
|
||||
"TodoWrite": "planning",
|
||||
"Skill": "opening a skill",
|
||||
"mcp__gateway__spawn": "opening a conversation",
|
||||
"mcp__gateway__read_conversation": "reading a conversation",
|
||||
"mcp__gateway__say": "speaking",
|
||||
"mcp__gateway__schedule": "setting a reminder",
|
||||
"mcp__gateway__inject": "passing to another conversation",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TelegramTexts:
|
||||
commands: tuple[tuple[str, str], ...] = (
|
||||
("merge", "merge this branch into the master"),
|
||||
("new", "new branch: in General a new topic, in a topic a fresh one here"),
|
||||
("chat", "open a deep chat in the vault: /chat topic"),
|
||||
("status", "this conversation"),
|
||||
("help", "commands"),
|
||||
)
|
||||
help_head: str = "General is the master, any other topic is a branch."
|
||||
branch: str = "branch"
|
||||
closed_prefix: str = "✅ "
|
||||
waiting: str = "⏳ thinking"
|
||||
thinking: str = "🤔 thinking"
|
||||
writing: str = "✍️ writing"
|
||||
tool: str = "⏳ {label}"
|
||||
tool_labels: dict[str, str] = field(default_factory=lambda: dict(TOOL_LABELS))
|
||||
attachment_sticker: str = "[attachment: sticker - not supported]"
|
||||
attachment_animation: str = "[attachment: animation - not supported]"
|
||||
attachment_kinds: dict[str, str] = field(
|
||||
default_factory=lambda: {
|
||||
"photo": "photo",
|
||||
"document": "file",
|
||||
"voice": "voice message",
|
||||
"audio": "audio",
|
||||
"video": "video",
|
||||
"video_note": "video note",
|
||||
}
|
||||
)
|
||||
attachment_too_big: str = (
|
||||
"[attachment: {kind} {name}, {mb} MB - over 20 MB, Telegram does not "
|
||||
"serve it to bots]"
|
||||
)
|
||||
attachment_failed: str = "[attachment: {kind} {name} - download failed: {error}]"
|
||||
attachment_size: str = ", {kb} KB"
|
||||
attachment: str = "[attachment: {kind} {path}{size}]"
|
||||
attachment_kept: str = (
|
||||
"[attachment: {kind} {path}{size}; kept {days} days, move it into the "
|
||||
"vault if it matters]"
|
||||
)
|
||||
merge_nothing: str = "nothing to merge: this is not an open branch"
|
||||
merging: str = "🔀 merging into the master"
|
||||
merged: str = "✅ merged into the master"
|
||||
merge_failed: str = "⚠️ merge failed: {error}"
|
||||
new_branch_topic: str = "🌿 branch «{title}» - in a new topic"
|
||||
new_branch_here: str = "🌿 new branch on this topic"
|
||||
chat_usage: str = "/chat <topic>"
|
||||
chat_failed: str = "could not open: {error}"
|
||||
chat_opened: str = "💬 deep chat: {where}"
|
||||
status_unbound: str = "this topic is bound to nothing - write, and a branch opens"
|
||||
status_line: str = "session: {live} · turn: {turn} · queued: {queued}"
|
||||
status_live: str = "live"
|
||||
status_dead: str = "none"
|
||||
status_running: str = "running"
|
||||
status_idle: str = "idle"
|
||||
status_question: str = "❓ waiting for an answer"
|
||||
status_pool: str = "pool: {sessions} sessions, rss {mb} MB"
|
||||
status_outbox: str = "outbox: {pending} queued"
|
||||
turn_failed: str = "⚠️ the turn failed, see the gateway logs"
|
||||
background_failed: str = (
|
||||
"⚠️ a background turn ({origin}) failed, see the gateway logs"
|
||||
)
|
||||
question_closed: str = "the question is already closed"
|
||||
question_timeout: str = "⌛ time is up - answer as text"
|
||||
question_answered: str = "✅ {answer}"
|
||||
question_typed: str = "✍️ {text}"
|
||||
done_button: str = "✅ done"
|
||||
mirrored: str = "📝 from the panel:\n{text}"
|
||||
|
||||
@property
|
||||
def help(self) -> str:
|
||||
return (
|
||||
self.help_head + "\n" + "\n".join(f"/{c} - {d}" for c, d in self.commands)
|
||||
)
|
||||
Reference in New Issue
Block a user