소스 검색

feat: chatto_list_rooms tool (resolve room names to IDs, e.g. for cron deliver targets)

- Lists rooms the bot is a member of: room_id, name, kind, threading_mode, archived
- Optional case-insensitive substring filter 'name' (skips unnamed DM rooms when set)
- Shared _chatto_tool_client helper for config-fallback client construction
- Verified in isolated subprocess simulating cron scope (env vars popped)
Liv Klumpp 2 주 전
부모
커밋
1037c8391c
1개의 변경된 파일과 120개의 추가작업 그리고 0개의 파일을 삭제
  1. 120 0
      adapter.py

+ 120 - 0
adapter.py

@@ -3511,6 +3511,63 @@ def _capabilities() -> list[str]:
 # Plugin tools: send DM to a user, or send to a room
 # --------------------------------------------------------------------------- #
 
+CHATTO_LIST_ROOMS_SCHEMA = {
+    "name": "chatto_list_rooms",
+    "description": (
+        "List the Chatto rooms (channels and DM rooms) the bot is a member of, "
+        "with their room IDs. Use this to resolve a channel name to its room ID — "
+        "e.g. to find the 'platform:chat_id' address for a cron job's `deliver` "
+        "target. Optional `name` filters case-insensitively by substring."
+    ),
+    "parameters": {
+        "type": "object",
+        "properties": {
+            "name": {
+                "type": "string",
+                "description": (
+                    "Optional: case-insensitive substring filter on the room name "
+                    "(e.g. 'liv' matches 'Liv' and 'Liv-Dev'). Empty lists all rooms."
+                ),
+            },
+            "base_url": {
+                "type": "string",
+                "description": (
+                    "Chatto server base URL (e.g. 'https://chat.oberrhein.de'). "
+                    "If omitted, falls back to the configured server URL."
+                ),
+            },
+            "token": {
+                "type": "string",
+                "description": (
+                    "Chatto bot token (cht_BK_...). "
+                    "If omitted, falls back to the configured token."
+                ),
+            },
+        },
+        "required": [],
+        "additionalProperties": False,
+    },
+}
+
+
+def _chatto_tool_client(args: dict, cfg):
+    """Shared client construction for tool handlers. Returns (client, error_json)."""
+    token = args.get("token") or ""
+    if not token and cfg is not None:
+        token = cfg.token.value or ""
+    base_url = args.get("base_url") or ""
+    if not base_url and cfg is not None:
+        base_url = cfg.base_url.value or "https://chat.chatto.run"
+    if not base_url:
+        base_url = "https://chat.chatto.run"
+    if not token:
+        return None, json.dumps({"success": False, "error": "Chatto token not configured"})
+    try:
+        return ChattoClient(base_url=base_url, token=token), None
+    except Exception as exc:
+        return None, json.dumps({"success": False, "error": f"Failed to create Chatto client: {exc}"})
+
+
 CHATTO_SEND_DM_SCHEMA = {
     "name": "chatto_send_dm",
     "description": (
@@ -3924,6 +3981,61 @@ async def _handle_chatto_send_room(args: dict) -> dict:
             pass
 
 
+async def _handle_chatto_list_rooms(args: dict) -> dict:
+    """Tool handler for chatto_list_rooms: list rooms the bot is a member of."""
+    name_filter = (args.get("name") or "").strip().casefold()
+
+    cfg = _chatto_config_for_tools()
+    client, err = _chatto_tool_client(args, cfg)
+    if err:
+        return err
+
+    try:
+        rooms_out = []
+        for rws in await client.list_rooms():
+            room = getattr(rws, "room", None)
+            if not room:
+                continue
+            # DM rooms have no name; label them by their ID so they stay
+            # distinguishable in the output.
+            name = room.name or ""
+            kind = str(getattr(room.kind, "value", getattr(room.kind, "name", room.kind)) or "")
+            label = name or f"(DM room {room.id})"
+            if name_filter:
+                # A filter implies named rooms: skip DMs (empty name) and
+                # non-matching names alike.
+                if not name or name_filter not in name.casefold():
+                    continue
+            rooms_out.append({
+                "room_id": room.id,
+                "name": name,
+                "kind": kind,
+                "threading_mode": str(
+                    getattr(getattr(room, "threading_mode", None), "value", "") or ""
+                ),
+                "archived": bool(getattr(room, "archived", False)),
+                "label": label,
+            })
+        return json.dumps({
+            "success": True,
+            "count": len(rooms_out),
+            "rooms": rooms_out,
+            "hint": (
+                "Use room_id in chatto_send_room, or as the chat_id part of a "
+                "cron deliver target like 'chatto-platform:<room_id>'."
+            ),
+        })
+    except ChattoAuthError as exc:
+        return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
+    except ChattoError as exc:
+        return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
+    finally:
+        try:
+            await client.close()
+        except Exception:
+            pass
+
+
 def register(ctx) -> None:
     """Plugin entry point — called by the Hermes plugin system."""
     logger.info("Registering Chatto platform plugin on Hermes Agent")
@@ -3945,6 +4057,14 @@ def register(ctx) -> None:
         is_async=True,
         description=CHATTO_SEND_ROOM_SCHEMA["description"],
     )
+    ctx.register_tool(
+        name="chatto_list_rooms",
+        toolset="chatto-platform",
+        schema=CHATTO_LIST_ROOMS_SCHEMA,
+        handler=_handle_chatto_list_rooms,
+        is_async=True,
+        description=CHATTO_LIST_ROOMS_SCHEMA["description"],
+    )
 
     for capability in _capabilities():
         logger.info("Chatto capability: %s", capability)