@inline-chat/hermes-agent-adapter 0.0.22 → 1.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -29
- package/dist/install.js +18 -3
- package/package.json +10 -4
- package/plugin/inline/adapter.py +396 -242
- package/plugin/inline/cli.py +73 -7
- package/plugin/inline/plugin.yaml +6 -10
- package/plugin/inline/profile.py +86 -0
- package/plugin/inline/sidecar/index.mjs +2464 -160
- package/plugin/inline/tools.py +261 -32
package/plugin/inline/tools.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import http.client
|
|
6
|
+
from collections import OrderedDict
|
|
5
7
|
import os
|
|
6
8
|
import re
|
|
7
9
|
import urllib.error
|
|
@@ -9,6 +11,7 @@ import urllib.request
|
|
|
9
11
|
from typing import Any, Dict, Iterable, Optional
|
|
10
12
|
|
|
11
13
|
from .message_actions import build_inline_agent_action_id
|
|
14
|
+
from .profile import hermes_home, inline_env, multiplex_active
|
|
12
15
|
|
|
13
16
|
try:
|
|
14
17
|
from tools.registry import tool_error, tool_result
|
|
@@ -76,11 +79,57 @@ INLINE_PLATFORM_GUIDANCE = (
|
|
|
76
79
|
"Use <footer>Attribution or brief metadata</footer> for a short footer."
|
|
77
80
|
)
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
# Where each turn's triggering message physically lives, and who sent it. Hermes tells tools
|
|
83
|
+
# the session's chat and message id, but a thread's opener is keyed on the thread while its
|
|
84
|
+
# message lives in the parent chat, so "this message" must be paired with its own chat.
|
|
85
|
+
_turn_messages: "OrderedDict[tuple[str, str, str, str], Dict[str, Any]]" = OrderedDict()
|
|
86
|
+
_TURN_MESSAGES_MAX = 1000
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def remember_turn_message(
|
|
90
|
+
*, session_chat_id: str, thread_id: str, message_id: str, chat_id: str, sender_id: str, sender_is_bot: bool,
|
|
91
|
+
chat_type: str = "", in_reply_thread: bool = False,
|
|
92
|
+
) -> None:
|
|
93
|
+
key = (str(hermes_home()), str(session_chat_id or ""), str(thread_id or ""), str(message_id or ""))
|
|
94
|
+
_turn_messages[key] = {
|
|
95
|
+
"chatId": str(chat_id), "senderId": str(sender_id or ""), "senderIsBot": bool(sender_is_bot),
|
|
96
|
+
"chatType": str(chat_type or ""), "inReplyThread": bool(in_reply_thread),
|
|
97
|
+
}
|
|
98
|
+
_turn_messages.move_to_end(key)
|
|
99
|
+
while len(_turn_messages) > _TURN_MESSAGES_MAX:
|
|
100
|
+
_turn_messages.popitem(last=False)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _current_turn() -> Dict[str, Any]:
|
|
104
|
+
"""This turn's message: its id, the chat it lives in, and its sender when known."""
|
|
105
|
+
message_id = _session_message_id()
|
|
106
|
+
session_chat = _session_chat_id(prefer_thread=False)
|
|
107
|
+
thread_id = _inline_id(_session_env("HERMES_SESSION_THREAD_ID", ""))
|
|
108
|
+
known = _turn_messages.get((str(hermes_home()), session_chat, thread_id, message_id)) or {}
|
|
109
|
+
# The turn already runs in a thread when reply threads moved it there (its message lives
|
|
110
|
+
# in the parent chat) or when its message was sent inside the thread.
|
|
111
|
+
in_thread = bool(thread_id) and (bool(known.get("inReplyThread")) or (known.get("chatId") or session_chat) != thread_id)
|
|
112
|
+
return {
|
|
113
|
+
"messageId": message_id,
|
|
114
|
+
"chatId": known.get("chatId") or session_chat,
|
|
115
|
+
"senderId": known.get("senderId") or "",
|
|
116
|
+
"senderIsBot": known.get("senderIsBot"),
|
|
117
|
+
"chatType": known.get("chatType") or "",
|
|
118
|
+
"inReplyThread": bool(known.get("inReplyThread")),
|
|
119
|
+
"threadId": thread_id if in_thread else "",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Live sidecars by profile home: one gateway process can serve several profiles,
|
|
124
|
+
# and a tool call must reach the sidecar of the bot it runs for.
|
|
125
|
+
_sidecars: Dict[str, Dict[str, Any]] = {}
|
|
126
|
+
# The adapter that owns each profile's Inline settings (reply threads), for the same reason.
|
|
127
|
+
_settings_owners: Dict[str, Any] = {}
|
|
80
128
|
|
|
81
129
|
_ACTION_MANIFEST = [
|
|
82
130
|
("send_message", "(chat_id?|user_id?, text, buttons?)", "Send a message, including optional agent-owned buttons."),
|
|
83
|
-
("get_chat", "(chat_id?)", "Get metadata for a chat or reply thread."),
|
|
131
|
+
("get_chat", "(chat_id?)", "Get metadata for a chat or reply thread, including whether you follow it."),
|
|
132
|
+
("set_follow_mode", "(chat_id?, mode)", "Follow a thread to join its conversation, or unfollow it to answer only mentions and replies. Applies to you only; mode is following or unfollowed."),
|
|
84
133
|
("get_messages", "(chat_id?, message_ids)", "Fetch specific Inline messages by ID."),
|
|
85
134
|
("get_history", "(chat_id?, limit?, anchor_id?)", "Fetch recent or anchored Inline history."),
|
|
86
135
|
("search_messages", "(chat_id?|user_id?, query, limit?, offset_id?)", "Search Inline message text in a chat, thread, or DM."),
|
|
@@ -92,8 +141,12 @@ _ACTION_MANIFEST = [
|
|
|
92
141
|
("pin_message", "(chat_id?|user_id?, message_id?)", "Pin an Inline message for the conversation."),
|
|
93
142
|
("unpin_message", "(chat_id?|user_id?, message_id?)", "Unpin an Inline message for the conversation."),
|
|
94
143
|
("list_pins", "(chat_id?)", "List pinned Inline message IDs for a chat or reply thread."),
|
|
95
|
-
("create_thread", "(parent_chat_id?, parent_message_id?, title?)", "Create an Inline reply thread."),
|
|
96
|
-
("
|
|
144
|
+
("create_thread", "(parent_chat_id?, parent_message_id?, title?)", "Create an Inline reply thread on a message. For the message you're answering, your answer goes into the thread; if this conversation already has its thread, you get that one back."),
|
|
145
|
+
("set_reply_threads", "(mode)", "Change where your answers go in this chat when someone asks you to: auto answers in the chat and starts a thread only when asked; on answers each message in a reply thread; default clears this chat's setting."),
|
|
146
|
+
("create_chat", "(title, space_id?, participant_user_ids?, is_public?, include_requester?)", "Create a top-level Inline thread/chat. A private chat includes the person who asked unless include_requester is false."),
|
|
147
|
+
("add_participants", "(chat_id?, user_ids)", "Add people or agents to a chat or thread you manage."),
|
|
148
|
+
("update_chat", "(chat_id?, title?, emoji?)", "Rename a chat or thread, or change its emoji."),
|
|
149
|
+
("delete_chat", "(chat_id)", "Delete a chat or thread you created. This can't be undone."),
|
|
97
150
|
("create_agent", "(name, skill_key?, instructions?)", "Create a named Inline Agent backed by this bot."),
|
|
98
151
|
("get_agent", "(agent_id)", "Inspect one Agent backed by this bot."),
|
|
99
152
|
("list_agents", "()", "List Agents backed by this bot."),
|
|
@@ -106,16 +159,43 @@ _PRESENCE_KINDS = ["idle", "happy", "waving", "jumping", "failed", "waiting", "r
|
|
|
106
159
|
|
|
107
160
|
|
|
108
161
|
def configure_sidecar(*, bind: str, port: int, token: str) -> None:
|
|
109
|
-
"""Store live adapter sidecar details for model tools in this
|
|
162
|
+
"""Store live adapter sidecar details for model tools in this profile."""
|
|
110
163
|
if not token:
|
|
111
164
|
return
|
|
112
|
-
|
|
165
|
+
_sidecars[str(hermes_home())] = {"bind": bind, "port": int(port), "token": token}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _current_sidecar() -> Dict[str, Any]:
|
|
169
|
+
current = _sidecars.get(str(hermes_home()))
|
|
170
|
+
if current is not None:
|
|
171
|
+
return current
|
|
172
|
+
# Only a gateway serving one profile may fall back to its one sidecar; with several
|
|
173
|
+
# profiles, another profile's sidecar would act as the wrong bot.
|
|
174
|
+
if len(_sidecars) == 1 and not multiplex_active():
|
|
175
|
+
return next(iter(_sidecars.values()))
|
|
176
|
+
if _sidecars:
|
|
177
|
+
raise InlineToolError("Inline is not connected for this profile", "forbidden")
|
|
178
|
+
return {}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def configure_settings(owner: Any) -> None:
|
|
182
|
+
"""Register the adapter that keeps this profile's Inline settings."""
|
|
183
|
+
_settings_owners[str(hermes_home())] = owner
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _current_settings_owner() -> Any:
|
|
187
|
+
current = _settings_owners.get(str(hermes_home()))
|
|
188
|
+
if current is not None or multiplex_active():
|
|
189
|
+
return current
|
|
190
|
+
return next(iter(_settings_owners.values())) if len(_settings_owners) == 1 else None
|
|
113
191
|
|
|
114
192
|
|
|
115
193
|
def check_inline_tool_requirements() -> bool:
|
|
116
|
-
if
|
|
194
|
+
if multiplex_active():
|
|
195
|
+
return str(hermes_home()) in _sidecars
|
|
196
|
+
if _sidecars or inline_env("INLINE_SIDECAR_TOKEN"):
|
|
117
197
|
return True
|
|
118
|
-
return bool(
|
|
198
|
+
return bool(inline_env("INLINE_TOKEN") or inline_env("INLINE_BOT_TOKEN"))
|
|
119
199
|
|
|
120
200
|
|
|
121
201
|
def inline_sender_guidance(
|
|
@@ -186,7 +266,8 @@ INLINE_TOOL_SCHEMA = {
|
|
|
186
266
|
"When chat_id is omitted, the tool uses the current Inline reply thread if present, otherwise the current chat. "
|
|
187
267
|
"Return normal assistant replies as text. Use send_message only for a button card or a different destination. "
|
|
188
268
|
"Agent-owned button presses return as ordinary turns, and the normal response replaces the source card while clearing omitted buttons. "
|
|
189
|
-
"Use create_thread
|
|
269
|
+
"Use create_thread on the current triggering message when someone asks to continue in a thread; your answer then goes into it, so don't also post it in the chat. "
|
|
270
|
+
"Use set_reply_threads when someone asks you to always or never answer in threads here. "
|
|
190
271
|
"Use create_chat to create a new top-level destination outside the current conversation; it defaults to private, and public creation must explicitly set is_public with a space_id. "
|
|
191
272
|
"Use search_messages for exact catch-up across older chat history. "
|
|
192
273
|
"Use pin_message/unpin_message only when the user explicitly asks because pins are durable shared-chat state. "
|
|
@@ -213,14 +294,14 @@ INLINE_TOOL_SCHEMA = {
|
|
|
213
294
|
"type": "string",
|
|
214
295
|
"description": "Parent message ID for create_thread. Defaults to the triggering message when available.",
|
|
215
296
|
},
|
|
216
|
-
"title": {"type": "string", "description": "Thread title. Required for create_chat and
|
|
297
|
+
"title": {"type": "string", "description": "Thread title. Required for create_chat, optional for create_thread, and the new name for update_chat."},
|
|
217
298
|
"name": {"type": "string", "description": "Agent name for create_agent."},
|
|
218
299
|
"agent_id": {"type": "string", "description": "Globally unique Inline Agent ID for get_agent, update_agent, or delete_agent."},
|
|
219
300
|
"handle": {"type": "string", "description": "Optional Agent handle."},
|
|
220
301
|
"skill_key": {"type": "string", "description": "Optional harness skill key for create_agent or update_agent."},
|
|
221
302
|
"instructions": {"type": "string", "description": "Optional specialized Agent instructions for create_agent or update_agent."},
|
|
222
303
|
"description": {"type": "string", "description": "Optional thread or Agent description."},
|
|
223
|
-
"emoji": {"type": "string", "description": "
|
|
304
|
+
"emoji": {"type": "string", "description": "Reaction emoji for reaction actions. For create_chat, create_thread and update_chat, one fitting emoji helps people spot the chat in their list; pass one unless the topic is sensitive or serious. Also an Agent's emoji."},
|
|
224
305
|
"space_id": {"type": "string", "description": "Optional parent space ID for create_chat."},
|
|
225
306
|
"participant_user_ids": {
|
|
226
307
|
"type": "array",
|
|
@@ -263,6 +344,9 @@ INLINE_TOOL_SCHEMA = {
|
|
|
263
344
|
"offset_id": {"type": "string", "description": "Offset message ID for search_messages pagination."},
|
|
264
345
|
"kind": {"type": "string", "enum": _PRESENCE_KINDS, "description": "Bot avatar presence/status kind."},
|
|
265
346
|
"comment": {"type": "string", "description": "Optional bot avatar presence/status message."},
|
|
347
|
+
"user_ids": {"type": "array", "items": {"type": "string"}, "description": "add_participants: Inline user IDs of the people or agents to add."},
|
|
348
|
+
"include_requester": {"type": "boolean", "description": "create_chat: include the person who asked (default true for private chats). Set false only for a deliberately bot-only chat."},
|
|
349
|
+
"mode": {"type": "string", "enum": ["following", "unfollowed", "auto", "on", "default"], "description": "set_follow_mode: following joins the thread's conversation; unfollowed limits you to mentions and replies. set_reply_threads: auto, on, or default to clear this chat's setting."},
|
|
266
350
|
},
|
|
267
351
|
"required": ["action"],
|
|
268
352
|
},
|
|
@@ -290,6 +374,12 @@ def _handle_inline_tool(args: Dict[str, Any], **_: Any) -> str:
|
|
|
290
374
|
return tool_error(f"inline: unknown action {action or '(empty)'}", allowed_actions=_ACTIONS)
|
|
291
375
|
|
|
292
376
|
try:
|
|
377
|
+
if action == "set_follow_mode":
|
|
378
|
+
return tool_result({"success": True, "action": action, "result": _set_follow_mode(args)})
|
|
379
|
+
if action == "set_reply_threads":
|
|
380
|
+
return tool_result({"success": True, "action": action, "result": _set_reply_threads(args)})
|
|
381
|
+
if action == "create_thread":
|
|
382
|
+
return tool_result({"success": True, "action": action, "result": _create_thread(args)})
|
|
293
383
|
path, body = _request_for_action(action, args)
|
|
294
384
|
response = _sidecar_call(path, body)
|
|
295
385
|
result = _compact_result(action, response.get("result") or {})
|
|
@@ -359,9 +449,10 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
359
449
|
}
|
|
360
450
|
|
|
361
451
|
if action in {"add_reaction", "remove_reaction"}:
|
|
452
|
+
target, message_id = _message_target(args)
|
|
362
453
|
body = {
|
|
363
|
-
"target":
|
|
364
|
-
"messageId":
|
|
454
|
+
"target": target,
|
|
455
|
+
"messageId": message_id,
|
|
365
456
|
"emoji": _required_str(args, "emoji", max_chars=64),
|
|
366
457
|
}
|
|
367
458
|
if action == "remove_reaction":
|
|
@@ -369,16 +460,12 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
369
460
|
return "/reaction", body
|
|
370
461
|
|
|
371
462
|
if action == "get_reactions":
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"messageId": _message_id_or_current(args),
|
|
375
|
-
}
|
|
463
|
+
target, message_id = _message_target(args)
|
|
464
|
+
return "/reactions", {"target": target, "messageId": message_id}
|
|
376
465
|
|
|
377
466
|
if action in {"pin_message", "unpin_message"}:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
"messageId": _message_id_or_current(args),
|
|
381
|
-
}
|
|
467
|
+
target, message_id = _message_target(args)
|
|
468
|
+
body = {"target": target, "messageId": message_id}
|
|
382
469
|
if action == "unpin_message":
|
|
383
470
|
body["unpin"] = True
|
|
384
471
|
return "/pin", body
|
|
@@ -387,11 +474,15 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
387
474
|
return "/pins", {"target": _target(args)}
|
|
388
475
|
|
|
389
476
|
if action == "create_thread":
|
|
390
|
-
|
|
477
|
+
turn = _current_turn()
|
|
478
|
+
parent_chat_id = _inline_id(args.get("parent_chat_id")) or turn["chatId"]
|
|
391
479
|
if not parent_chat_id:
|
|
392
480
|
raise InlineToolError("create_thread requires parent_chat_id or current Inline chat context", "bad_format")
|
|
393
481
|
body = {"parentChatId": parent_chat_id}
|
|
394
|
-
|
|
482
|
+
# Message ids are per chat: anchor on this turn's message only inside its own chat.
|
|
483
|
+
parent_message_id = _inline_id(args.get("parent_message_id")) or (
|
|
484
|
+
turn["messageId"] if parent_chat_id == turn["chatId"] else ""
|
|
485
|
+
)
|
|
395
486
|
if parent_message_id:
|
|
396
487
|
body["parentMessageId"] = parent_message_id
|
|
397
488
|
for key in ("title", "description", "emoji"):
|
|
@@ -414,6 +505,12 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
414
505
|
raise InlineToolError("public create_chat requires space_id", "bad_format")
|
|
415
506
|
if participant_user_ids:
|
|
416
507
|
raise InlineToolError("public create_chat cannot include participant_user_ids", "bad_format")
|
|
508
|
+
if not body["isPublic"] and _bool(args.get("include_requester"), True):
|
|
509
|
+
# The person who asked can open the chat the agent made for them (Jack, J13).
|
|
510
|
+
turn = _current_turn()
|
|
511
|
+
requester = _inline_id(turn["senderId"])
|
|
512
|
+
if requester and turn["senderIsBot"] is False and requester not in participant_user_ids:
|
|
513
|
+
participant_user_ids = [requester, *participant_user_ids]
|
|
417
514
|
body["participantUserIds"] = participant_user_ids
|
|
418
515
|
for key, limit in (("description", 1000), ("emoji", 16)):
|
|
419
516
|
value = _str(args.get(key))
|
|
@@ -421,6 +518,28 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
421
518
|
body[key] = _truncate(value, limit)
|
|
422
519
|
return "/create-chat", body
|
|
423
520
|
|
|
521
|
+
if action == "add_participants":
|
|
522
|
+
user_ids = _id_list(args.get("user_ids"), max_items=50)
|
|
523
|
+
if not user_ids:
|
|
524
|
+
raise InlineToolError("add_participants requires user_ids", "bad_format")
|
|
525
|
+
return "/add-participants", {"chatId": _chat_id_or_current(args), "userIds": user_ids}
|
|
526
|
+
|
|
527
|
+
if action == "update_chat":
|
|
528
|
+
body = {"chatId": _chat_id_or_current(args)}
|
|
529
|
+
title = _str(args.get("title"))
|
|
530
|
+
emoji = _str(args.get("emoji"))
|
|
531
|
+
if title:
|
|
532
|
+
body["title"] = _truncate(title, 200)
|
|
533
|
+
if emoji:
|
|
534
|
+
body["emoji"] = _truncate(emoji, 16)
|
|
535
|
+
if len(body) == 1:
|
|
536
|
+
raise InlineToolError("update_chat requires title or emoji", "bad_format")
|
|
537
|
+
return "/update-chat", body
|
|
538
|
+
|
|
539
|
+
if action == "delete_chat":
|
|
540
|
+
# Deleting is never implied by the current chat: name it.
|
|
541
|
+
return "/delete-chat", {"chatId": _required_id(args, "chat_id")}
|
|
542
|
+
|
|
424
543
|
if action == "create_agent":
|
|
425
544
|
body = {"name": _required_str(args, "name", max_chars=256)}
|
|
426
545
|
for key in ("handle", "emoji", "description", "skill_key", "instructions"):
|
|
@@ -462,6 +581,94 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
462
581
|
raise InlineToolError(f"inline: unimplemented action {action}", "bad_format")
|
|
463
582
|
|
|
464
583
|
|
|
584
|
+
def _set_follow_mode(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
585
|
+
mode = _str(args.get("mode")).lower()
|
|
586
|
+
if mode not in {"following", "unfollowed"}:
|
|
587
|
+
raise InlineToolError("set_follow_mode requires mode: following or unfollowed", "bad_format")
|
|
588
|
+
target = _target(args)
|
|
589
|
+
if "chatId" not in target:
|
|
590
|
+
raise InlineToolError("Follow state applies to threads, not direct messages", "unsupported")
|
|
591
|
+
write_error: Optional[InlineToolError] = None
|
|
592
|
+
try:
|
|
593
|
+
_sidecar_call("/follow-mode", {"target": target, "mode": mode})
|
|
594
|
+
except InlineToolError as exc:
|
|
595
|
+
if exc.error_kind != "transient":
|
|
596
|
+
raise
|
|
597
|
+
write_error = exc
|
|
598
|
+
# Report the state the server holds, not the one requested: a lost response can
|
|
599
|
+
# still have applied the change.
|
|
600
|
+
chat = _sidecar_call("/chat", {"target": target}).get("result") or {}
|
|
601
|
+
actual = _follow_mode_from_chat(chat)
|
|
602
|
+
if actual == mode:
|
|
603
|
+
return {"chatId": target["chatId"], "mode": actual, "message": _FOLLOW_CONFIRMATIONS[mode]}
|
|
604
|
+
if write_error:
|
|
605
|
+
raise write_error
|
|
606
|
+
raise InlineToolError(f"Follow state is {actual or 'default'} after the change", "unknown")
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _set_reply_threads(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
610
|
+
mode = _str(args.get("mode")).lower()
|
|
611
|
+
if mode not in {"auto", "on", "default"}:
|
|
612
|
+
raise InlineToolError("set_reply_threads requires mode auto, on or default", "bad_format")
|
|
613
|
+
owner = _current_settings_owner()
|
|
614
|
+
if owner is None:
|
|
615
|
+
raise InlineToolError("Inline settings aren't available for this profile", "unsupported")
|
|
616
|
+
turn = _current_turn()
|
|
617
|
+
if not turn.get("senderId"):
|
|
618
|
+
raise InlineToolError("Only the person who asked can change reply threads, and this turn has no sender", "forbidden")
|
|
619
|
+
try:
|
|
620
|
+
return owner.set_reply_threads_from_tool(
|
|
621
|
+
chat_id=turn["chatId"],
|
|
622
|
+
chat_type=turn.get("chatType") or "group",
|
|
623
|
+
in_reply_thread=bool(turn.get("inReplyThread")),
|
|
624
|
+
actor_id=turn["senderId"],
|
|
625
|
+
actor_is_bot=bool(turn.get("senderIsBot")),
|
|
626
|
+
mode=mode,
|
|
627
|
+
)
|
|
628
|
+
except PermissionError as exc:
|
|
629
|
+
raise InlineToolError(str(exc), "forbidden") from exc
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _create_thread(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
633
|
+
"""One conversation, one thread. A thread for the message being answered is this turn's
|
|
634
|
+
thread: if the turn already runs in one (reply threads on, or a message inside a thread),
|
|
635
|
+
that thread comes back instead of a second one, and a new one receives the turn's answer."""
|
|
636
|
+
path, body = _request_for_action("create_thread", args)
|
|
637
|
+
turn = _current_turn()
|
|
638
|
+
owner = _current_settings_owner()
|
|
639
|
+
for_this_turn = bool(turn["messageId"]) and body["parentChatId"] == turn["chatId"] and (
|
|
640
|
+
body.get("parentMessageId") in {None, turn["messageId"]}
|
|
641
|
+
)
|
|
642
|
+
if for_this_turn:
|
|
643
|
+
existing = turn.get("threadId") or (owner.turn_thread(turn["chatId"], turn["messageId"]) if owner else "")
|
|
644
|
+
if existing:
|
|
645
|
+
return {"chatId": existing, "existing": True, "message": "This conversation already has its thread; your answer goes there."}
|
|
646
|
+
result = _compact_result("create_thread", _sidecar_call(path, body).get("result") or {})
|
|
647
|
+
thread_id = _inline_id(result.get("chatId"))
|
|
648
|
+
if for_this_turn and owner is not None and thread_id:
|
|
649
|
+
owner.note_turn_thread(turn["chatId"], turn["messageId"], thread_id)
|
|
650
|
+
return result
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
_FOLLOW_CONFIRMATIONS = {
|
|
654
|
+
"following": "Following this chat.",
|
|
655
|
+
"unfollowed": "Unfollowed; mentions and replies still reach me.",
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _follow_mode_from_chat(chat: Dict[str, Any]) -> Optional[str]:
|
|
660
|
+
value = chat.get("dialogFollowMode") if isinstance(chat, dict) else None
|
|
661
|
+
dialog = chat.get("dialog") if isinstance(chat, dict) else None
|
|
662
|
+
if value is None and isinstance(dialog, dict):
|
|
663
|
+
value = dialog.get("followMode")
|
|
664
|
+
text = _str(value).lower()
|
|
665
|
+
if text in {"1", "following"}:
|
|
666
|
+
return "following"
|
|
667
|
+
if text in {"2", "unfollowed"}:
|
|
668
|
+
return "unfollowed"
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
465
672
|
def _sidecar_call(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
466
673
|
base_url, token = _sidecar_config()
|
|
467
674
|
request = urllib.request.Request(
|
|
@@ -474,13 +681,16 @@ def _sidecar_call(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
474
681
|
method="POST",
|
|
475
682
|
)
|
|
476
683
|
try:
|
|
477
|
-
|
|
684
|
+
# IPC stays on loopback even when the host configures an HTTP proxy.
|
|
685
|
+
# The sidecar token must never be forwarded to that proxy.
|
|
686
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
687
|
+
with opener.open(request, timeout=15) as response:
|
|
478
688
|
raw = response.read().decode("utf-8")
|
|
479
689
|
status = response.status
|
|
480
690
|
except urllib.error.HTTPError as exc:
|
|
481
691
|
raw = exc.read().decode("utf-8", errors="replace")
|
|
482
692
|
status = exc.code
|
|
483
|
-
except
|
|
693
|
+
except (OSError, http.client.HTTPException) as exc:
|
|
484
694
|
raise InlineToolError(f"Inline sidecar is not reachable: {exc}", "transient") from exc
|
|
485
695
|
|
|
486
696
|
try:
|
|
@@ -496,11 +706,12 @@ def _sidecar_call(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
496
706
|
|
|
497
707
|
|
|
498
708
|
def _sidecar_config() -> tuple[str, str]:
|
|
499
|
-
|
|
709
|
+
_sidecar = _current_sidecar()
|
|
710
|
+
token = _str(_sidecar.get("token")) or _str(inline_env("INLINE_SIDECAR_TOKEN"))
|
|
500
711
|
if not token:
|
|
501
712
|
raise InlineToolError("Inline sidecar token is not configured; start the Inline gateway adapter first", "forbidden")
|
|
502
|
-
bind = _normalize_bind(_sidecar.get("bind") or
|
|
503
|
-
port = _normalize_port(_sidecar.get("port") or
|
|
713
|
+
bind = _normalize_bind(_sidecar.get("bind") or inline_env("INLINE_SIDECAR_BIND"))
|
|
714
|
+
port = _normalize_port(_sidecar.get("port") or inline_env("INLINE_SIDECAR_PORT"))
|
|
504
715
|
host = f"[{bind}]" if ":" in bind and not bind.startswith("[") else bind
|
|
505
716
|
return f"http://{host}:{port}", token
|
|
506
717
|
|
|
@@ -570,11 +781,27 @@ def _message_ids(args: Dict[str, Any]) -> list[str]:
|
|
|
570
781
|
return ids
|
|
571
782
|
|
|
572
783
|
|
|
573
|
-
def
|
|
574
|
-
|
|
575
|
-
|
|
784
|
+
def _message_target(args: Dict[str, Any]) -> tuple[Dict[str, str], str]:
|
|
785
|
+
"""Target and message id; "this message" always goes with the chat it lives in."""
|
|
786
|
+
explicit = _inline_id(args.get("message_id"))
|
|
787
|
+
if explicit:
|
|
788
|
+
return _target(args), explicit
|
|
789
|
+
turn = _current_turn()
|
|
790
|
+
if not turn["messageId"]:
|
|
791
|
+
raise InlineToolError("message_id is required outside an Inline message context", "bad_format")
|
|
792
|
+
own_chat = {"chatId": turn["chatId"]} if turn["chatId"] else None
|
|
793
|
+
if (_inline_id(args.get("chat_id")) or _inline_id(args.get("user_id"))) and _target(args) != own_chat:
|
|
794
|
+
raise InlineToolError("message_id is required when targeting a different chat", "bad_format")
|
|
795
|
+
if own_chat is None:
|
|
576
796
|
raise InlineToolError("message_id is required outside an Inline message context", "bad_format")
|
|
577
|
-
return
|
|
797
|
+
return own_chat, turn["messageId"]
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _chat_id_or_current(args: Dict[str, Any]) -> str:
|
|
801
|
+
chat_id = _inline_id(args.get("chat_id")) or _session_chat_id(prefer_thread=True)
|
|
802
|
+
if not chat_id:
|
|
803
|
+
raise InlineToolError("chat_id is required outside an Inline chat", "bad_format")
|
|
804
|
+
return chat_id
|
|
578
805
|
|
|
579
806
|
|
|
580
807
|
def _message_actions(args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
@@ -723,6 +950,8 @@ def _compact_chat(chat: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
723
950
|
]:
|
|
724
951
|
if source in chat and chat[source] is not None and dest not in out:
|
|
725
952
|
out[dest] = chat[source]
|
|
953
|
+
if "chatId" in out:
|
|
954
|
+
out["followMode"] = _follow_mode_from_chat(chat) or "default"
|
|
726
955
|
return out
|
|
727
956
|
|
|
728
957
|
|