Parcourir la source

Carry inbound replies as MessageEvent.in_reply_to

The dispatch assembled its MessageEvent piecemeal without threading
info, so replies reached the agent's session unanchored. Build the full
event up front (text, source, id, timestamp, media) and let command
detection flow through message_event.is_command() instead of local
string special-casing.
Paul Klumpp il y a 1 semaine
Parent
commit
fc982e8a8d
2 fichiers modifiés avec 33 ajouts et 35 suppressions
  1. 32 34
      adapter.py
  2. 1 1
      platform_config.py

+ 32 - 34
adapter.py

@@ -12,6 +12,7 @@ including both outbound messaging and realtime WebSocket connections.
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+from builtins import ImportError
 import random
 import random
 
 
 from gateway.platforms.helpers import MessageDeduplicator
 from gateway.platforms.helpers import MessageDeduplicator
@@ -673,13 +674,14 @@ class ChattoAdapter(BasePlatformAdapter):
         except RuntimeError:
         except RuntimeError:
             logger.warning("Chatto: chattolib event loop aborted - no client available")
             logger.warning("Chatto: chattolib event loop aborted - no client available")
             return
             return
-    
-        logger.info("Chatto WS: 'message_posted' event_payload:%s", payload)
+        logger.info("Chatto WS: 'message_posted' payload:%s", payload)
 
 
         message = await payload.fetch_message(client=client)
         message = await payload.fetch_message(client=client)
         if message is None or message.deleted_at:
         if message is None or message.deleted_at:
             return
             return
         message_body = message.body or ""
         message_body = message.body or ""
+        logger.info("message: %s", message)
+
         attachments = list(message.attachments or [])
         attachments = list(message.attachments or [])
         # A message carrying only an image/PDF has an empty body — dropping it
         # A message carrying only an image/PDF has an empty body — dropping it
         # here is what made attachments sent to Hermes disappear silently.
         # here is what made attachments sent to Hermes disappear silently.
@@ -749,13 +751,13 @@ class ChattoAdapter(BasePlatformAdapter):
                 await self.add_reaction(message.room_id, message.id, "🫥")
                 await self.add_reaction(message.room_id, message.id, "🫥")
             return
             return
 
 
-        # Thread anchoring — if the incoming message is inside a thread, we
+        # Thread anchoring — if the incoming message is inside a Chatto thread, we
         # keep that thread by default; otherwise leave thread_id unset so
         # keep that thread by default; otherwise leave thread_id unset so
         # replies land at the root.
         # replies land at the root.
-        thread_id = payload.thread_root_event_id or None
-        if not thread_id and room_kind != RoomKind.DM and self.chatto_config.auto_thread.value:
+        thread_id = payload.thread_root_event_id or None # we could also take "payload.room_id" but then, we're in a thread already.
+        if not thread_id and room_kind != RoomKind.DM:
             thread_id = message.id
             thread_id = message.id
-            
+
         source = self.build_source(
         source = self.build_source(
             chat_id=payload.room_id,
             chat_id=payload.room_id,
             chat_name=self._room_names.get(message.room_id),
             chat_name=self._room_names.get(message.room_id),
@@ -767,37 +769,33 @@ class ChattoAdapter(BasePlatformAdapter):
             role_authorized=True,
             role_authorized=True,
         )
         )
 
 
-        msg_type = MessageType.COMMAND if (message_body.lstrip().startswith("/")) else MessageType.TEXT
-
-        my_body = message_body.lstrip() if msg_type == MessageType.COMMAND else message_body
+        # prepare a MessageEvent
+        message_event = MessageEvent(
+            text=message_body,
+            source=source,
+            message_id=message.id,
+            timestamp=message.created_at or datetime.now(timezone.utc),
+            raw_message=message,
+            reply_to_message_id=message.in_reply_to,
+        )
+        if message_event.is_command():
+            message_event.message_type = MessageType.COMMAND
 
 
         # Attachments — download and hand the local cache paths to the gateway,
         # Attachments — download and hand the local cache paths to the gateway,
         # which runs vision enrichment / document extraction off media_urls.
         # which runs vision enrichment / document extraction off media_urls.
-        media_urls, media_types, media_kinds = await self._cache_attachments(
+        message_event.media_urls, message_event.media_types, media_kinds = await self._cache_attachments(
             payload.room_id, attachments,
             payload.room_id, attachments,
         )
         )
-        if media_kinds and msg_type != MessageType.COMMAND:
+        if media_kinds:
             # Same precedence as the Teams/Signal adapters: document-context
             # Same precedence as the Teams/Signal adapters: document-context
             # injection gates strictly on DOCUMENT, image handling keys off the
             # injection gates strictly on DOCUMENT, image handling keys off the
             # per-path image/* MIME regardless of message_type.
             # per-path image/* MIME regardless of message_type.
-            msg_type = self._message_type_for_media_kinds(media_kinds)
+            message_event.message_type = self._message_type_for_media_kinds(media_kinds)
+        else:
+            message_event.message_type = MessageType.TEXT
 
 
-        message_event = MessageEvent(
-            text=my_body,
-            source=source,
-            message_id=message.id,
-            message_type=msg_type,
-            media_urls=media_urls,
-            media_types=media_types,
-            timestamp=message.created_at or datetime.now(timezone.utc),
-            raw_message=message,
-            reply_to_message_id=message.id,
-            reply_to_text=message_body,
-            reply_to_author_id=user.id,
-            reply_to_author_name=user.login,
-        )
-        logger.info("Chatto: Dispatching MessageEvent to Hermes: %s", message_event)
 
 
+        logger.info("Chatto: Dispatching MessageEvent to Hermes: %s", message_event)
         await self.handle_message(message_event)
         await self.handle_message(message_event)
         return
         return
 
 
@@ -1934,9 +1932,9 @@ class ChattoAdapter(BasePlatformAdapter):
 # ---------------------------------------------------------------------------
 # ---------------------------------------------------------------------------
 
 
 async def hermes_standalone_sender_fn(
 async def hermes_standalone_sender_fn(
-    pconfig,
-    chat_id,
-    message,
+    pconfig: PlatformConfig,
+    chat_id: str,
+    message: str,
     *,
     *,
     thread_id=None,
     thread_id=None,
     media_files=None,
     media_files=None,
@@ -1974,14 +1972,14 @@ async def hermes_standalone_sender_fn(
     try:
     try:
         kwargs: Dict[str, Any] = {}
         kwargs: Dict[str, Any] = {}
         if chatto_config.auto_thread.value and thread_id:
         if chatto_config.auto_thread.value and thread_id:
-            kwargs["in_reply_to"] = thread_id
+            kwargs["thread_root_event_id"] = thread_id
         if media_files and media_files.get("attachment_asset_ids"):
         if media_files and media_files.get("attachment_asset_ids"):
             kwargs["attachment_asset_ids"] = list(media_files["attachment_asset_ids"])
             kwargs["attachment_asset_ids"] = list(media_files["attachment_asset_ids"])
         try:
         try:
             posted = await client.post_message(chat_id, message, **kwargs)
             posted = await client.post_message(chat_id, message, **kwargs)
         except Exception as exc:
         except Exception as exc:
             return SendResult(success=False, error=str(exc))
             return SendResult(success=False, error=str(exc))
-        return SendResult(success=True, message_id=getattr(posted, "id", "") or None)
+        return SendResult(success=True, message_id=posted.id, raw_response=posted)
     finally:
     finally:
         try:
         try:
             await client.close()
             await client.close()
@@ -2019,7 +2017,7 @@ def hermes_check_fn() -> bool:
     """Check if Chatto is configured and dependencies are available.
     """Check if Chatto is configured and dependencies are available.
     Add real logic?! Or just .. there are no dependencies.. always return true. Really. Docs suck."""
     Add real logic?! Or just .. there are no dependencies.. always return true. Really. Docs suck."""
     try:
     try:
-        import chattolib.client  # noqa: F401 — vendored dependency probe
+        from .vendor.common.chattolib import client  # noqa: F401 — vendored dependency probe
         return True
         return True
     except ImportError:
     except ImportError:
         return False
         return False
@@ -2100,7 +2098,7 @@ def hermes_env_enablement_fn() -> Optional[dict]:
     # Seed keys must be the config.yaml "extra" keys (config_key), NOT the env
     # Seed keys must be the config.yaml "extra" keys (config_key), NOT the env
     # var names — ChattoConfiguration reads extra[config_key].
     # var names — ChattoConfiguration reads extra[config_key].
     seed: Dict[str, Any] = {
     seed: Dict[str, Any] = {
-        ChattoConfiguration.base_url.config_key: (
+        ChattoConfiguration.base_url.field_name: (
             os.getenv(ChattoConfiguration.base_url.env_name) or ChattoClient.DEFAULT_BASE_URL
             os.getenv(ChattoConfiguration.base_url.env_name) or ChattoClient.DEFAULT_BASE_URL
         ).strip(),
         ).strip(),
     }
     }

+ 1 - 1
platform_config.py

@@ -27,7 +27,7 @@ import utils
 # own modules import each other absolutely ("from chattolib.x import y").
 # own modules import each other absolutely ("from chattolib.x import y").
 # Importing it relatively as well would load a *second* copy of every module
 # Importing it relatively as well would load a *second* copy of every module
 # under a different name, so isinstance() checks across the two would fail.
 # under a different name, so isinstance() checks across the two would fail.
-from chattolib.client import ChattoClient
+from chattolib import ChattoClient
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)