Ver código fonte

feat(chatto): add chatto_set_custom_status and chatto_delete_custom_status tools

Expose chattolib's set_custom_status/delete_custom_status as two new plugin
tools. The bot can now set an emoji+text status (e.g. 🛠️ processing) visible
in the Chatto client, optionally with an auto-expire timer.

- chatto_set_custom_status: set emoji + text, optional expires_in_seconds
- chatto_delete_custom_status: clear status entirely

Follows the existing two-door pattern: schema + handler in adapter.py,
registration in _PLUGIN_TOOLS and plugin.yaml provides_tools, test list
updated to match.
Paul Klumpp 1 semana atrás
pai
commit
101c3a8c55
3 arquivos alterados com 139 adições e 0 exclusões
  1. 135 0
      adapter.py
  2. 2 0
      plugin.yaml
  3. 2 0
      test_adapter.py

+ 135 - 0
adapter.py

@@ -4530,6 +4530,56 @@ CHATTO_PIN_MESSAGE_SCHEMA = {
     },
 }
 
+CHATTO_SET_CUSTOM_STATUS_SCHEMA = {
+    "name": "chatto_set_custom_status",
+    "description": (
+        "Set the bot's custom status in Chatto. Shows an emoji and text next "
+        "to the bot name in the client (e.g. 🛠️ processing, ⏳ awaiting review). "
+        "Optional ``expires_in_seconds`` auto-clears the status; omit or 0 for "
+        "manual clearing."
+    ),
+    "parameters": {
+        "type": "object",
+        "properties": {
+            "emoji": {
+                "type": "string",
+                "description": (
+                    "A single emoji to show next to the status text "
+                    "(e.g. 🛠️, ⏳, 🔴)."
+                ),
+            },
+            "text": {
+                "type": "string",
+                "description": (
+                    "The status text to display (e.g. 'processing', 'awaiting review')."
+                ),
+            },
+            "expires_in_seconds": {
+                "type": "integer",
+                "description": (
+                    "Optional: auto-clear the status after this many seconds. "
+                    "Omit or 0 to keep the status until cleared."
+                ),
+            },
+        },
+        "required": ["emoji", "text"],
+        "additionalProperties": False,
+    },
+}
+
+CHATTO_DELETE_CUSTOM_STATUS_SCHEMA = {
+    "name": "chatto_delete_custom_status",
+    "description": (
+        "Clear the bot's custom status, reverting to no status display."
+    ),
+    "parameters": {
+        "type": "object",
+        "properties": {},
+        "required": [],
+        "additionalProperties": False,
+    },
+}
+
 
 CHATTO_SEND_DM_SCHEMA = {
     "name": "chatto_send_dm",
@@ -5771,6 +5821,81 @@ async def _handle_chatto_list_rooms(args: dict) -> str:
         return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
 
 
+async def _handle_chatto_set_custom_status(args: dict) -> str:
+    """Tool handler for chatto_set_custom_status.
+
+    Returns a JSON string: Hermes' tool contract accepts no other type, and a
+    dict (or an unflagged coroutine) fails at dispatch with "unsupported result
+    type". Every early exit arrives here as a _ToolRefusal and leaves as that
+    refusal's own JSON, so no path escapes as an unhandled error.
+    """
+    try:
+        return await _set_custom_status(args)
+    except _ToolRefusal as refusal:
+        return refusal.result_json
+    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}"})
+
+
+async def _handle_chatto_delete_custom_status(args: dict) -> str:
+    """Tool handler for chatto_delete_custom_status.
+
+    Returns a JSON string: Hermes' tool contract accepts no other type, and a
+    dict (or an unflagged coroutine) fails at dispatch with "unsupported result
+    type". Every early exit arrives here as a _ToolRefusal and leaves as that
+    refusal's own JSON, so no path escapes as an unhandled error.
+    """
+    try:
+        return await _delete_custom_status(args)
+    except _ToolRefusal as refusal:
+        return refusal.result_json
+    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}"})
+
+
+async def _set_custom_status(args: dict) -> str:
+    """Set the bot's custom status. See _handle_chatto_set_custom_status."""
+    emoji = (args.get("emoji") or "").strip()
+    text = (args.get("text") or "").strip()
+    expires_in = args.get("expires_in_seconds")
+    if expires_in is not None:
+        expires_in = int(expires_in)
+        if expires_in < 0:
+            raise _refuse("expires_in_seconds must be non-negative")
+
+    if not emoji or not text:
+        raise _refuse("emoji and text are required")
+
+    async with _tool_client(_chatto_config_for_tools()) as client:
+        expires_at = None
+        if expires_in and expires_in > 0:
+            from datetime import UTC, datetime, timedelta
+
+            expires_at = datetime.now(UTC) + timedelta(seconds=expires_in)
+        await client.set_custom_status(emoji, text, expires_at=expires_at)
+        return json.dumps(
+            {
+                "success": True,
+                "emoji": emoji,
+                "text": text,
+                "expires_at": (
+                    _iso_or_none(expires_at) if expires_at else None
+                ),
+            }
+        )
+
+
+async def _delete_custom_status(args: dict) -> str:
+    """Clear the bot's custom status. See _handle_chatto_delete_custom_status."""
+    async with _tool_client(_chatto_config_for_tools()) as client:
+        await client.delete_custom_status()
+        return json.dumps({"success": True, "message": "Custom status cleared"})
+
+
 async def _handle_chatto_get_message(args: dict) -> str:
     """Tool handler for chatto_get_message.
 
@@ -5914,6 +6039,16 @@ _PLUGIN_TOOLS = (
         CHATTO_PIN_MESSAGE_SCHEMA,
         _handle_chatto_pin_message,
     ),
+    (
+        "chatto_set_custom_status",
+        CHATTO_SET_CUSTOM_STATUS_SCHEMA,
+        _handle_chatto_set_custom_status,
+    ),
+    (
+        "chatto_delete_custom_status",
+        CHATTO_DELETE_CUSTOM_STATUS_SCHEMA,
+        _handle_chatto_delete_custom_status,
+    ),
 )
 
 

+ 2 - 0
plugin.yaml

@@ -19,6 +19,8 @@ provides_tools:
   - chatto_list_room_attachments
   - chatto_list_pinned_messages
   - chatto_pin_message
+  - chatto_set_custom_status
+  - chatto_delete_custom_status
 requires_env:
   - name: CHATTO_BASE_URL
     description: "Base URL of the Chatto server (e.g. https://chat.example.com) leave blank for ChattoHQ (https://chat.chatto.run)"

+ 2 - 0
test_adapter.py

@@ -4471,6 +4471,8 @@ class TestPluginToolSchemas:
             "chatto_list_room_attachments",
             "chatto_list_pinned_messages",
             "chatto_pin_message",
+            "chatto_set_custom_status",
+            "chatto_delete_custom_status",
         ]
         for tool in ctx.registered_tools:
             assert tool["is_async"] is True, tool["name"]