feat: rebuild AI commands and migrate config to TOML
This commit is contained in:
113
app/agent_tools.py
Normal file
113
app/agent_tools.py
Normal file
@@ -0,0 +1,113 @@
|
||||
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 "")
|
||||
Reference in New Issue
Block a user