mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(photon): remove reply-to support and fix typing API
Drop `replyTo` from all outbound send paths and update the `/typing`
endpoint to use the documented `typing("start" | "stop")` content
builder. Adds a `stop_typing` method on the adapter to pair with
`send_typing`.
This commit is contained in:
parent
e79e44af79
commit
0d25cae041
2 changed files with 34 additions and 58 deletions
|
|
@ -629,7 +629,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
reply_to: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> SendResult:
|
||||
return await self._sidecar_send(chat_id, content, reply_to=reply_to)
|
||||
return await self._sidecar_send(chat_id, content)
|
||||
|
||||
# -- Outbound media (parity with the BlueBubbles iMessage channel) -----
|
||||
#
|
||||
|
|
@ -655,7 +655,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
# Couldn't fetch the URL — fall back to sending it as text.
|
||||
return await super().send_image(chat_id, image_url, caption, reply_to)
|
||||
return await self._sidecar_send_attachment(
|
||||
chat_id, local_path, caption=caption, reply_to=reply_to,
|
||||
chat_id, local_path, caption=caption,
|
||||
)
|
||||
|
||||
async def send_image_file(
|
||||
|
|
@ -668,7 +668,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
**kwargs,
|
||||
) -> SendResult:
|
||||
return await self._sidecar_send_attachment(
|
||||
chat_id, image_path, caption=caption, reply_to=reply_to,
|
||||
chat_id, image_path, caption=caption,
|
||||
)
|
||||
|
||||
async def send_voice(
|
||||
|
|
@ -681,7 +681,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
**kwargs,
|
||||
) -> SendResult:
|
||||
return await self._sidecar_send_attachment(
|
||||
chat_id, audio_path, caption=caption, reply_to=reply_to, kind="voice",
|
||||
chat_id, audio_path, caption=caption, kind="voice",
|
||||
)
|
||||
|
||||
async def send_video(
|
||||
|
|
@ -694,7 +694,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
**kwargs,
|
||||
) -> SendResult:
|
||||
return await self._sidecar_send_attachment(
|
||||
chat_id, video_path, caption=caption, reply_to=reply_to,
|
||||
chat_id, video_path, caption=caption,
|
||||
)
|
||||
|
||||
async def send_document(
|
||||
|
|
@ -708,7 +708,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
**kwargs,
|
||||
) -> SendResult:
|
||||
return await self._sidecar_send_attachment(
|
||||
chat_id, file_path, name=file_name, caption=caption, reply_to=reply_to,
|
||||
chat_id, file_path, name=file_name, caption=caption,
|
||||
)
|
||||
|
||||
async def send_animation(
|
||||
|
|
@ -726,10 +726,20 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
|
||||
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
||||
try:
|
||||
await self._sidecar_call("/typing", {"spaceId": chat_id})
|
||||
await self._sidecar_call(
|
||||
"/typing", {"spaceId": chat_id, "state": "start"}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("[photon] send_typing failed: %s", e)
|
||||
|
||||
async def stop_typing(self, chat_id: str) -> None:
|
||||
try:
|
||||
await self._sidecar_call(
|
||||
"/typing", {"spaceId": chat_id, "state": "stop"}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("[photon] stop_typing failed: %s", e)
|
||||
|
||||
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
||||
"""Return whatever we know about a Spectrum space id.
|
||||
|
||||
|
|
@ -738,9 +748,7 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
"""
|
||||
return {"name": chat_id, "type": "dm", "id": chat_id}
|
||||
|
||||
async def _sidecar_send(
|
||||
self, space_id: str, text: str, *, reply_to: Optional[str] = None,
|
||||
) -> SendResult:
|
||||
async def _sidecar_send(self, space_id: str, text: str) -> SendResult:
|
||||
if len(text) > self.MAX_MESSAGE_LENGTH:
|
||||
logger.warning(
|
||||
"[photon] truncating outbound from %d to %d chars",
|
||||
|
|
@ -748,8 +756,6 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
)
|
||||
text = text[: self.MAX_MESSAGE_LENGTH]
|
||||
body: Dict[str, Any] = {"spaceId": space_id, "text": text}
|
||||
if reply_to:
|
||||
body["replyTo"] = reply_to
|
||||
try:
|
||||
data = await self._sidecar_call("/send", body)
|
||||
except Exception as e:
|
||||
|
|
@ -764,7 +770,6 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
name: Optional[str] = None,
|
||||
mime_type: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
kind: str = "attachment",
|
||||
) -> SendResult:
|
||||
"""POST a local file to the sidecar's ``/send-attachment`` endpoint.
|
||||
|
|
@ -799,8 +804,6 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
body["mimeType"] = mime_type
|
||||
if caption:
|
||||
body["caption"] = caption
|
||||
if reply_to:
|
||||
body["replyTo"] = reply_to
|
||||
try:
|
||||
data = await self._sidecar_call("/send-attachment", body)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -11,21 +11,21 @@
|
|||
// loopback `GET /inbound` (NDJSON). We pause pulling from the stream while
|
||||
// no consumer is attached so a backlog isn't pulled-and-lost before the
|
||||
// gateway connects.
|
||||
// Outbound (Hermes -> gRPC): `/send` and `/typing` drive `space.send(...)` /
|
||||
// `space.startTyping()` on the SDK.
|
||||
// Outbound (Hermes -> gRPC): `/send` drives `space.send(...)`; `/typing`
|
||||
// sends the documented `typing("start" | "stop")` content builder.
|
||||
//
|
||||
// Protocol (all requests require `X-Hermes-Sidecar-Token: ${TOKEN}`):
|
||||
// - GET /inbound -> 200 NDJSON stream; one JSON event per line, blank
|
||||
// lines are heartbeats. One consumer at a time.
|
||||
// - POST /healthz -> {"ok": true}
|
||||
// - POST /send -> {"ok": true, "messageId": "..."}
|
||||
// body: {"spaceId": "...", "text": "...", "replyTo": "..." | null}
|
||||
// body: {"spaceId": "...", "text": "..."}
|
||||
// - POST /send-attachment -> {"ok": true, "messageId": "..."}
|
||||
// body: {"spaceId": "...", "path": "...", "name": "..." | null,
|
||||
// "mimeType": "..." | null, "caption": "..." | null,
|
||||
// "kind": "attachment" | "voice", "replyTo": "..." | null}
|
||||
// "kind": "attachment" | "voice"}
|
||||
// - POST /typing -> {"ok": true}
|
||||
// body: {"spaceId": "..."}
|
||||
// body: {"spaceId": "...", "state": "start" | "stop"}
|
||||
// - POST /shutdown -> {"ok": true}; then process exits
|
||||
//
|
||||
// On SIGINT/SIGTERM the sidecar calls `app.stop()` (3s graceful) before
|
||||
|
|
@ -69,14 +69,14 @@ if (!projectId || !projectSecret || !sharedToken) {
|
|||
|
||||
// Lazy-load spectrum-ts so a missing install fails with a clear message
|
||||
// instead of a cryptic module-resolution error during import.
|
||||
let Spectrum, imessage, attachment, voice, spectrumReply, spectrumText;
|
||||
let Spectrum, imessage, attachment, voice, spectrumText, spectrumTyping;
|
||||
try {
|
||||
({
|
||||
Spectrum,
|
||||
attachment,
|
||||
voice,
|
||||
reply: spectrumReply,
|
||||
text: spectrumText,
|
||||
typing: spectrumTyping,
|
||||
} = await import("spectrum-ts"));
|
||||
({ imessage } = await import("spectrum-ts/providers/imessage"));
|
||||
} catch (e) {
|
||||
|
|
@ -401,28 +401,6 @@ async function resolveSpace(spaceId) {
|
|||
throw new Error(`unable to resolve space id ${spaceId}`);
|
||||
}
|
||||
|
||||
async function maybeReplyContent(space, builder, replyTo) {
|
||||
if (!replyTo) return builder;
|
||||
if (typeof space.getMessage !== "function") {
|
||||
console.error("photon-sidecar: reply requested but space.getMessage is unavailable");
|
||||
return builder;
|
||||
}
|
||||
try {
|
||||
const target = await space.getMessage(replyTo);
|
||||
if (!target) {
|
||||
console.error(`photon-sidecar: reply target ${replyTo} not found; sending normally`);
|
||||
return builder;
|
||||
}
|
||||
return spectrumReply(builder, target);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"photon-sidecar: failed to resolve reply target; sending normally: " +
|
||||
(e && e.stack ? e.stack : String(e))
|
||||
);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.headers["x-hermes-sidecar-token"] !== sharedToken) {
|
||||
return unauthorized(res);
|
||||
|
|
@ -446,17 +424,16 @@ const server = http.createServer(async (req, res) => {
|
|||
}
|
||||
const body = await readBody(req);
|
||||
if (req.url === "/send") {
|
||||
const { spaceId, text, replyTo } = body || {};
|
||||
const { spaceId, text } = body || {};
|
||||
if (!spaceId || typeof text !== "string") {
|
||||
return badRequest(res, "spaceId and text are required");
|
||||
}
|
||||
const space = await resolveSpace(spaceId);
|
||||
const content = await maybeReplyContent(space, spectrumText(text), replyTo);
|
||||
const result = await space.send(content);
|
||||
const result = await space.send(spectrumText(text));
|
||||
return ok(res, { messageId: result?.id || result?.messageId || null });
|
||||
}
|
||||
if (req.url === "/send-attachment") {
|
||||
const { spaceId, path, name, mimeType, caption, kind, replyTo } =
|
||||
const { spaceId, path, name, mimeType, caption, kind } =
|
||||
body || {};
|
||||
if (!spaceId || typeof path !== "string" || !path) {
|
||||
return badRequest(res, "spaceId and path are required");
|
||||
|
|
@ -474,8 +451,7 @@ const server = http.createServer(async (req, res) => {
|
|||
? voice(path, Object.keys(opts).length ? opts : undefined)
|
||||
: attachment(path, Object.keys(opts).length ? opts : undefined);
|
||||
|
||||
const content = await maybeReplyContent(space, builder, replyTo);
|
||||
const result = await space.send(content);
|
||||
const result = await space.send(builder);
|
||||
|
||||
// iMessage delivers the caption as a separate bubble; send it
|
||||
// after the media so the attachment renders first.
|
||||
|
|
@ -492,16 +468,13 @@ const server = http.createServer(async (req, res) => {
|
|||
return ok(res, { messageId: result?.id || result?.messageId || null });
|
||||
}
|
||||
if (req.url === "/typing") {
|
||||
const { spaceId } = body || {};
|
||||
const { spaceId, state = "start" } = body || {};
|
||||
if (!spaceId) return badRequest(res, "spaceId is required");
|
||||
const space = await resolveSpace(spaceId);
|
||||
if (typeof space.startTyping === "function") {
|
||||
await space.startTyping();
|
||||
} else if (typeof space.typing === "function") {
|
||||
await space.typing();
|
||||
} else if (typeof space.setTyping === "function") {
|
||||
await space.setTyping(true);
|
||||
if (state !== "start" && state !== "stop") {
|
||||
return badRequest(res, "state must be start or stop");
|
||||
}
|
||||
const space = await resolveSpace(spaceId);
|
||||
await space.send(spectrumTyping(state));
|
||||
return ok(res, {});
|
||||
}
|
||||
res.statusCode = 404;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue