From aa014229fd4e7a8464ccfcae93af16b554ecb0c4 Mon Sep 17 00:00:00 2001 From: Abiba Date: Mon, 6 Jul 2026 00:00:50 +0000 Subject: [PATCH] feat: add edit_message + streaming support to Zulip adapter Implements edit_message() using Zulip's PATCH /api/v1/messages/{id} API. Enables the Hermes GatewayStreamConsumer to progressively update Zulip messages during agent generation, giving users real-time visibility into agent thinking via progressive edits. Adds _api_patch() helper for PATCH HTTP method. --- hermes-zulip-plugin/src/adapter.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/hermes-zulip-plugin/src/adapter.py b/hermes-zulip-plugin/src/adapter.py index f302aaa..7542b9c 100644 --- a/hermes-zulip-plugin/src/adapter.py +++ b/hermes-zulip-plugin/src/adapter.py @@ -203,6 +203,22 @@ class ZulipAdapter(BasePlatformAdapter): logger.error("Zulip POST %s network error: %s", path, exc) return {"result": "error", "msg": str(exc)} + async def _api_patch(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: + import aiohttp + url = f"{self._site}/api/v1/{path.lstrip('/')}" + try: + async with self._session.patch( + url, data=payload, auth=self._auth(), + timeout=aiohttp.ClientTimeout(total=15), + ) as resp: + data = await resp.json() + if resp.status >= 400: + logger.debug("Zulip PATCH %s -> %s: %s", path, resp.status, str(data)[:200]) + return data + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + logger.debug("Zulip PATCH %s network error: %s", path, exc) + return {"result": "error", "msg": str(exc)} + async def _api_delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: import aiohttp url = f"{self._site}/api/v1/{path.lstrip('/')}" @@ -326,6 +342,23 @@ class ZulipAdapter(BasePlatformAdapter): return SendResult(success=True, message_id=str(last_id) if last_id else None) + async def edit_message( + self, + chat_id: str, + message_id: str, + content: str, + **kwargs, + ) -> SendResult: + """Edit a previously-sent message. Used by the gateway stream consumer + for progressive message updates during agent streaming.""" + formatted = self.format_message(content) + data = await self._api_patch(f"messages/{message_id}", {"content": formatted}) + if data.get("result") != "success": + msg = str(data.get("msg", "edit failed")) + logger.debug("Zulip edit_message(%s) -> %s", message_id, msg) + return SendResult(success=False, error=msg) + return SendResult(success=True, message_id=message_id) + async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: kind, to, topic = _parse_target(chat_id, self._default_topic) if kind == "direct":