Files
beaver-agent/beaver_agent/hands/__init__.py
T

172 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Руки: какие MCP есть у сетапа и кому из агентов какая рука выдана.
Секреты MCP - только через env подпроцесса, никогда argv: процесс модели
видит `ps` всего контейнера. Рука, чей env не задан, просто не появляется.
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
from beaver_gateway.agents.base import ExposedMcp
from beaver_gateway.mcp.types import McpServer, McpServerT
from beaver_agent.hands.calendars import Calendars, calendars, feeds
from beaver_agent.hands.homeassistant import HomeAssistant
from beaver_agent.hands.komodo import Komodo
from beaver_agent.hands.vibegram import Vibegram
from beaver_agent.vault import TZ
env = os.environ.get
_log = logging.getLogger(__name__)
obsidian = McpServer.stdio(
name="obsidian-fs",
command=["bunx", "-y", "@modelcontextprotocol/server-filesystem", "/vault"],
lenient=True,
)
"""Файловые тулзы для тех, у кого нет своих (Raycast); у Claude vault смонтирован."""
firefly = (
McpServer.stdio(
name="firefly",
command=["bunx", "-y", "@firefly-iii-mcp/local", "--preset", "default"],
env={
"FIREFLY_III_PAT": os.environ["FIREFLY_PAT"],
"FIREFLY_III_BASE_URL": os.environ["FIREFLY_BASE_URL"],
},
lenient=True,
)
if env("FIREFLY_PAT")
else None
)
firefly_exposed = (ExposedMcp(name="firefly", deny=("delete_*",)),) if firefly else ()
telegram = (
McpServer.http(name="telegram", url=os.environ["BEAVERGRAM_MCP"])
if env("BEAVERGRAM_MCP")
else None
)
"""beavergram: история чатов Бобра в Telegram."""
calendar_servers = calendars(env("CALENDAR_MCPS", ""))
calendar_exposed = tuple(ExposedMcp(name=m.name) for m in calendar_servers)
CALENDAR_FEEDS = feeds(env("CALENDAR_MCPS", ""))
CALENDARS = Calendars(CALENDAR_FEEDS, tz=TZ) if CALENDAR_FEEDS else None
"""Те же фиды, но напрямую: ближайшие события едут в справку без модели."""
KOMODO = (
Komodo(
url=os.environ["KOMODO_URL"],
key=os.environ["KOMODO_KEY"],
secret=os.environ["KOMODO_SECRET"],
)
if env("KOMODO_URL")
else None
)
"""Флот: перечислимые действия, ключ остаётся в gateway."""
komodo = McpServer.python_tool(name="komodo", tools=[KOMODO.komodo]) if KOMODO else None
komodo_exposed = (ExposedMcp(name="komodo"),) if KOMODO else ()
HA = (
HomeAssistant(url=os.environ["HA_URL"], token=os.environ["HA_TOKEN"])
if env("HA_URL") and env("HA_TOKEN")
else None
)
"""Дом: HA живёт на хосте, из контейнера - host.docker.internal."""
ha = McpServer.python_tool(name="ha", tools=[HA.ha]) if HA else None
ha_exposed = (ExposedMcp(name="ha"),) if HA else ()
VIBEGRAM_REPO = Path("/vibegram") / env("VIBEGRAM_REPO", "org/room").split("/")[-1]
VIBEGRAM = (
Vibegram(
hub=os.environ["VIBEGRAM_HUB"],
token=os.environ["VIBEGRAM_TOKEN"],
nick=os.environ["VIBEGRAM_NICK"],
repo=VIBEGRAM_REPO if VIBEGRAM_REPO.exists() else None,
tz=TZ,
)
if env("VIBEGRAM_TOKEN") and env("VIBEGRAM_HUB") and env("VIBEGRAM_NICK")
else None
)
"""Комната агентов: диспетчер сидит в ней сам; клон репы комнаты - том `vibegram`."""
vibegram = (
McpServer.python_tool(name="vibegram", tools=[VIBEGRAM.vibegram])
if VIBEGRAM
else None
)
vibegram_exposed = (ExposedMcp(name="vibegram"),) if VIBEGRAM else ()
VIBEGRAM_HB_NICK = env("VIBEGRAM_HB_NICK") or env("VIBEGRAM_NICK")
VIBEGRAM_HB = (
Vibegram(
hub=os.environ["VIBEGRAM_HUB"],
token=os.environ["VIBEGRAM_HB_TOKEN"],
nick=VIBEGRAM_HB_NICK or "",
tz=TZ,
)
if env("VIBEGRAM_HB_TOKEN") and env("VIBEGRAM_HUB") and VIBEGRAM_HB_NICK
else None
)
"""Сторожевая комната: ВТОРОЙ, независимый экземпляр клиента.
Комната на хабе определяется токеном, а `/api/pending` - курсор на стороне
хаба: общий с основной комнатой экземпляр означал бы, что отметки соседа
съедает триаж-джоб, а сами они тонут в разговоре. Модели эта рука не
выдаётся: сторож - механизм, у него нет тулзы.
"""
if VIBEGRAM_HB is None:
_log.info(
"сторож: VIBEGRAM_HB_TOKEN не задан - отметка и слежка за соседом выключены"
)
t3code = (
McpServer.http(name="t3code", url=env("T3CODE_MCP", "http://t3code-mcp:8000/mcp"))
if "t3" in env("COMPOSE_PROFILES", "").split(",")
else None
)
"""t3code-mcp - сосед по compose (профиль t3); машины и allowlist в его t3code.toml."""
t3code_exposed = (ExposedMcp(name="t3code"),) if t3code else ()
msos = (
McpServer.http(name="msos", url=os.environ["MSOS_MCP"]) if env("MSOS_MCP") else None
)
"""Таскер рабочих проектов (скилл `задачи`)."""
msos_exposed = (ExposedMcp(name="msos"),) if msos else ()
mcps: list[McpServerT] = [
m
for m in (
obsidian,
firefly,
telegram,
*calendar_servers,
komodo,
ha,
vibegram,
t3code,
msos,
)
if m is not None
]
CLAUDE = (
*firefly_exposed,
ExposedMcp(name="telegram"),
*calendar_exposed,
*komodo_exposed,
*ha_exposed,
)
"""Руки любого Claude-агента: firefly без delete, телега, календари, флот, дом."""
DISPATCHER = (*CLAUDE, *t3code_exposed, *vibegram_exposed, *msos_exposed)
"""Диспетчер ещё ходит в код, в комнату агентов и в таскер; глубоким это не даётся."""
RAYCAST = (
ExposedMcp(name="obsidian-fs"),
*firefly_exposed,
ExposedMcp(name="telegram"),
*calendar_exposed,
)