|
@@ -4358,6 +4358,11 @@ FOLLOW_REPLY_CHAIN_MAX = 10
|
|
|
HISTORY_PAGE_SIZE = 20
|
|
HISTORY_PAGE_SIZE = 20
|
|
|
HISTORY_PAGE_MAX = 100
|
|
HISTORY_PAGE_MAX = 100
|
|
|
|
|
|
|
|
|
|
+# Read-tool bounds for pinned messages and attachments — one call per page,
|
|
|
|
|
+# kept small enough that a model can digest the output without choking.
|
|
|
|
|
+PINNED_PAGE_SIZE = 20
|
|
|
|
|
+ATTACHMENT_PAGE_SIZE = 40
|
|
|
|
|
+
|
|
|
|
|
|
|
|
CHATTO_LIST_ROOMS_SCHEMA = {
|
|
CHATTO_LIST_ROOMS_SCHEMA = {
|
|
|
"name": "chatto_list_rooms",
|
|
"name": "chatto_list_rooms",
|
|
@@ -4385,6 +4390,118 @@ CHATTO_LIST_ROOMS_SCHEMA = {
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+CHATTO_LIST_ROOM_ATTACHMENTS_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_list_room_attachments",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "List the file attachments in a Chatto room (images, documents, etc.). "
|
|
|
|
|
+ "Returns each attachment's ID, filename, MIME type, size and an optional "
|
|
|
|
|
+ "thumbnail URL. Optional ``limit`` (default 40) and ``offset`` allow "
|
|
|
|
|
+ "paginating through large attachment lists."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID to list attachments in.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "limit": {
|
|
|
|
|
+ "type": "integer",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ f"Maximum attachments to return (default {ATTACHMENT_PAGE_SIZE})."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "offset": {
|
|
|
|
|
+ "type": "integer",
|
|
|
|
|
+ "description": "Skip this many attachments (for pagination).",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": [],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_LIST_PINNED_MESSAGES_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_list_pinned_messages",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "List pinned messages in a Chatto room. Returns each message's event ID, "
|
|
|
|
|
+ "body, author (user ID and login), and the pin timestamp. Optional "
|
|
|
|
|
+ "``limit`` (default 20) and ``offset`` allow paginating through long "
|
|
|
|
|
+ "pin lists."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID to list pinned messages in.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "limit": {
|
|
|
|
|
+ "type": "integer",
|
|
|
|
|
+ "description": f"Maximum pinned messages to return (default {PINNED_PAGE_SIZE}).",
|
|
|
|
|
+ },
|
|
|
|
|
+ "offset": {
|
|
|
|
|
+ "type": "integer",
|
|
|
|
|
+ "description": "Skip this many pinned messages (for pagination).",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": [],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_PIN_MESSAGE_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_pin_message",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Pin or unpin a message in a Chatto room. Use ``pin=true`` to pin a "
|
|
|
|
|
+ "message (it becomes visible via ``chatto_list_pinned_messages``), or "
|
|
|
|
|
+ "``pin=false`` to remove an existing pin. Requires the ``event_id`` of "
|
|
|
|
|
+ "the message to pin."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "event_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The message event ID to pin or unpin.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "pin": {
|
|
|
|
|
+ "type": "boolean",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "True to pin, false to unpin the message."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["event_id", "pin"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
CHATTO_SEND_DM_SCHEMA = {
|
|
CHATTO_SEND_DM_SCHEMA = {
|
|
|
"name": "chatto_send_dm",
|
|
"name": "chatto_send_dm",
|
|
@@ -5172,6 +5289,136 @@ async def _list_rooms(args: dict) -> str:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+async def _list_room_attachments(args: dict) -> str:
|
|
|
|
|
+ """List file attachments in a Chatto room. See _handle_chatto_list_room_attachments."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ limit = _int_arg(
|
|
|
|
|
+ args, "limit", ATTACHMENT_PAGE_SIZE, low=1, high=ATTACHMENT_PAGE_SIZE * 5
|
|
|
|
|
+ )
|
|
|
|
|
+ offset = args.get("offset")
|
|
|
|
|
+ if offset is not None:
|
|
|
|
|
+ offset = int(offset)
|
|
|
|
|
+ if offset is not None and offset < 0:
|
|
|
|
|
+ raise _refuse("offset must be non-negative")
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ attachments, _ = await client.list_room_attachments(
|
|
|
|
|
+ room.id, limit=limit, offset=offset
|
|
|
|
|
+ )
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "room_name": room.name,
|
|
|
|
|
+ "count": len(attachments),
|
|
|
|
|
+ "offset": offset or 0,
|
|
|
|
|
+ "has_more": False, # chattolib returns full list without Page here
|
|
|
|
|
+ "attachments": attachments,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _list_pinned_messages(args: dict) -> str:
|
|
|
|
|
+ """List pinned messages in a Chatto room. See _handle_chatto_list_pinned_messages."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ limit = _int_arg(args, "limit", PINNED_PAGE_SIZE, low=1, high=PINNED_PAGE_SIZE * 5)
|
|
|
|
|
+ offset = args.get("offset")
|
|
|
|
|
+ if offset is not None:
|
|
|
|
|
+ offset = int(offset)
|
|
|
|
|
+ if offset is not None and offset < 0:
|
|
|
|
|
+ raise _refuse("offset must be non-negative")
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ page = await client.list_pinned_messages(room.id, limit=limit, offset=offset)
|
|
|
|
|
+ author_logins: dict[str, str] = {}
|
|
|
|
|
+ results: list[dict] = []
|
|
|
|
|
+ for pm in page.pinned_messages:
|
|
|
|
|
+ msg = pm.message
|
|
|
|
|
+ if msg is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ author_id = msg.actor_id
|
|
|
|
|
+ if author_id and author_id not in author_logins:
|
|
|
|
|
+ logins = await _batch_author_logins(client, room.id, [author_id])
|
|
|
|
|
+ author_logins.update(logins)
|
|
|
|
|
+ results.append(
|
|
|
|
|
+ {
|
|
|
|
|
+ "event_id": msg.id,
|
|
|
|
|
+ "body": msg.body,
|
|
|
|
|
+ "author_id": author_id,
|
|
|
|
|
+ "author_login": author_logins.get(author_id) or "",
|
|
|
|
|
+ "pinned_at": _iso_or_none(pm.message.created_at if hasattr(pm, 'message') and pm.message else None),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "room_name": room.name,
|
|
|
|
|
+ "count": len(results),
|
|
|
|
|
+ "has_older": page.page.has_more,
|
|
|
|
|
+ "pinned_messages": results,
|
|
|
|
|
+ "latest_pin_marker": page.latest_pin_marker,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _pin_message(args: dict) -> str:
|
|
|
|
|
+ """Pin or unpin a message in a Chatto room. See _handle_chatto_pin_message."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ event_id = args.get("event_id") or ""
|
|
|
|
|
+ pin = args.get("pin")
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+ if not event_id:
|
|
|
|
|
+ raise _refuse("event_id is required")
|
|
|
|
|
+ if pin is None:
|
|
|
|
|
+ raise _refuse("pin (true/false) is required")
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ if pin:
|
|
|
|
|
+ pinned = await client.pin_message(room.id, event_id)
|
|
|
|
|
+ message = pinned.message if hasattr(pinned, 'message') else None
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "action": "pinned",
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "event_id": event_id,
|
|
|
|
|
+ "pinned_at": _iso_or_none(message.created_at if message else None),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ success = await client.unpin_message(room.id, event_id)
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "action": "unpinned",
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "event_id": event_id,
|
|
|
|
|
+ "unpinned": success,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _iso_or_none(moment: datetime | None) -> str | None:
|
|
def _iso_or_none(moment: datetime | None) -> str | None:
|
|
|
"""A Message timestamp as ISO-8601, or None when the server sent none.
|
|
"""A Message timestamp as ISO-8601, or None when the server sent none.
|
|
|
|
|
|
|
@@ -5532,6 +5779,60 @@ async def _handle_chatto_get_history(args: dict) -> str:
|
|
|
return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+async def _handle_chatto_list_room_attachments(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_list_room_attachments.
|
|
|
|
|
+
|
|
|
|
|
+ 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 _list_room_attachments(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_list_pinned_messages(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_list_pinned_messages.
|
|
|
|
|
+
|
|
|
|
|
+ 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 _list_pinned_messages(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_pin_message(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_pin_message.
|
|
|
|
|
+
|
|
|
|
|
+ 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 _pin_message(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}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _register_bundled_skills(ctx) -> None:
|
|
def _register_bundled_skills(ctx) -> None:
|
|
|
"""Register the skills shipped in ``skills/``, each with its own description.
|
|
"""Register the skills shipped in ``skills/``, each with its own description.
|
|
|
|
|
|
|
@@ -5570,6 +5871,21 @@ _PLUGIN_TOOLS = (
|
|
|
("chatto_list_rooms", CHATTO_LIST_ROOMS_SCHEMA, _handle_chatto_list_rooms),
|
|
("chatto_list_rooms", CHATTO_LIST_ROOMS_SCHEMA, _handle_chatto_list_rooms),
|
|
|
("chatto_get_message", CHATTO_GET_MESSAGE_SCHEMA, _handle_chatto_get_message),
|
|
("chatto_get_message", CHATTO_GET_MESSAGE_SCHEMA, _handle_chatto_get_message),
|
|
|
("chatto_get_history", CHATTO_GET_HISTORY_SCHEMA, _handle_chatto_get_history),
|
|
("chatto_get_history", CHATTO_GET_HISTORY_SCHEMA, _handle_chatto_get_history),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_list_room_attachments",
|
|
|
|
|
+ CHATTO_LIST_ROOM_ATTACHMENTS_SCHEMA,
|
|
|
|
|
+ _handle_chatto_list_room_attachments,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_list_pinned_messages",
|
|
|
|
|
+ CHATTO_LIST_PINNED_MESSAGES_SCHEMA,
|
|
|
|
|
+ _handle_chatto_list_pinned_messages,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_pin_message",
|
|
|
|
|
+ CHATTO_PIN_MESSAGE_SCHEMA,
|
|
|
|
|
+ _handle_chatto_pin_message,
|
|
|
|
|
+ ),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|