fix: support AI commands in media captions
This commit is contained in:
@@ -6,10 +6,12 @@ Repository guidance for automated coding tools and contributors.
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run crawl4ai-setup
|
||||
uv run python -m playwright install chromium
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
Do not use `crawl4ai-setup` in unprivileged containers such as Pterodactyl: it invokes Playwright with `--with-deps` and attempts to become root. Installing Chromium with the command above only writes to the current user's Playwright cache. If Chromium reports missing shared libraries at runtime, they must be included in the Docker image by its administrator.
|
||||
|
||||
Logs go to `bot.log` by default (rotating, 10 MB, 3 backups). Override with `LOG_FILE=<path>`.
|
||||
|
||||
Run the test suite with:
|
||||
@@ -53,7 +55,7 @@ On supported platforms, `main.py` runs the application with `uvloop`; Windows au
|
||||
|
||||
### Agent loop (`app/agentic.py`)
|
||||
|
||||
`.ai [count] [.ds] [.th] <prompt>` runs `micro-agents` with GLM by default. `.ds` selects DeepSeek and `.th` enables reasoning. Every session exposes the `micro-agents-tools` Brave Search tool. Unknown leading dot-arguments are ignored. The optional count adds preceding Telegram messages; a replied message is always injected once. Media at or below 5 MiB is attached. Invalid syntax and runtime/API failures only react with 👎. Guests need an ACL grant for `ai` and invoke it as `..ai`.
|
||||
`.ai [count] [.ds] [.th] <prompt>` runs `micro-agents` with GLM by default. `.ds` selects DeepSeek and `.th` enables reasoning. Every session exposes the `micro-agents-tools` Brave Search tool. Unknown leading dot-arguments are ignored. The optional count adds preceding Telegram messages; a replied message is always injected once. Commands are accepted from either message text or a media caption. Media at or below 5 MiB, including media attached directly to the command message, is attached. Invalid syntax and runtime/API failures only react with 👎. Guests need an ACL grant for `ai` and invoke it as `..ai`.
|
||||
|
||||
While an `.ai` command is running, its Telegram message has a 🤔 reaction. The reaction is removed after a successful answer and replaced with 👎 on failure.
|
||||
|
||||
|
||||
@@ -437,8 +437,14 @@ async def run_agent_turn(
|
||||
context["base_dir"] = config.base_dir
|
||||
existing.session.context.clear()
|
||||
existing.session.context.update(context)
|
||||
media = await _download_message_media(
|
||||
client,
|
||||
command_message,
|
||||
Path(existing.workspace.name),
|
||||
config.max_media_bytes,
|
||||
)
|
||||
return AgentTurnResult(
|
||||
answer=await _run_session(existing.session, invocation.prompt),
|
||||
answer=await _run_session(existing.session, invocation.prompt, media=media),
|
||||
handle=existing,
|
||||
)
|
||||
|
||||
@@ -463,6 +469,12 @@ async def run_agent_turn(
|
||||
user_template=templates.user,
|
||||
template_context=context,
|
||||
)
|
||||
media = await _download_message_media(
|
||||
client,
|
||||
command_message,
|
||||
temp_path,
|
||||
config.max_media_bytes,
|
||||
)
|
||||
tools = [
|
||||
*WebPageTools(
|
||||
timeout_seconds=config.web_page_timeout_seconds,
|
||||
@@ -501,7 +513,7 @@ async def run_agent_turn(
|
||||
session_name=f"telegram:{chat_id}",
|
||||
messages=messages,
|
||||
)
|
||||
answer = await _run_session(session, invocation.prompt)
|
||||
answer = await _run_session(session, invocation.prompt, media=media)
|
||||
return AgentTurnResult(
|
||||
answer=answer,
|
||||
handle=AgentSessionHandle(session, workspace, set()),
|
||||
@@ -511,8 +523,11 @@ async def run_agent_turn(
|
||||
raise
|
||||
|
||||
|
||||
async def _run_session(session: Any, prompt: str) -> str:
|
||||
result = await session.run(prompt)
|
||||
async def _run_session(session: Any, prompt: str, *, media: list[Media] | None = None) -> str:
|
||||
if media:
|
||||
result = await session.run(prompt, media=media)
|
||||
else:
|
||||
result = await session.run(prompt)
|
||||
if not result.succeeded or result.final_message is None:
|
||||
raise AgentError("Agent loop failed") from result.error
|
||||
answer = result.final_message.message().strip()
|
||||
|
||||
@@ -59,10 +59,12 @@ class CommandDispatcher:
|
||||
account: "AccountState",
|
||||
) -> bool:
|
||||
text = getattr(message, "text", None)
|
||||
if not isinstance(text, str):
|
||||
caption = getattr(message, "caption", None)
|
||||
command_text = text if isinstance(text, str) else caption
|
||||
if not isinstance(command_text, str):
|
||||
return False
|
||||
|
||||
parsed = parse_command(text)
|
||||
parsed = parse_command(command_text)
|
||||
if parsed is None:
|
||||
return False
|
||||
|
||||
@@ -85,7 +87,7 @@ class CommandDispatcher:
|
||||
message=message,
|
||||
command=definition,
|
||||
prefix=parsed.prefix,
|
||||
raw_text=text,
|
||||
raw_text=command_text,
|
||||
args=parsed.args,
|
||||
alias=parsed.alias,
|
||||
)
|
||||
|
||||
@@ -18,4 +18,4 @@ def register_common_handlers(client: "Client", manager: "AccountManager", accoun
|
||||
async def command_handler(client_: "Client", message) -> None:
|
||||
await dispatcher.dispatch(client_, message, manager, account)
|
||||
|
||||
client.add_handler(MessageHandler(command_handler, filters.text))
|
||||
client.add_handler(MessageHandler(command_handler, filters.text | filters.caption))
|
||||
|
||||
@@ -167,6 +167,7 @@ class AgentContextTests(unittest.IsolatedAsyncioTestCase):
|
||||
chat=SimpleNamespace(id=99),
|
||||
from_user=SimpleNamespace(id=42),
|
||||
reply_to_message=None,
|
||||
photo=SimpleNamespace(file_size=100),
|
||||
)
|
||||
account = SimpleNamespace(
|
||||
me=SimpleNamespace(id=7, full_name="Owner", username="owner"),
|
||||
@@ -184,6 +185,7 @@ class AgentContextTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
agent = MagicMock()
|
||||
agent.create_session.return_value = session
|
||||
attached_media = MagicMock()
|
||||
|
||||
with (
|
||||
patch("app.agentic.OpenAIBasedLLM") as llm_class,
|
||||
@@ -191,6 +193,10 @@ class AgentContextTests(unittest.IsolatedAsyncioTestCase):
|
||||
patch("app.agentic.BraveSearchTools") as brave_tools,
|
||||
patch("app.agentic.WebPageTools") as web_page_tools,
|
||||
patch("app.agentic._build_context_messages", AsyncMock(return_value=[])),
|
||||
patch(
|
||||
"app.agentic._download_message_media",
|
||||
AsyncMock(return_value=[attached_media]),
|
||||
),
|
||||
):
|
||||
brave_tools.return_value.tools.return_value = []
|
||||
web_page_tools.return_value.tools.return_value = []
|
||||
@@ -210,6 +216,7 @@ class AgentContextTests(unittest.IsolatedAsyncioTestCase):
|
||||
model="ds-model",
|
||||
)
|
||||
self.assertTrue(aggregator_class.call_args.kwargs["thinking"])
|
||||
session.run.assert_awaited_once_with("prompt", media=[attached_media])
|
||||
brave_tools.assert_called_once_with(api_key="brave-key", timeout_seconds=10.0)
|
||||
web_page_tools.assert_called_once_with(timeout_seconds=45.0, max_chars=60_000)
|
||||
result.handle.close()
|
||||
|
||||
@@ -8,6 +8,32 @@ from app.commands import CommandDispatcher, CommandRegistry
|
||||
|
||||
|
||||
class CommandDispatcherTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_command_in_media_caption_is_dispatched(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
handler = AsyncMock()
|
||||
registry.command("photo", "Caption command")(handler)
|
||||
dispatcher = CommandDispatcher(registry)
|
||||
message = SimpleNamespace(
|
||||
text=None,
|
||||
caption=".photo argument",
|
||||
from_user=SimpleNamespace(id=123),
|
||||
reply_text=AsyncMock(),
|
||||
)
|
||||
account = SimpleNamespace(me=SimpleNamespace(id=123), acl_store=None)
|
||||
|
||||
handled = await dispatcher.dispatch(
|
||||
client=SimpleNamespace(),
|
||||
message=message,
|
||||
manager=SimpleNamespace(),
|
||||
account=account,
|
||||
)
|
||||
|
||||
self.assertTrue(handled)
|
||||
handler.assert_awaited_once()
|
||||
context = handler.await_args.args[0]
|
||||
self.assertEqual(context.args, "argument")
|
||||
self.assertEqual(context.raw_text, ".photo argument")
|
||||
|
||||
async def test_unknown_owner_command_is_silent(self) -> None:
|
||||
dispatcher = CommandDispatcher(CommandRegistry())
|
||||
message = SimpleNamespace(
|
||||
|
||||
Reference in New Issue
Block a user