feat: add edit_message + streaming support to Zulip adapter (#33)
This commit was merged in pull request #33.
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -99,6 +99,23 @@ def _build_auth_header(email: str, api_key: str) -> str:
|
||||
return f"Basic {token}"
|
||||
|
||||
|
||||
def _strip_html(text: str) -> str:
|
||||
"""Strip HTML tags and decode HTML entities from Zulip message content.
|
||||
|
||||
Zulip delivers message content as rendered HTML (e.g. <p>/approve</p>).
|
||||
The gateway slash command parser expects plain text, so HTML tags
|
||||
prevent command matching. This helper strips tags and decodes entities.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
# Strip HTML tags
|
||||
text = re.sub(r"<[^>]+>", "", text)
|
||||
# Decode common HTML entities
|
||||
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
text = text.replace(""", '"').replace("'", "'").replace(" ", " ")
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _truncate(text: str, limit: int = MAX_ZULIP_MESSAGE) -> str:
|
||||
"""Truncate to Zulip's message limit with notice."""
|
||||
if len(text) <= limit:
|
||||
@@ -784,6 +801,8 @@ class ZulipAdapter(BasePlatformAdapter):
|
||||
sender_name = msg.get("sender_full_name", "Unknown")
|
||||
sender_id = msg.get("sender_id")
|
||||
content = msg.get("content", "")
|
||||
# Strip Zulip HTML for slash command matching (zulip-approval-fix contract)
|
||||
content = _strip_html(content)
|
||||
|
||||
# Echo-loop prevention: skip own messages
|
||||
if sender_email == self._bot_email:
|
||||
|
||||
Reference in New Issue
Block a user