mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-12-01 01:25:07 +08:00
Add NLP
This commit is contained in:
parent
a13128f356
commit
6ec3ac66f7
@ -75,6 +75,7 @@ def load_builtin_plugins():
|
|||||||
|
|
||||||
|
|
||||||
from .command import on_command, CommandSession, CommandGroup
|
from .command import on_command, CommandSession, CommandGroup
|
||||||
|
from .natural_language import on_natural_language, NLPSession, NLPResult
|
||||||
from .notice_request import (
|
from .notice_request import (
|
||||||
on_notice, NoticeSession,
|
on_notice, NoticeSession,
|
||||||
on_request, RequestSession,
|
on_request, RequestSession,
|
||||||
|
@ -29,7 +29,7 @@ _sessions = {}
|
|||||||
class Command:
|
class Command:
|
||||||
__slots__ = ('name', 'func', 'permission', 'only_to_me', 'args_parser_func')
|
__slots__ = ('name', 'func', 'permission', 'only_to_me', 'args_parser_func')
|
||||||
|
|
||||||
def __init__(self, name: Tuple[str], func: Callable, permission: int, *,
|
def __init__(self, *, name: Tuple[str], func: Callable, permission: int,
|
||||||
only_to_me: bool):
|
only_to_me: bool):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.func = func
|
self.func = func
|
||||||
@ -79,11 +79,11 @@ def on_command(name: Union[str, Tuple[str]], *,
|
|||||||
for alias in aliases:
|
for alias in aliases:
|
||||||
_aliases[alias] = cmd_name
|
_aliases[alias] = cmd_name
|
||||||
|
|
||||||
def args_parser(parser_func: Callable):
|
def args_parser_deco(parser_func: Callable):
|
||||||
cmd.args_parser_func = parser_func
|
cmd.args_parser_func = parser_func
|
||||||
return parser_func
|
return parser_func
|
||||||
|
|
||||||
func.args_parser = args_parser
|
func.args_parser = args_parser_deco
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return deco
|
return deco
|
||||||
@ -156,10 +156,10 @@ class CommandSession(BaseSession):
|
|||||||
super().__init__(bot, ctx)
|
super().__init__(bot, ctx)
|
||||||
self.cmd = cmd
|
self.cmd = cmd
|
||||||
self.current_key = None
|
self.current_key = None
|
||||||
self.current_arg = current_arg
|
self.current_arg = None
|
||||||
self.current_arg_text = Message(current_arg).extract_plain_text()
|
self.current_arg_text = None
|
||||||
self.current_arg_images = [s.data['url'] for s in ctx['message']
|
self.current_arg_images = None
|
||||||
if s.type == 'image' and 'url' in s.data]
|
self.refresh(ctx, current_arg=current_arg)
|
||||||
self.args = args or {}
|
self.args = args or {}
|
||||||
self.last_interaction = None
|
self.last_interaction = None
|
||||||
|
|
||||||
@ -172,8 +172,9 @@ class CommandSession(BaseSession):
|
|||||||
"""
|
"""
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.current_arg = current_arg
|
self.current_arg = current_arg
|
||||||
self.current_arg_text = Message(current_arg).extract_plain_text()
|
current_arg_as_msg = Message(current_arg)
|
||||||
self.current_arg_images = [s.data['url'] for s in ctx['message']
|
self.current_arg_text = current_arg_as_msg.extract_plain_text()
|
||||||
|
self.current_arg_images = [s.data['url'] for s in current_arg_as_msg
|
||||||
if s.type == 'image' and 'url' in s.data]
|
if s.type == 'image' and 'url' in s.data]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -185,37 +186,38 @@ class CommandSession(BaseSession):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def require_arg(self, key: str, prompt: str = None, *,
|
def get(self, key: str, *, prompt: str = None,
|
||||||
prompt_expr: Union[str, Sequence[str], Callable] = None,
|
prompt_expr: Union[str, Sequence[str], Callable] = None) -> Any:
|
||||||
interactive: bool = True) -> Any:
|
|
||||||
"""
|
"""
|
||||||
Get an argument with a given key.
|
Get an argument with a given key.
|
||||||
|
|
||||||
If "interactive" is True, and the argument does not exist
|
If the argument does not exist in the current session,
|
||||||
in the current session, a FurtherInteractionNeeded exception
|
a FurtherInteractionNeeded exception will be raised,
|
||||||
will be raised, and the caller of the command will know
|
and the caller of the command will know it should keep
|
||||||
it should keep the session for further interaction with the user.
|
the session for further interaction with the user.
|
||||||
|
|
||||||
If "interactive" is False, missed key will cause a result of None.
|
|
||||||
|
|
||||||
:param key: argument key
|
:param key: argument key
|
||||||
:param prompt: prompt to ask the user
|
:param prompt: prompt to ask the user
|
||||||
:param prompt_expr: prompt expression to ask the user
|
:param prompt_expr: prompt expression to ask the user
|
||||||
:param interactive: should enter interactive mode while key missing
|
|
||||||
:return: the argument value
|
:return: the argument value
|
||||||
:raise FurtherInteractionNeeded: further interaction is needed
|
:raise FurtherInteractionNeeded: further interaction is needed
|
||||||
"""
|
"""
|
||||||
value = self.args.get(key)
|
value = self.get_optional(key)
|
||||||
if value is not None or not interactive:
|
if value is not None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
self.current_key = key
|
self.current_key = key
|
||||||
# ask the user for more information
|
# ask the user for more information
|
||||||
if prompt_expr is not None:
|
if prompt_expr is not None:
|
||||||
prompt = render(prompt_expr, key=key)
|
prompt = render(prompt_expr, key=key)
|
||||||
|
if prompt:
|
||||||
asyncio.ensure_future(self.send(prompt))
|
asyncio.ensure_future(self.send(prompt))
|
||||||
raise _FurtherInteractionNeeded
|
raise _FurtherInteractionNeeded
|
||||||
|
|
||||||
|
def get_optional(self, key: str,
|
||||||
|
default: Optional[Any] = None) -> Optional[Any]:
|
||||||
|
return self.args.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
def _new_command_session(bot: CQHttp,
|
def _new_command_session(bot: CQHttp,
|
||||||
ctx: Dict[str, Any]) -> Optional[CommandSession]:
|
ctx: Dict[str, Any]) -> Optional[CommandSession]:
|
||||||
@ -301,12 +303,40 @@ async def handle_command(bot: CQHttp, ctx: Dict[str, Any]) -> bool:
|
|||||||
session = _new_command_session(bot, ctx)
|
session = _new_command_session(bot, ctx)
|
||||||
if not session:
|
if not session:
|
||||||
return False
|
return False
|
||||||
_sessions[src] = session
|
|
||||||
|
|
||||||
|
return await _real_run_command(session, src, check_perm=check_perm)
|
||||||
|
|
||||||
|
|
||||||
|
async def call_command(bot: CQHttp, ctx: Dict[str, Any],
|
||||||
|
name: Union[str, Tuple[str]],
|
||||||
|
args: Dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
Call a command internally.
|
||||||
|
|
||||||
|
This function is typically called by some other commands
|
||||||
|
or "handle_natural_language" when handling NLPResult object.
|
||||||
|
|
||||||
|
:param bot: CQHttp instance
|
||||||
|
:param ctx: message context
|
||||||
|
:param name: command name
|
||||||
|
:param args: command args
|
||||||
|
:return: the command is successfully called
|
||||||
|
"""
|
||||||
|
cmd = _find_command(name)
|
||||||
|
if not cmd:
|
||||||
|
return False
|
||||||
|
session = CommandSession(bot, ctx, cmd, args=args)
|
||||||
|
return await _real_run_command(session, context_source(session.ctx),
|
||||||
|
check_perm=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def _real_run_command(session: CommandSession,
|
||||||
|
ctx_src: str, **kwargs) -> bool:
|
||||||
|
_sessions[ctx_src] = session
|
||||||
try:
|
try:
|
||||||
res = await session.cmd.run(session, check_perm=check_perm)
|
res = await session.cmd.run(session, **kwargs)
|
||||||
# the command is finished, remove the session
|
# the command is finished, remove the session
|
||||||
del _sessions[src]
|
del _sessions[ctx_src]
|
||||||
return res
|
return res
|
||||||
except _FurtherInteractionNeeded:
|
except _FurtherInteractionNeeded:
|
||||||
session.last_interaction = datetime.now()
|
session.last_interaction = datetime.now()
|
||||||
|
@ -8,6 +8,7 @@ PORT = 8080
|
|||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
SUPERUSERS = set()
|
SUPERUSERS = set()
|
||||||
|
NICKNAME = ''
|
||||||
COMMAND_START = {'/', '!', '/', '!'}
|
COMMAND_START = {'/', '!', '/', '!'}
|
||||||
COMMAND_SEP = {'/', '.'}
|
COMMAND_SEP = {'/', '.'}
|
||||||
SESSION_EXPIRE_TIMEOUT = timedelta(minutes=5)
|
SESSION_EXPIRE_TIMEOUT = timedelta(minutes=5)
|
||||||
|
@ -1,14 +1,56 @@
|
|||||||
|
import re
|
||||||
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any, Iterable, Optional, Callable, Union
|
||||||
|
|
||||||
from aiocqhttp import CQHttp
|
from aiocqhttp import CQHttp
|
||||||
|
from aiocqhttp.message import Message
|
||||||
|
|
||||||
|
from . import permission as perm
|
||||||
|
from .session import BaseSession
|
||||||
|
from .command import call_command
|
||||||
|
from .log import logger
|
||||||
|
|
||||||
_nl_processors = set()
|
_nl_processors = set()
|
||||||
|
|
||||||
|
|
||||||
class NLProcessor:
|
class NLProcessor:
|
||||||
__slots__ = ('func', 'permission', 'only_to_me', 'keywords',
|
__slots__ = ('func', 'keywords', 'permission', 'only_to_me')
|
||||||
'precondition_func')
|
|
||||||
|
def __init__(self, *, func: Callable, keywords: Optional[Iterable],
|
||||||
|
permission: int, only_to_me: bool):
|
||||||
|
self.func = func
|
||||||
|
self.keywords = keywords
|
||||||
|
self.permission = permission
|
||||||
|
self.only_to_me = only_to_me
|
||||||
|
|
||||||
|
|
||||||
|
def on_natural_language(keywords: Union[Optional[Iterable], Callable] = None, *,
|
||||||
|
permission: int = perm.EVERYBODY,
|
||||||
|
only_to_me: bool = True) -> Callable:
|
||||||
|
def deco(func: Callable) -> Callable:
|
||||||
|
nl_processor = NLProcessor(func=func, keywords=keywords,
|
||||||
|
permission=permission, only_to_me=only_to_me)
|
||||||
|
_nl_processors.add(nl_processor)
|
||||||
|
return func
|
||||||
|
|
||||||
|
if isinstance(keywords, Callable):
|
||||||
|
# here "keywords" is the function to be decorated
|
||||||
|
return on_natural_language()(keywords)
|
||||||
|
else:
|
||||||
|
return deco
|
||||||
|
|
||||||
|
|
||||||
|
class NLPSession(BaseSession):
|
||||||
|
__slots__ = ('msg', 'msg_text', 'msg_images')
|
||||||
|
|
||||||
|
def __init__(self, bot: CQHttp, ctx: Dict[str, Any], msg: str):
|
||||||
|
super().__init__(bot, ctx)
|
||||||
|
self.msg = msg
|
||||||
|
tmp_msg = Message(msg)
|
||||||
|
self.msg_text = tmp_msg.extract_plain_text()
|
||||||
|
self.msg_images = [s.data['url'] for s in tmp_msg
|
||||||
|
if s.type == 'image' and 'url' in s.data]
|
||||||
|
|
||||||
|
|
||||||
NLPResult = namedtuple('NLPResult', (
|
NLPResult = namedtuple('NLPResult', (
|
||||||
@ -18,5 +60,36 @@ NLPResult = namedtuple('NLPResult', (
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
async def handle_natural_language(bot: CQHttp, ctx: Dict[str, Any]) -> None:
|
async def handle_natural_language(bot: CQHttp, ctx: Dict[str, Any]) -> bool:
|
||||||
pass
|
msg = str(ctx['message'])
|
||||||
|
if bot.config.NICKNAME:
|
||||||
|
m = re.search(rf'^{bot.config.NICKNAME}[\s,,]+', msg)
|
||||||
|
if m:
|
||||||
|
ctx['to_me'] = True
|
||||||
|
msg = msg[m.end():]
|
||||||
|
session = NLPSession(bot, ctx, msg)
|
||||||
|
|
||||||
|
coros = []
|
||||||
|
for p in _nl_processors:
|
||||||
|
should_run = await perm.check_permission(bot, ctx, p.permission)
|
||||||
|
if should_run and p.keywords:
|
||||||
|
for kw in p.keywords:
|
||||||
|
if kw in session.msg_text:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# no keyword matches
|
||||||
|
should_run = False
|
||||||
|
if should_run and p.only_to_me and not ctx['to_me']:
|
||||||
|
should_run = False
|
||||||
|
|
||||||
|
if should_run:
|
||||||
|
coros.append(p.func(session))
|
||||||
|
|
||||||
|
if coros:
|
||||||
|
results = sorted(filter(lambda r: r, await asyncio.gather(*coros)),
|
||||||
|
key=lambda r: r.confidence, reverse=True)
|
||||||
|
logger.debug(results)
|
||||||
|
if results and results[0].confidence >= 60.0:
|
||||||
|
return await call_command(bot, ctx,
|
||||||
|
results[0].cmd_name, results[0].cmd_args)
|
||||||
|
return False
|
||||||
|
@ -5,10 +5,10 @@ from none import on_command, CommandSession, permission as perm
|
|||||||
|
|
||||||
@on_command('echo', only_to_me=False)
|
@on_command('echo', only_to_me=False)
|
||||||
async def echo(session: CommandSession):
|
async def echo(session: CommandSession):
|
||||||
await session.send(session.args.get('message') or session.current_arg)
|
await session.send(session.get_optional('message') or session.current_arg)
|
||||||
|
|
||||||
|
|
||||||
@on_command('say', permission=perm.SUPERUSER, only_to_me=False)
|
@on_command('say', permission=perm.SUPERUSER, only_to_me=False)
|
||||||
async def _(session: CommandSession):
|
async def _(session: CommandSession):
|
||||||
await session.send(
|
await session.send(
|
||||||
unescape(session.args.get('message') or session.current_arg))
|
unescape(session.get_optional('message') or session.current_arg))
|
||||||
|
@ -6,5 +6,6 @@ HOST = '0.0.0.0'
|
|||||||
SECRET = 'abc'
|
SECRET = 'abc'
|
||||||
|
|
||||||
SUPERUSERS = {1002647525}
|
SUPERUSERS = {1002647525}
|
||||||
|
NICKNAME = '奶茶'
|
||||||
COMMAND_START = {'', '/', '!', '/', '!', re.compile(r'^>+\s*')}
|
COMMAND_START = {'', '/', '!', '/', '!', re.compile(r'^>+\s*')}
|
||||||
COMMAND_SEP = {'/', '.', re.compile(r'#|::?')}
|
COMMAND_SEP = {'/', '.', re.compile(r'#|::?')}
|
||||||
|
37
none_demo/plugins/tuling.py
Normal file
37
none_demo/plugins/tuling.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from none import (
|
||||||
|
on_command, CommandSession,
|
||||||
|
on_natural_language, NLPSession, NLPResult
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@on_command('tuling', aliases=('聊天', '对话'))
|
||||||
|
async def tuling(session: CommandSession):
|
||||||
|
message = session.get('message', prompt='我已经准备好啦,来跟我聊天吧~')
|
||||||
|
|
||||||
|
finish = message in ('结束', '拜拜', '再见')
|
||||||
|
if finish:
|
||||||
|
asyncio.ensure_future(session.send('拜拜啦,你忙吧,下次想聊天随时找我哦~'))
|
||||||
|
return
|
||||||
|
|
||||||
|
# call tuling api
|
||||||
|
reply = f'你说了:{message}'
|
||||||
|
|
||||||
|
one_time = session.get_optional('one_time', False)
|
||||||
|
if one_time:
|
||||||
|
asyncio.ensure_future(session.send(reply))
|
||||||
|
else:
|
||||||
|
del session.args['message']
|
||||||
|
session.get('message', prompt=reply)
|
||||||
|
|
||||||
|
|
||||||
|
@tuling.args_parser
|
||||||
|
async def _(session: CommandSession):
|
||||||
|
if session.current_key == 'message':
|
||||||
|
session.args[session.current_key] = session.current_arg_text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
@on_natural_language
|
||||||
|
async def _(session: NLPSession):
|
||||||
|
return NLPResult(60.0, 'tuling', {'message': session.msg, 'one_time': True})
|
@ -1,4 +1,7 @@
|
|||||||
from none import CommandSession, CommandGroup
|
from none import (
|
||||||
|
CommandSession, CommandGroup,
|
||||||
|
on_natural_language, NLPSession, NLPResult
|
||||||
|
)
|
||||||
|
|
||||||
from . import expressions as expr
|
from . import expressions as expr
|
||||||
|
|
||||||
@ -7,34 +10,24 @@ w = CommandGroup('weather')
|
|||||||
|
|
||||||
@w.command('weather', aliases=('天气', '天气预报'))
|
@w.command('weather', aliases=('天气', '天气预报'))
|
||||||
async def weather(session: CommandSession):
|
async def weather(session: CommandSession):
|
||||||
city = session.require_arg('city', prompt_expr=expr.WHICH_CITY)
|
city = session.get('city', prompt_expr=expr.WHICH_CITY)
|
||||||
await session.send_expr(expr.REPORT, city=city)
|
await session.send_expr(expr.REPORT, city=city)
|
||||||
|
|
||||||
|
|
||||||
@weather.args_parser
|
@weather.args_parser
|
||||||
async def _(session: CommandSession):
|
async def _(session: CommandSession):
|
||||||
|
striped_arg = session.current_arg_text.strip()
|
||||||
if session.current_key:
|
if session.current_key:
|
||||||
session.args[session.current_key] = session.current_arg_text.strip()
|
session.args[session.current_key] = striped_arg
|
||||||
|
elif striped_arg:
|
||||||
|
session.args['city'] = striped_arg
|
||||||
|
|
||||||
|
|
||||||
# @on_natural_language(keywords={'天气', '雨', '雪', '晴', '阴', '多云', '冰雹'},
|
@on_natural_language({'天气', '雨', '雪', '晴', '阴'}, only_to_me=False)
|
||||||
# only_to_me=False)
|
async def _(session: NLPSession):
|
||||||
# async def weather_nlp(session: NaturalLanguageSession):
|
if not ('?' in session.msg_text or '?' in session.msg_text):
|
||||||
# return NLPResult(89.5, ('weather', 'weather'), {'city': '南京'})
|
return None
|
||||||
#
|
return NLPResult(90.0, ('weather', 'weather'), {})
|
||||||
#
|
|
||||||
# @weather_nlp.condition
|
|
||||||
# async def _(session: NaturalLanguageSession):
|
|
||||||
# keywords = {'天气', '雨', '雪', '晴', '阴', '多云', '冰雹'}
|
|
||||||
# for kw in keywords:
|
|
||||||
# if kw in session.text:
|
|
||||||
# keyword_hit = True
|
|
||||||
# break
|
|
||||||
# else:
|
|
||||||
# keyword_hit = False
|
|
||||||
# if session.ctx['to_me'] and keyword_hit:
|
|
||||||
# return True
|
|
||||||
# return False
|
|
||||||
|
|
||||||
|
|
||||||
@w.command('suggestion', aliases=('生活指数', '生活建议', '生活提示'))
|
@w.command('suggestion', aliases=('生活指数', '生活建议', '生活提示'))
|
||||||
|
Loading…
Reference in New Issue
Block a user