nonebot2/nonebot/natural_language.py

218 lines
7.2 KiB
Python
Raw Normal View History

2018-07-01 17:51:01 +08:00
import asyncio
2020-04-07 21:58:10 +08:00
import warnings
from functools import update_wrapper
2020-04-07 21:58:10 +08:00
from typing import Set, Iterable, Optional, Callable, Union, NamedTuple
2018-07-01 11:01:24 +08:00
from aiocqhttp import Event as CQEvent
2020-04-22 13:55:33 +08:00
from aiocqhttp.message import Message
2020-04-22 13:55:33 +08:00
from .log import logger
2018-07-04 09:28:31 +08:00
from . import NoneBot, permission as perm
2018-07-01 17:51:01 +08:00
from .command import call_command
2018-07-02 16:54:29 +08:00
from .session import BaseSession
from .typing import CommandName_T, CommandArgs_T
2018-07-01 11:01:24 +08:00
class NLProcessor:
2020-04-07 21:58:10 +08:00
__slots__ = ('func', 'keywords', 'permission', 'only_to_me',
2020-04-20 13:50:38 +08:00
'only_short_message', 'allow_empty_message')
2018-07-01 17:51:01 +08:00
def __init__(self, *, func: Callable, keywords: Optional[Iterable],
permission: int, only_to_me: bool, only_short_message: bool,
allow_empty_message: bool):
2018-07-01 17:51:01 +08:00
self.func = func
self.keywords = keywords
self.permission = permission
self.only_to_me = only_to_me
2018-07-27 22:53:38 +08:00
self.only_short_message = only_short_message
self.allow_empty_message = allow_empty_message
2018-07-01 17:51:01 +08:00
2020-04-07 21:58:10 +08:00
class NLPManager:
_nl_processors: Set[NLProcessor] = set()
def __init__(self):
self.nl_processors = NLPManager._nl_processors.copy()
@classmethod
def add_nl_processor(cls, processor: NLProcessor) -> None:
"""Register a natural language processor
Args:
processor (NLProcessor): Processor object
"""
if processor in cls._nl_processors:
warnings.warn(f"NLProcessor {processor} already exists")
return
cls._nl_processors.add(processor)
@classmethod
def remove_nl_processor(cls, processor: NLProcessor) -> bool:
"""Remove a natural language processor globally
Args:
processor (NLProcessor): Processor to remove
Returns:
bool: Success or not
"""
if processor in cls._nl_processors:
cls._nl_processors.remove(processor)
return True
return False
@classmethod
2020-04-11 14:56:39 +08:00
def switch_nlprocessor_global(cls,
processor: NLProcessor,
state: Optional[bool] = None
) -> Optional[bool]:
"""Remove or add a natural language processor globally
2020-04-07 21:58:10 +08:00
Args:
processor (NLProcessor): Processor object
Returns:
bool: True if removed, False if added
"""
if processor in cls._nl_processors and not state:
cls._nl_processors.remove(processor)
return True
elif processor not in cls._nl_processors and state != False:
cls._nl_processors.add(processor)
return False
2020-04-11 14:56:39 +08:00
def switch_nlprocessor(self,
processor: NLProcessor,
state: Optional[bool] = None) -> Optional[bool]:
"""Remove or add a natural language processor
2020-04-07 21:58:10 +08:00
Args:
processor (NLProcessor): Processor to remove
Returns:
bool: True if removed, False if added
"""
if processor in self.nl_processors and not state:
self.nl_processors.remove(processor)
return True
elif processor not in self.nl_processors and state != False:
self.nl_processors.add(processor)
return False
2018-07-01 17:51:01 +08:00
class NLPSession(BaseSession):
__slots__ = ('msg', 'msg_text', 'msg_images')
def __init__(self, bot: NoneBot, event: CQEvent, msg: str):
super().__init__(bot, event)
2018-07-01 17:51:01 +08:00
self.msg = msg
tmp_msg = Message(msg)
self.msg_text = tmp_msg.extract_plain_text()
2020-04-07 21:58:10 +08:00
self.msg_images = [
s.data['url']
for s in tmp_msg
if s.type == 'image' and 'url' in s.data
]
2018-07-01 11:01:24 +08:00
2018-10-16 01:03:50 +08:00
class NLPResult(NamedTuple):
"""
Deprecated.
Use class IntentCommand instead.
"""
2018-10-16 01:03:50 +08:00
confidence: float
cmd_name: Union[str, CommandName_T]
cmd_args: Optional[CommandArgs_T] = None
2018-07-01 11:01:24 +08:00
def to_intent_command(self):
return IntentCommand(confidence=self.confidence,
name=self.cmd_name,
2019-02-01 19:38:50 +08:00
args=self.cmd_args)
class IntentCommand(NamedTuple):
"""
To represent a command that we think the user may be intended to call.
"""
confidence: float
name: Union[str, CommandName_T]
args: Optional[CommandArgs_T] = None
2019-02-01 19:38:50 +08:00
current_arg: str = ''
2018-07-01 11:01:24 +08:00
2020-04-07 21:58:10 +08:00
async def handle_natural_language(bot: NoneBot, event: CQEvent,
manager: NLPManager) -> bool:
2018-07-01 20:01:05 +08:00
"""
Handle a message as natural language.
This function is typically called by "handle_message".
2018-07-04 09:28:31 +08:00
:param bot: NoneBot instance
:param event: message event
2020-04-07 21:58:10 +08:00
:param manager: natural language processor manager
2018-07-01 20:01:05 +08:00
:return: the message is handled as natural language
"""
session = NLPSession(bot, event, str(event.message))
2018-07-01 17:51:01 +08:00
2018-07-27 22:53:38 +08:00
# use msg_text here because CQ code "share" may be very long,
# at the same time some plugins may want to handle it
msg_text_length = len(session.msg_text)
2018-12-25 20:40:36 +08:00
futures = []
2020-04-07 21:58:10 +08:00
for p in manager.nl_processors:
if not p.allow_empty_message and not session.msg:
# don't allow empty msg, but it is one, so skip to next
continue
2018-07-27 22:53:38 +08:00
if p.only_short_message and \
msg_text_length > bot.config.SHORT_MESSAGE_MAX_LENGTH:
continue
if p.only_to_me and not event['to_me']:
2018-07-27 22:53:38 +08:00
continue
should_run = await perm.check_permission(bot, event, p.permission)
2018-07-01 17:51:01 +08:00
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:
2018-12-25 20:40:36 +08:00
futures.append(asyncio.ensure_future(p.func(session)))
2018-07-01 17:51:01 +08:00
2018-12-25 20:40:36 +08:00
if futures:
# wait for intent commands, and sort them by confidence
intent_commands = []
2018-12-25 20:40:36 +08:00
for fut in futures:
try:
res = await fut
if isinstance(res, NLPResult):
intent_commands.append(res.to_intent_command())
elif isinstance(res, IntentCommand):
intent_commands.append(res)
2018-12-25 20:40:36 +08:00
except Exception as e:
logger.error('An exception occurred while running '
'some natural language processor:')
logger.exception(e)
intent_commands.sort(key=lambda ic: ic.confidence, reverse=True)
logger.debug(f'Intent commands: {intent_commands}')
if intent_commands and intent_commands[0].confidence >= 60.0:
# choose the intent command with highest confidence
chosen_cmd = intent_commands[0]
logger.debug(
f'Intent command with highest confidence: {chosen_cmd}')
2020-04-07 21:58:10 +08:00
return await call_command(bot,
event,
chosen_cmd.name,
args=chosen_cmd.args,
current_arg=chosen_cmd.current_arg,
check_perm=False) # type: ignore
2018-07-21 00:46:34 +08:00
else:
logger.debug('No intent command has enough confidence')
2018-07-01 17:51:01 +08:00
return False