From 6f08c22b5b85c8bb9d9cacc0a8b6bf25533bc890 Mon Sep 17 00:00:00 2001 From: passthem Date: Fri, 21 Nov 2025 16:13:38 +0800 Subject: [PATCH] =?UTF-8?q?LLM=20=E8=83=9C=E5=88=A9=E4=BA=86=EF=BC=81?= =?UTF-8?q?=EF=BC=81=EF=BC=81=EF=BC=81=EF=BC=81=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/common/ptimeparse/README.md | 3 + konabot/common/ptimeparse/__init__.py | 18 +--- konabot/common/ptimeparse/ptime_ast.py | 7 +- konabot/common/ptimeparse/semantic.py | 4 +- konabot/plugins/bilibili_fetch/__init__.py | 25 ++--- konabot/plugins/nya_echo/__init__.py | 11 +- konabot/plugins/simple_notify/__init__.py | 20 ++-- konabot/plugins/simple_notify/ask_llm.py | 120 +++++++++++++++++++++ 8 files changed, 149 insertions(+), 59 deletions(-) create mode 100644 konabot/common/ptimeparse/README.md create mode 100644 konabot/plugins/simple_notify/ask_llm.py diff --git a/konabot/common/ptimeparse/README.md b/konabot/common/ptimeparse/README.md new file mode 100644 index 0000000..18cf586 --- /dev/null +++ b/konabot/common/ptimeparse/README.md @@ -0,0 +1,3 @@ +# 已废弃 + +坏枪用简单的 LLM + 提示词工程,完成了这 200 块的 `qwen3-coder-plus` 都搞不定的 nb 功能 diff --git a/konabot/common/ptimeparse/__init__.py b/konabot/common/ptimeparse/__init__.py index 0bb1cac..adf02f8 100644 --- a/konabot/common/ptimeparse/__init__.py +++ b/konabot/common/ptimeparse/__init__.py @@ -9,7 +9,6 @@ import datetime from typing import Optional from .expression import TimeExpression -from .err import TokenUnhandledException, MultipleSpecificationException def parse(text: str, now: Optional[datetime.datetime] = None) -> datetime.datetime: @@ -56,19 +55,4 @@ class Parser: TokenUnhandledException: If the input cannot be parsed """ return TimeExpression.parse(text, self.now) - - def digest_chinese_number(self, text: str) -> tuple[str, int]: - """ - Parse a Chinese number from the beginning of text and return the rest and the parsed number. - - This matches the interface of the original digest_chinese_number method. - - Args: - text: Text that may start with a Chinese number - - Returns: - Tuple of (remaining_text, parsed_number) - """ - from .chinese_number import ChineseNumberParser - parser = ChineseNumberParser() - return parser.digest(text) \ No newline at end of file + diff --git a/konabot/common/ptimeparse/ptime_ast.py b/konabot/common/ptimeparse/ptime_ast.py index e2de608..ad09ddf 100644 --- a/konabot/common/ptimeparse/ptime_ast.py +++ b/konabot/common/ptimeparse/ptime_ast.py @@ -2,10 +2,9 @@ Abstract Syntax Tree (AST) nodes for the time expression parser. """ -from abc import ABC, abstractmethod -from typing import Optional, List +from abc import ABC +from typing import Optional from dataclasses import dataclass -import datetime @dataclass @@ -69,4 +68,4 @@ class TimeExpressionNode(ASTNode): time: Optional[TimeNode] = None relative_date: Optional[RelativeDateNode] = None relative_time: Optional[RelativeTimeNode] = None - weekday: Optional[WeekdayNode] = None \ No newline at end of file + weekday: Optional[WeekdayNode] = None diff --git a/konabot/common/ptimeparse/semantic.py b/konabot/common/ptimeparse/semantic.py index fb1bd12..ef5b790 100644 --- a/konabot/common/ptimeparse/semantic.py +++ b/konabot/common/ptimeparse/semantic.py @@ -10,7 +10,7 @@ from .ptime_ast import ( TimeExpressionNode, DateNode, TimeNode, RelativeDateNode, RelativeTimeNode, WeekdayNode, NumberNode ) -from .err import TokenUnhandledException, MultipleSpecificationException +from .err import TokenUnhandledException class SemanticAnalyzer: @@ -366,4 +366,4 @@ class SemanticAnalyzer: smart_time = self.infer_smart_time(time.hour, time.minute, time.second, base_time=result) result = smart_time - return result \ No newline at end of file + return result diff --git a/konabot/plugins/bilibili_fetch/__init__.py b/konabot/plugins/bilibili_fetch/__init__.py index b7bd6e9..15ff7b3 100644 --- a/konabot/plugins/bilibili_fetch/__init__.py +++ b/konabot/plugins/bilibili_fetch/__init__.py @@ -6,34 +6,25 @@ from nonebot_plugin_alconna import Reference, Reply, UniMsg from nonebot.adapters import Event -matcher_fix = on_message() - pattern = ( r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|" r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:]|]|\])哔哩哔哩).{0,500}" ) -@matcher_fix.handle() -async def _(msg: UniMsg, event: Event): +def _rule(msg: UniMsg): to_search = msg.exclude(Reply, Reference).dump(json=True) to_search2 = msg.exclude(Reply, Reference).extract_plain_text() if not re.search(pattern, to_search) and not re.search(pattern, to_search2): - return + return False + return True + +matcher_fix = on_message(rule=_rule) + +@matcher_fix.handle() +async def _(event: Event): from nonebot_plugin_analysis_bilibili import handle_analysis await handle_analysis(event) - # b_url: str - # b_page: str | None - # b_time: str | None - # - # from nonebot_plugin_analysis_bilibili.analysis_bilibili import extract as bilibili_extract - # - # b_url, b_page, b_time = bilibili_extract(to_search) - # if b_url is None: - # return - # - # await matcher_fix.send(await UniMessage().text(b_url).export()) - diff --git a/konabot/plugins/nya_echo/__init__.py b/konabot/plugins/nya_echo/__init__.py index e9f6d7f..cce5a62 100644 --- a/konabot/plugins/nya_echo/__init__.py +++ b/konabot/plugins/nya_echo/__init__.py @@ -1,9 +1,10 @@ from nonebot import on_message -from nonebot_plugin_alconna import UniMessage, UniMsg +from nonebot_plugin_alconna import UniMessage -evt = on_message() +from konabot.common.nb.match_keyword import match_keyword + +evt = on_message(rule=match_keyword("喵")) @evt.handle() -async def _(msg: UniMsg): - if msg.extract_plain_text() == "喵": - await evt.send(await UniMessage().text("喵").export()) +async def _(): + await evt.send(await UniMessage().text("喵").export()) diff --git a/konabot/plugins/simple_notify/__init__.py b/konabot/plugins/simple_notify/__init__.py index 972dc5c..91be3c7 100644 --- a/konabot/plugins/simple_notify/__init__.py +++ b/konabot/plugins/simple_notify/__init__.py @@ -1,9 +1,9 @@ +import re import aiohttp import asyncio as asynkio from math import ceil from pathlib import Path from typing import Any -import datetime import nanoid import nonebot @@ -14,9 +14,10 @@ from nonebot_plugin_alconna import Alconna, Args, Subcommand, UniMessage, UniMsg from pydantic import BaseModel from konabot.common.longtask import DepLongTaskTarget, LongTask, create_longtask, handle_long_task, longtask_data -from konabot.common.ptimeparse import parse +from konabot.common.nb.match_keyword import match_keyword +from konabot.plugins.simple_notify.ask_llm import ask_ai -evt = on_message() +evt = on_message(rule=match_keyword(re.compile("^.+提醒我.+$"))) (Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True) DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json" @@ -76,21 +77,12 @@ async def _(msg: UniMsg, mEvt: Event, target: DepLongTaskTarget): return text = msg.extract_plain_text() - if "提醒我" not in text: - return - segments = text.split("提醒我", maxsplit=1) if len(segments) != 2: return - notify_time, notify_text = segments - try: - target_time = parse(notify_time) - logger.info(f"从 {notify_time} 解析出了时间:{target_time}") - except Exception: - logger.info(f"无法从 {notify_time} 中解析出时间") - return - if not notify_text: + target_time, notify_text = await ask_ai(text) + if target_time is None: return await create_longtask( diff --git a/konabot/plugins/simple_notify/ask_llm.py b/konabot/plugins/simple_notify/ask_llm.py new file mode 100644 index 0000000..5b536bc --- /dev/null +++ b/konabot/plugins/simple_notify/ask_llm.py @@ -0,0 +1,120 @@ +import datetime +import json +import re + +from loguru import logger + +from konabot.common.llm import get_llm + + +SYSTEM_PROMPT = """你是一个专门解析提醒请求的助手。请分析用户输入,识别其中是否包含提醒信息,并输出标准化的JSON格式结果。 + +输入格式通常是:"现在是zzzz;xxxx提醒我yyyy",其中: +- zzzz 是系统将发给你的当前时间 +- xxxx 是用户提供的时间信息 +- yyyy 是提醒内容 + +输出要求: +- 必须是有效的JSON对象 +- 包含以下字段: + * datetime: 如果是绝对时间,填入ISO 8601格式的日期时间字符串;否则为null + * datetime_delta: 如果是相对时间,填入ISO 8601持续时间格式;否则为null + * datetime_delta_minus: 如果时间偏移量是负数,则此项为 true,否则为 false + * content: 提醒内容的字符串 + * is_notice: 布尔值,表示这是否是真正的提醒请求 + +时间处理规则: +- 绝对时间示例:如果 xxxx 输入了非常明确的时间点,如"2024年12月25日" → 转换为具体datetime +- 相对时间示例:如果 xxxx 没有输入非常明确的时间点,如"10分钟后"、"2小时后"、"3天后" → 转换为datetime_delta +- 如果用户输入了需要计算的时间,你需要计算出正确的结果,如"10分钟后的8分钟前" → 转换为 “PT2M” +- zzzz 是系统提供的时间,每句话肯定都有,这不是你判断相对或绝对时间的依据,需严格按照 xxx 来判断 +- datetime和datetime_delta有且仅有一个不为null + +时间格式要求: +- datetime: "YYYY-MM-DDTHH:MM:SS" (ISO 8601) +- datetime_delta: "PTxHxMxS" 格式 (如"PT1H30M"表示1小时30分钟) + +判断标准: +- is_notice=true: 明确包含时间+提醒内容的请求 +- is_notice=false: 闲聊、疑问句、或不符合提醒格式的内容 + +示例: +用户:"明天下午2点提醒我开会" +输出:{"datetime": "2024-01-16T14:00:00", "datetime_delta": null, +"datetime_delta_minus": false, "content": "开会", "is_notice": true} + +用户:"5分钟后提醒我关火" +输出:{"datetime": null, "datetime_delta": "PT5M", "datetime_delta_minus": false, "content": "关火", "is_notice": true} + +用户:"5分钟前提醒我关火" +输出:{"datetime": null, "datetime_delta": "PT5M", "datetime_delta_minus": true, "content": "关火", "is_notice": true} + +用户:"昨天提醒我关火" +输出:{"datetime": null, "datetime_delta": "PT1D", "datetime_delta_minus": true, "content": "关火", "is_notice": true} + +用户:"什么是提醒功能?" +输出:{"datetime": null, "datetime_delta": null, "datetime_delta_minus": false, "content": "", "is_notice": false} + +请严格按照上述格式输出JSON,不要添加任何其他文字说明。现在是 DATETIME""" + +pt_pattern = re.compile( + r"^PT" + r"((?P\d+)D)?" + r"((?P\d+)H)?" + r"((?P\d+)M)?" + r"((?P\d+)S)?$" +) + + +def tryint(s: str | None): + if s: + if re.match(r"^\d+$", s): + return int(s) + return 0 + + +async def ask_ai(expression: str, now: datetime.datetime | None = None) -> tuple[datetime.datetime | None, str]: + if now is None: + now = datetime.datetime.now() + prompt = SYSTEM_PROMPT.replace("DATETIME", str(now)) + + llm = get_llm() + message = await llm.chat([ + { "role": "system", "content": prompt }, + { "role": "user", "content": expression }, + ]) + result = message.content + if result is None: + return (None, "") + + try: + data = json.loads(result) + except json.JSONDecodeError: + logger.info(f"提醒功能:解析 AI 返回值时出现问题 raw={result}") + return (None, "") + + datetime_absolute = data.get("datetime", None) + datetime_delta = data.get("datetime_delta", None) + content = data.get("content", "") + is_notice = data.get("is_notice", False) + + if not is_notice: + return (None, "") + if datetime_absolute: + try: + return (datetime.datetime.strptime(datetime_absolute, "%Y-%m-%dT%H:%M:%S"), content) + except ValueError: + pass + + if datetime_delta and (match := pt_pattern.match(datetime_delta)): + days = tryint(match.group("day")) + hours = tryint(match.group("hour")) + minutes = tryint(match.group("minute")) + seconds = tryint(match.group("second")) + + dt = datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) + return (now + dt, content) + + logger.warning(f"提醒功能:解析 AI 返回值时没有找到解析方法 raw={result}") + return (None, "") +