-
Notifications
You must be signed in to change notification settings - Fork 1
feat(pipeline)!: require explicit prefixes for built-in LLM access #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1b0f6b4
d819e79
dc3178d
bbd91c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,26 +36,32 @@ async def handle_session_control_agent(self, event: AstrMessageEvent) -> None: | |
|
|
||
| @filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize - 1) | ||
| async def handle_empty_mention(self, event: AstrMessageEvent): | ||
| """处理只有一个 @ 或仅有唤醒前缀的消息,并等待用户下一条内容。""" | ||
| """Wait for the next message when the user sent only a command prefix.""" | ||
| try: | ||
| messages = event.get_messages() | ||
| cfg = self.context.config.get(umo=event.unified_msg_origin) | ||
| p_settings = cfg["platform_settings"] | ||
| command_prefixes = cfg.get("command_prefixes", []) | ||
| if len(messages) != 1: | ||
| return | ||
| if not p_settings.get("empty_mention_waiting", True): | ||
| return | ||
|
|
||
| is_empty_mention = ( | ||
| isinstance(messages[0], Comp.Mention) | ||
| and str(messages[0].target) == str(event.get_self_id()) | ||
| and p_settings.get("empty_mention_waiting", True) | ||
| ) | ||
| is_command_prefix_only = ( | ||
| isinstance(messages[0], Comp.Plain) | ||
| and messages[0].text.strip() in command_prefixes | ||
| ) | ||
| if not is_command_prefix_only: | ||
| return | ||
|
|
||
| if not (is_empty_mention or is_command_prefix_only): | ||
| llm_access = cfg.get("llm_access") or {} | ||
| mode = llm_access.get( | ||
| "private" if event.is_private_chat() else "group", | ||
| "prefix", | ||
| ) | ||
| if mode not in {"open", "prefix", "off"}: | ||
| mode = "prefix" | ||
| if mode == "off": | ||
| return | ||
|
|
||
| if p_settings.get("empty_mention_waiting_need_reply", True): | ||
|
|
@@ -78,7 +84,7 @@ async def handle_empty_mention(self, event: AstrMessageEvent): | |
|
|
||
| yield event.request_llm( | ||
| prompt=( | ||
| "注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。" | ||
| "注意,你正在社交媒体上中与用户进行聊天,用户只发送了指令前缀,尚未输入内容,他可能会在接下来一条发送他想发送的内容。" | ||
| "你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。" | ||
| "请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西" | ||
| ), | ||
|
|
@@ -91,25 +97,22 @@ async def handle_empty_mention(self, event: AstrMessageEvent): | |
| logger.error(f"LLM response failed: {e!s}") | ||
| yield event.plain_result("想要问什么呢?😄") | ||
|
|
||
| async def empty_mention_waiter( | ||
| async def prefix_only_waiter( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This waiter still Stamp a one-shot admit on |
||
| controller, | ||
| event: AstrMessageEvent, | ||
| ) -> None: | ||
| if not event.message_str or not event.message_str.strip(): | ||
| return | ||
| event.message_obj.message.insert( | ||
| 0, | ||
| Comp.Mention(target=event.get_self_id(), name=event.get_self_id()), | ||
| ) | ||
| new_event = copy.copy(event) | ||
| new_event.set_extra("explicit_surface", True) | ||
| self.context.messages.submit(new_event) | ||
| event.stop_event() | ||
| controller.stop() | ||
|
|
||
| try: | ||
| await self.context.messages.wait_for( | ||
| event, | ||
| empty_mention_waiter, | ||
| prefix_only_waiter, | ||
| timeout_seconds=60, | ||
| ) | ||
| except TimeoutError: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,11 +9,11 @@ | |
| CommandResolution, | ||
| CommandResolutionKind, | ||
| ) | ||
| from astrbot.core.message.components import Mention, MentionAll, Reply | ||
| from astrbot.core.message.components import Mention, Reply | ||
|
|
||
| RouteKind = Literal["ordinary", "passthrough", "turn_flush"] | ||
| PrivateAccess = Literal["open", "prefix", "off"] | ||
| GroupAccess = Literal["open", "prefix", "mention", "prefix_or_mention", "off"] | ||
| GroupAccess = Literal["open", "prefix", "off"] | ||
|
|
||
| INBOUND_FLUSH_KEYS = ("turn_flush", "turn_continuation") | ||
| MANAGER_FLUSH_TOKEN = "_turn_flush_token" | ||
|
|
@@ -37,7 +37,7 @@ class LlmAccess: | |
| """LLM access policy for one configuration profile.""" | ||
|
|
||
| prefixes: tuple[str, ...] = ("/",) | ||
| private: PrivateAccess = "open" | ||
| private: PrivateAccess = "prefix" | ||
| group: GroupAccess = "prefix" | ||
| reply_to_bot: bool = False | ||
|
|
||
|
|
@@ -56,8 +56,7 @@ class TurnRouteInput: | |
| is_notice_or_request: bool = False | ||
| has_open_window: bool = False | ||
| is_manager_flush: bool = False | ||
| adapter_preconfigured: bool = False | ||
| ignore_at_all: bool = False | ||
| explicit_surface: bool = False | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
|
|
@@ -126,11 +125,11 @@ def llm_access_from_config(config: dict) -> LlmAccess: | |
| prefixes = tuple( | ||
| str(item) for item in raw.get("prefixes", ["/"]) if str(item).strip() | ||
| ) | ||
| private = raw.get("private", "open") | ||
| private = raw.get("private", "prefix") | ||
| group = raw.get("group", "prefix") | ||
| if private not in {"open", "prefix", "off"}: | ||
| private = "open" | ||
| if group not in {"open", "prefix", "mention", "prefix_or_mention", "off"}: | ||
| private = "prefix" | ||
| if group not in {"open", "prefix", "off"}: | ||
| group = "prefix" | ||
| return LlmAccess( | ||
| prefixes=prefixes or ("/",), | ||
|
|
@@ -231,16 +230,18 @@ def route_turn(inp: TurnRouteInput) -> TurnRouteResult: | |
| resolution, | ||
| ) | ||
|
|
||
| if inp.adapter_preconfigured: | ||
| llm_text, reasons = _llm_payload(inp, blocked_by_other_mention) | ||
| return TurnRouteResult( | ||
| False, | ||
| True, | ||
| "ordinary", | ||
| frozenset({"adapter_preconfigured", *reasons}), | ||
| llm_text, | ||
| False, | ||
| ) | ||
| if inp.explicit_surface: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Honor |
||
| mode = inp.llm_access.private if inp.is_private else inp.llm_access.group | ||
| if mode != "off": | ||
| llm_text, reasons = _llm_payload(inp, blocked_by_other_mention) | ||
| return TurnRouteResult( | ||
| False, | ||
| True, | ||
| "ordinary", | ||
| frozenset({"explicit_surface", *reasons}), | ||
| llm_text, | ||
| False, | ||
| ) | ||
|
|
||
| llm_ok, reasons = _llm_gate(inp, blocked_by_other_mention) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Default waiting + Add |
||
| if llm_ok: | ||
|
|
@@ -287,60 +288,47 @@ def _first_mention_is_other(inp: TurnRouteInput) -> bool: | |
| return str(first.target) != str(inp.self_id) | ||
|
|
||
|
|
||
| def _llm_prefix_has_payload( | ||
| inp: TurnRouteInput, blocked_by_other_mention: bool | ||
| ) -> bool: | ||
| if blocked_by_other_mention: | ||
| return False | ||
| text = inp.message_str.strip(" \t") | ||
| prefix = longest_prefix_match(text, inp.llm_access.prefixes) | ||
| if prefix is None: | ||
| return False | ||
| return bool(text[len(prefix) :].strip(" \t")) | ||
|
|
||
|
|
||
| def _llm_gate( | ||
| inp: TurnRouteInput, blocked_by_other_mention: bool | ||
| ) -> tuple[bool, set[str]]: | ||
| if inp.has_open_window: | ||
| return True, {"turn_continuation"} | ||
| mentioned_bot, mentioned_all, reply_to_bot = _mention_flags(inp) | ||
| if inp.is_private: | ||
| mode = inp.llm_access.private | ||
| if mode == "open": | ||
| return True, {"llm_open"} | ||
| if mode == "off": | ||
| return False, set() | ||
| if blocked_by_other_mention: | ||
| return False, set() | ||
| if longest_prefix_match(inp.message_str.strip(" \t"), inp.llm_access.prefixes): | ||
| if _llm_prefix_has_payload(inp, False): | ||
| return True, {"llm_prefix"} | ||
| return False, set() | ||
|
|
||
| reasons: set[str] = set() | ||
| base = False | ||
| mode = inp.llm_access.group | ||
| prefix_hit = False | ||
| if not blocked_by_other_mention: | ||
| prefix_hit = ( | ||
| longest_prefix_match(inp.message_str.strip(" \t"), inp.llm_access.prefixes) | ||
| is not None | ||
| ) | ||
| prefix_hit = _llm_prefix_has_payload(inp, blocked_by_other_mention) | ||
| if mode == "open": | ||
| base = True | ||
| reasons.add("llm_open") | ||
| elif mode == "prefix": | ||
| base = prefix_hit | ||
| if prefix_hit: | ||
| reasons.add("llm_prefix") | ||
| elif mode == "mention": | ||
| base = mentioned_bot or mentioned_all | ||
| if mentioned_bot: | ||
| reasons.add("mention_bot") | ||
| if mentioned_all: | ||
| reasons.add("mention_all") | ||
| elif mode == "prefix_or_mention": | ||
| base = prefix_hit or mentioned_bot or mentioned_all | ||
| if prefix_hit: | ||
| reasons.add("llm_prefix") | ||
| if mentioned_bot: | ||
| reasons.add("mention_bot") | ||
| if mentioned_all: | ||
| reasons.add("mention_all") | ||
| if inp.llm_access.reply_to_bot and reply_to_bot: | ||
| if inp.llm_access.reply_to_bot and _reply_to_bot(inp): | ||
| base = True | ||
| reasons.add("reply_to_bot") | ||
| if mode != "off" and mentioned_all: | ||
| base = True | ||
| reasons.add("mention_all") | ||
| return base, reasons | ||
|
|
||
|
|
||
|
|
@@ -353,20 +341,13 @@ def _llm_payload( | |
| prefix = longest_prefix_match(text, inp.llm_access.prefixes) | ||
| if prefix is None: | ||
| return text, set() | ||
| return text[len(prefix) :].strip(" \t"), {"llm_prefix"} if prefix else set() | ||
| return text[len(prefix) :].strip(" \t"), {"llm_prefix"} | ||
|
|
||
|
|
||
| def _mention_flags(inp: TurnRouteInput) -> tuple[bool, bool, bool]: | ||
| mentioned_bot = False | ||
| mentioned_all = False | ||
| reply_to_bot = False | ||
| def _reply_to_bot(inp: TurnRouteInput) -> bool: | ||
| for message in inp.messages: | ||
| if isinstance(message, Mention) and str(message.target) == str(inp.self_id): | ||
| mentioned_bot = True | ||
| if isinstance(message, MentionAll) and not inp.ignore_at_all: | ||
| mentioned_all = True | ||
| if isinstance(message, Reply) and str( | ||
| getattr(message, "sender_id", "") or "" | ||
| ) == str(inp.self_id): | ||
| reply_to_bot = True | ||
| return mentioned_bot, mentioned_all, reply_to_bot | ||
| return True | ||
| return False | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
request_llmhere is a pluginProviderRequest.ProcessStageruns it whenever this handler is activated, independent ofshould_run_llm.cfgis already loaded. Ifllm_access.private/groupisofffor this UMO, return before the courtesy prompt and beforewait_for. Docs now calloffthe kill switch for this wait path; the follow-up stamp honors it, this yield does not.Cover
/+offsorequest_llmandwait_forare not called.