This commit is contained in:
Richard Chien 2018-07-01 17:51:01 +08:00
parent a13128f356
commit 6ec3ac66f7
8 changed files with 189 additions and 53 deletions

View File

@ -75,6 +75,7 @@ def load_builtin_plugins():
from .command import on_command, CommandSession, CommandGroup
from .natural_language import on_natural_language, NLPSession, NLPResult
from .notice_request import (
on_notice, NoticeSession,
on_request, RequestSession,

View File

@ -29,7 +29,7 @@ _sessions = {}
class Command:
__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):
self.name = name
self.func = func
@ -79,11 +79,11 @@ def on_command(name: Union[str, Tuple[str]], *,
for alias in aliases:
_aliases[alias] = cmd_name
def args_parser(parser_func: Callable):
def args_parser_deco(parser_func: Callable):
cmd.args_parser_func = parser_func
return parser_func
func.args_parser = args_parser
func.args_parser = args_parser_deco
return func
return deco
@ -156,10 +156,10 @@ class CommandSession(BaseSession):
super().__init__(bot, ctx)
self.cmd = cmd
self.current_key = None
self.current_arg = current_arg
self.current_arg_text = Message(current_arg).extract_plain_text()
self.current_arg_images = [s.data['url'] for s in ctx['message']
if s.type == 'image' and 'url' in s.data]
self.current_arg = None
self.current_arg_text = None
self.current_arg_images = None
self.refresh(ctx, current_arg=current_arg)
self.args = args or {}
self.last_interaction = None
@ -172,8 +172,9 @@ class CommandSession(BaseSession):
"""
self.ctx = ctx
self.current_arg = current_arg
self.current_arg_text = Message(current_arg).extract_plain_text()
self.current_arg_images = [s.data['url'] for s in ctx['message']
current_arg_as_msg = Message(current_arg)
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]
@property
@ -185,37 +186,38 @@ class CommandSession(BaseSession):
return False
return True
def require_arg(self, key: str, prompt: str = None, *,
prompt_expr: Union[str, Sequence[str], Callable] = None,
interactive: bool = True) -> Any:
def get(self, key: str, *, prompt: str = None,
prompt_expr: Union[str, Sequence[str], Callable] = None) -> Any:
"""
Get an argument with a given key.
If "interactive" is True, and the argument does not exist
in the current session, a FurtherInteractionNeeded exception
will be raised, and the caller of the command will know
it should keep the session for further interaction with the user.
If "interactive" is False, missed key will cause a result of None.
If the argument does not exist in the current session,
a FurtherInteractionNeeded exception will be raised,
and the caller of the command will know it should keep
the session for further interaction with the user.
:param key: argument key
:param prompt: prompt 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
:raise FurtherInteractionNeeded: further interaction is needed
"""
value = self.args.get(key)
if value is not None or not interactive:
value = self.get_optional(key)
if value is not None:
return value
self.current_key = key
# ask the user for more information
if prompt_expr is not None:
prompt = render(prompt_expr, key=key)
if prompt:
asyncio.ensure_future(self.send(prompt))
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,
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)
if not session:
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:
res = await session.cmd.run(session, check_perm=check_perm)
res = await session.cmd.run(session, **kwargs)
# the command is finished, remove the session
del _sessions[src]
del _sessions[ctx_src]
return res
except _FurtherInteractionNeeded:
session.last_interaction = datetime.now()

View File

@ -8,6 +8,7 @@ PORT = 8080
DEBUG = True
SUPERUSERS = set()
NICKNAME = ''
COMMAND_START = {'/', '!', '', ''}
COMMAND_SEP = {'/', '.'}
SESSION_EXPIRE_TIMEOUT = timedelta(minutes=5)

View File

@ -1,14 +1,56 @@
import re
import asyncio
from collections import namedtuple
from typing import Dict, Any
from typing import Dict, Any, Iterable, Optional, Callable, Union
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()
class NLProcessor:
__slots__ = ('func', 'permission', 'only_to_me', 'keywords',
'precondition_func')
__slots__ = ('func', 'keywords', 'permission', 'only_to_me')
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', (
@ -18,5 +60,36 @@ NLPResult = namedtuple('NLPResult', (
))
async def handle_natural_language(bot: CQHttp, ctx: Dict[str, Any]) -> None:
pass
async def handle_natural_language(bot: CQHttp, ctx: Dict[str, Any]) -> bool:
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

View File

@ -5,10 +5,10 @@ from none import on_command, CommandSession, permission as perm
@on_command('echo', only_to_me=False)
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)
async def _(session: CommandSession):
await session.send(
unescape(session.args.get('message') or session.current_arg))
unescape(session.get_optional('message') or session.current_arg))

View File

@ -6,5 +6,6 @@ HOST = '0.0.0.0'
SECRET = 'abc'
SUPERUSERS = {1002647525}
NICKNAME = '奶茶'
COMMAND_START = {'', '/', '!', '', '', re.compile(r'^>+\s*')}
COMMAND_SEP = {'/', '.', re.compile(r'#|::?')}

View 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})

View File

@ -1,4 +1,7 @@
from none import CommandSession, CommandGroup
from none import (
CommandSession, CommandGroup,
on_natural_language, NLPSession, NLPResult
)
from . import expressions as expr
@ -7,34 +10,24 @@ w = CommandGroup('weather')
@w.command('weather', aliases=('天气', '天气预报'))
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)
@weather.args_parser
async def _(session: CommandSession):
striped_arg = session.current_arg_text.strip()
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={'天气', '雨', '雪', '晴', '阴', '多云', '冰雹'},
# only_to_me=False)
# async def weather_nlp(session: NaturalLanguageSession):
# return NLPResult(89.5, ('weather', 'weather'), {'city': '南京'})
#
#
# @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
@on_natural_language({'天气', '', '', '', ''}, only_to_me=False)
async def _(session: NLPSession):
if not ('?' in session.msg_text or '' in session.msg_text):
return None
return NLPResult(90.0, ('weather', 'weather'), {})
@w.command('suggestion', aliases=('生活指数', '生活建议', '生活提示'))