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