This commit is contained in:
3
konabot/common/ptimeparse/README.md
Normal file
3
konabot/common/ptimeparse/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 已废弃
|
||||||
|
|
||||||
|
坏枪用简单的 LLM + 提示词工程,完成了这 200 块的 `qwen3-coder-plus` 都搞不定的 nb 功能
|
||||||
@ -9,7 +9,6 @@ import datetime
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .expression import TimeExpression
|
from .expression import TimeExpression
|
||||||
from .err import TokenUnhandledException, MultipleSpecificationException
|
|
||||||
|
|
||||||
|
|
||||||
def parse(text: str, now: Optional[datetime.datetime] = None) -> datetime.datetime:
|
def parse(text: str, now: Optional[datetime.datetime] = None) -> datetime.datetime:
|
||||||
@ -57,18 +56,3 @@ class Parser:
|
|||||||
"""
|
"""
|
||||||
return TimeExpression.parse(text, self.now)
|
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)
|
|
||||||
@ -2,10 +2,9 @@
|
|||||||
Abstract Syntax Tree (AST) nodes for the time expression parser.
|
Abstract Syntax Tree (AST) nodes for the time expression parser.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC
|
||||||
from typing import Optional, List
|
from typing import Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import datetime
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from .ptime_ast import (
|
|||||||
TimeExpressionNode, DateNode, TimeNode,
|
TimeExpressionNode, DateNode, TimeNode,
|
||||||
RelativeDateNode, RelativeTimeNode, WeekdayNode, NumberNode
|
RelativeDateNode, RelativeTimeNode, WeekdayNode, NumberNode
|
||||||
)
|
)
|
||||||
from .err import TokenUnhandledException, MultipleSpecificationException
|
from .err import TokenUnhandledException
|
||||||
|
|
||||||
|
|
||||||
class SemanticAnalyzer:
|
class SemanticAnalyzer:
|
||||||
|
|||||||
@ -6,34 +6,25 @@ from nonebot_plugin_alconna import Reference, Reply, UniMsg
|
|||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
|
||||||
matcher_fix = on_message()
|
|
||||||
|
|
||||||
pattern = (
|
pattern = (
|
||||||
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
|
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
|
||||||
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:]|]|\])哔哩哔哩).{0,500}"
|
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:]|]|\])哔哩哔哩).{0,500}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@matcher_fix.handle()
|
def _rule(msg: UniMsg):
|
||||||
async def _(msg: UniMsg, event: Event):
|
|
||||||
to_search = msg.exclude(Reply, Reference).dump(json=True)
|
to_search = msg.exclude(Reply, Reference).dump(json=True)
|
||||||
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
|
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
|
||||||
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
|
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
|
from nonebot_plugin_analysis_bilibili import handle_analysis
|
||||||
|
|
||||||
await handle_analysis(event)
|
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())
|
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
from nonebot import on_message
|
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()
|
@evt.handle()
|
||||||
async def _(msg: UniMsg):
|
async def _():
|
||||||
if msg.extract_plain_text() == "喵":
|
await evt.send(await UniMessage().text("喵").export())
|
||||||
await evt.send(await UniMessage().text("喵").export())
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
|
import re
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio as asynkio
|
import asyncio as asynkio
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import datetime
|
|
||||||
|
|
||||||
import nanoid
|
import nanoid
|
||||||
import nonebot
|
import nonebot
|
||||||
@ -14,9 +14,10 @@ from nonebot_plugin_alconna import Alconna, Args, Subcommand, UniMessage, UniMsg
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from konabot.common.longtask import DepLongTaskTarget, LongTask, create_longtask, handle_long_task, longtask_data
|
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)
|
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
|
||||||
DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json"
|
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
|
return
|
||||||
|
|
||||||
text = msg.extract_plain_text()
|
text = msg.extract_plain_text()
|
||||||
if "提醒我" not in text:
|
|
||||||
return
|
|
||||||
|
|
||||||
segments = text.split("提醒我", maxsplit=1)
|
segments = text.split("提醒我", maxsplit=1)
|
||||||
if len(segments) != 2:
|
if len(segments) != 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
notify_time, notify_text = segments
|
target_time, notify_text = await ask_ai(text)
|
||||||
try:
|
if target_time is None:
|
||||||
target_time = parse(notify_time)
|
|
||||||
logger.info(f"从 {notify_time} 解析出了时间:{target_time}")
|
|
||||||
except Exception:
|
|
||||||
logger.info(f"无法从 {notify_time} 中解析出时间")
|
|
||||||
return
|
|
||||||
if not notify_text:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await create_longtask(
|
await create_longtask(
|
||||||
|
|||||||
120
konabot/plugins/simple_notify/ask_llm.py
Normal file
120
konabot/plugins/simple_notify/ask_llm.py
Normal file
@ -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<day>\d+)D)?"
|
||||||
|
r"((?P<hour>\d+)H)?"
|
||||||
|
r"((?P<minute>\d+)M)?"
|
||||||
|
r"((?P<second>\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, "")
|
||||||
|
|
||||||
Reference in New Issue
Block a user