Browse Source

feat(chatto): add chatto_fetch_link_preview tool

Expose chattolib's fetch_link_preview as a new plugin tool. Allows
fetching OpenGraph metadata (title, description, image URL, site name)
for any URL — useful when the bot needs link information before posting.

- chatto_fetch_link_preview: single 'url' parameter, returns all available
  preview metadata or null fields if the server found nothing.
Paul Klumpp 1 week ago
parent
commit
bcb6388f95
3 changed files with 69 additions and 0 deletions
  1. 67 0
      adapter.py
  2. 1 0
      plugin.yaml
  3. 1 0
      test_adapter.py

+ 67 - 0
adapter.py

@@ -4616,6 +4616,29 @@ CHATTO_SEND_DM_SCHEMA = {
     },
 }
 
+CHATTO_FETCH_LINK_PREVIEW_SCHEMA = {
+    "name": "chatto_fetch_link_preview",
+    "description": (
+        "Fetch an OpenGraph link preview for a URL. Returns title, description, "
+        "image URL and other metadata that Chatto's server would show when "
+        "rendering the link in a message. Passes through whatever metadata the "
+        "target site provides."
+    ),
+    "parameters": {
+        "type": "object",
+        "properties": {
+            "url": {
+                "type": "string",
+                "description": (
+                    "The URL to fetch a preview for (e.g. 'https://example.com')."
+                ),
+            },
+        },
+        "required": ["url"],
+        "additionalProperties": False,
+    },
+}
+
 
 CHATTO_SEND_ROOM_SCHEMA = {
     "name": "chatto_send_room",
@@ -5896,6 +5919,45 @@ async def _delete_custom_status(args: dict) -> str:
         return json.dumps({"success": True, "message": "Custom status cleared"})
 
 
+async def _fetch_link_preview(args: dict) -> str:
+    """Fetch an OpenGraph link preview. See _handle_chatto_fetch_link_preview."""
+    url = (args.get("url") or "").strip()
+    if not url:
+        raise _refuse("url is required")
+
+    async with _tool_client(_chatto_config_for_tools()) as client:
+        preview, _ = await client.fetch_link_preview(url)
+        return json.dumps(
+            {
+                "success": True,
+                "url": url,
+                "title": preview.title if preview else None,
+                "description": preview.description if preview else None,
+                "image_url": preview.image_url if preview else None,
+                "site_name": preview.site_name if preview else None,
+                "embed_type": preview.embed_type if preview else None,
+            }
+        )
+
+
+async def _handle_chatto_fetch_link_preview(args: dict) -> str:
+    """Tool handler for chatto_fetch_link_preview.
+
+    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 _fetch_link_preview(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_get_message(args: dict) -> str:
     """Tool handler for chatto_get_message.
 
@@ -6049,6 +6111,11 @@ _PLUGIN_TOOLS = (
         CHATTO_DELETE_CUSTOM_STATUS_SCHEMA,
         _handle_chatto_delete_custom_status,
     ),
+    (
+        "chatto_fetch_link_preview",
+        CHATTO_FETCH_LINK_PREVIEW_SCHEMA,
+        _handle_chatto_fetch_link_preview,
+    ),
 )
 
 

+ 1 - 0
plugin.yaml

@@ -21,6 +21,7 @@ provides_tools:
   - chatto_pin_message
   - chatto_set_custom_status
   - chatto_delete_custom_status
+  - chatto_fetch_link_preview
 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)"

+ 1 - 0
test_adapter.py

@@ -4473,6 +4473,7 @@ class TestPluginToolSchemas:
             "chatto_pin_message",
             "chatto_set_custom_status",
             "chatto_delete_custom_status",
+            "chatto_fetch_link_preview",
         ]
         for tool in ctx.registered_tools:
             assert tool["is_async"] is True, tool["name"]