114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Annotated, Any
|
|
|
|
from micro_agents import Injected, SessionContext, Tool, ToolManager
|
|
|
|
|
|
OWNER_ONLY_TOOL_NAMES = frozenset(
|
|
{
|
|
"list_files",
|
|
"read_file",
|
|
"attach_files",
|
|
"write_file",
|
|
"make_directory",
|
|
"execute_python",
|
|
}
|
|
)
|
|
|
|
|
|
class AgentToolManager(ToolManager):
|
|
async def is_allowed(self, tool: Tool, context: SessionContext) -> bool:
|
|
if tool.name in OWNER_ONLY_TOOL_NAMES:
|
|
return context.get("is_owner") is True
|
|
return True
|
|
|
|
|
|
class TelegramTools:
|
|
def __init__(
|
|
self,
|
|
client: Any,
|
|
chat_id: int | str,
|
|
*,
|
|
base_dir: Path,
|
|
guest_base_dir: Path | None = None,
|
|
reply_to_message_id: int | None = None,
|
|
) -> None:
|
|
self._client = client
|
|
self._chat_id = chat_id
|
|
self._base_dir = base_dir.resolve()
|
|
self._guest_base_dir = (guest_base_dir or base_dir).resolve()
|
|
self._reply_to_message_id = reply_to_message_id
|
|
|
|
def tools(self) -> list[Tool]:
|
|
return [
|
|
Tool(self.send_message, name="send_message", strict=False),
|
|
Tool(self.send_image, name="send_image", strict=False),
|
|
Tool(self.send_file, name="send_file", strict=False),
|
|
]
|
|
|
|
async def send_message(self, text: str) -> str:
|
|
"""Send a text message to the current Telegram chat."""
|
|
if not text.strip():
|
|
raise ValueError("text must not be empty")
|
|
message = await self._client.send_message(
|
|
self._chat_id,
|
|
text,
|
|
reply_to_message_id=self._reply_to_message_id,
|
|
)
|
|
return self._result("message", message)
|
|
|
|
async def send_image(
|
|
self,
|
|
path: str,
|
|
caption: str = "",
|
|
context: Annotated[SessionContext | None, Injected] = None,
|
|
) -> str:
|
|
"""Send a local image from the agent workspace to the current Telegram chat."""
|
|
target = self._resolve_file(path, context)
|
|
message = await self._client.send_photo(
|
|
self._chat_id,
|
|
str(target),
|
|
caption=caption or None,
|
|
reply_to_message_id=self._reply_to_message_id,
|
|
)
|
|
return self._result("image", message)
|
|
|
|
async def send_file(
|
|
self,
|
|
path: str,
|
|
caption: str = "",
|
|
context: Annotated[SessionContext | None, Injected] = None,
|
|
) -> str:
|
|
"""Send a local file from the agent workspace to the current Telegram chat."""
|
|
target = self._resolve_file(path, context)
|
|
message = await self._client.send_document(
|
|
self._chat_id,
|
|
str(target),
|
|
caption=caption or None,
|
|
reply_to_message_id=self._reply_to_message_id,
|
|
)
|
|
return self._result("file", message)
|
|
|
|
def _resolve_file(self, value: str, context: SessionContext | None) -> Path:
|
|
base_dir = (
|
|
self._base_dir
|
|
if context is not None and context.get("is_owner") is True
|
|
else self._guest_base_dir
|
|
)
|
|
path = Path(value)
|
|
if path.is_absolute():
|
|
raise ValueError("Absolute paths are not allowed")
|
|
target = (base_dir / path).resolve()
|
|
if target != base_dir and base_dir not in target.parents:
|
|
raise ValueError("Path escapes the agent workspace")
|
|
if not target.is_file():
|
|
raise ValueError(f"File does not exist: {value}")
|
|
return target
|
|
|
|
@staticmethod
|
|
def _result(kind: str, message: Any) -> str:
|
|
message_id = getattr(message, "id", None)
|
|
return f"Sent Telegram {kind}" + (f" (message_id={message_id})" if message_id else "")
|