2020-06-20 19:35:05 +03:00
|
|
|
# Abstract class Command
|
2020-08-29 19:58:39 +03:00
|
|
|
#
|
2020-06-20 19:35:05 +03:00
|
|
|
# Command - parent of all command classes
|
|
|
|
# command - object (class instance)
|
|
|
|
# Command.list - list of all commands
|
2020-08-29 19:58:39 +03:00
|
|
|
# Command.find() - recognize command from a string by fuzzywuzzy, return command object
|
|
|
|
# must return dict like {'cmd': cmd, 'params': params}
|
|
|
|
# Command.reg_find() - recognize command from a string with regexe patterns, return command object
|
2020-07-31 01:43:27 +03:00
|
|
|
# must return dict like {'cmd': cmd, 'params': params}
|
2020-08-29 19:58:39 +03:00
|
|
|
# this - object (class instance) pointer (self)
|
2020-06-20 19:35:05 +03:00
|
|
|
# abstract this.start() - required method for all commands
|
|
|
|
# this.keywords - dictionary of arrays keywords
|
|
|
|
# like {
|
|
|
|
# (int)weight : ['word1', 'word2', 'word3'],
|
|
|
|
# (int)weight1 : ['word3', 'word4'],
|
|
|
|
# (int)weight2 : ['word5', 'word6', 'word7', 'word8', 'word9'],
|
|
|
|
# }
|
2020-08-29 19:58:39 +03:00
|
|
|
# this.patterns - list of command patterns
|
|
|
|
# this.subpatterns - list of subpaterns (context patterns)
|
|
|
|
# like ['* который * час *', '* скольк* * (врем|час)* *']
|
|
|
|
# Command._entities - linked patterns $Pattern
|
|
|
|
# Command.regex - regex patterns dict for better syntax
|
|
|
|
#
|
|
|
|
#
|
2020-06-20 19:35:05 +03:00
|
|
|
#
|
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
|
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
from abc import ABC, abstractmethod # for abstract class and methods
|
2020-06-24 02:12:41 +03:00
|
|
|
from fuzzywuzzy import fuzz
|
2020-07-19 22:27:01 +03:00
|
|
|
from threading import Thread, Event
|
2020-07-28 03:13:58 +03:00
|
|
|
import re
|
2020-07-19 22:27:01 +03:00
|
|
|
|
|
|
|
class RThread(Thread):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self._return = None
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
if self._target is not None:
|
|
|
|
self._return = self._target(*self._args, **self._kwargs)
|
|
|
|
|
|
|
|
def join(self, *args, **kwargs):
|
|
|
|
super().join(*args, **kwargs)
|
|
|
|
return self._return
|
2020-06-20 19:35:05 +03:00
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
class Command(ABC):
|
2020-07-31 01:43:27 +03:00
|
|
|
_list = [] # list of all commands
|
2020-08-03 22:49:59 +03:00
|
|
|
_entities = {
|
|
|
|
'word': lambda: r'\b[A-Za-zА-ЯЁа-яё0-9\-]+\b',
|
|
|
|
'text': lambda: r'[A-Za-zА-ЯЁа-яё0-9\- ]+',
|
|
|
|
'quest' : lambda: Command.compilePattern('(кто|что|как|какой|какая|какое|где|зачем|почему|сколько|чей|куда|когда)'),
|
|
|
|
'repeat': lambda: Command.compilePattern('* ((повтор*)|(еще раз)|(еще*раз)*) *'),
|
2020-07-28 03:13:58 +03:00
|
|
|
}
|
2020-07-31 01:43:27 +03:00
|
|
|
_regex = {
|
2020-07-28 03:13:58 +03:00
|
|
|
# stars *
|
2020-07-28 17:25:41 +03:00
|
|
|
'([A-Za-zА-ЯЁа-яё0-9\(\)\[\]\{\}]+)\*([A-Za-zА-ЯЁа-яё0-9\(\)\[\]\{\}]+)': r'\\b\1.*\2\\b', # 'te*xt'
|
|
|
|
'\*([A-Za-zА-ЯЁа-яё0-9\(\)\[\]\{\}]+)': r'\\b.*\1', # '*text'
|
|
|
|
'([A-Za-zА-ЯЁа-яё0-9\(\)\[\]\{\}]+)\*': r'\1.*\\b', # 'text*'
|
|
|
|
'(^|\s)\*($|\s)': r'.*', # '*' ' * '
|
2020-07-28 03:13:58 +03:00
|
|
|
# one of the list (a|b|c)
|
|
|
|
'\(((?:.*\|)*.*)\)': r'(?:\1)',
|
2020-07-31 01:43:27 +03:00
|
|
|
# 0 or 1 the of list [abc]
|
2020-07-28 03:13:58 +03:00
|
|
|
'\[((?:.*\|?)*?.*?)\]': r'(?:\1)??',
|
|
|
|
# one or more of the list, without order {a|b|c}
|
|
|
|
'\{((?:.*\|?)*?.*?)\}': r'(?:\1)+?',
|
|
|
|
}
|
2020-07-31 23:15:53 +03:00
|
|
|
def __init__(this, name, keywords = {}, patterns = [], subPatterns = []): # initialisation of new command
|
2020-08-29 19:58:39 +03:00
|
|
|
this._name = name
|
|
|
|
this._keywords = keywords
|
|
|
|
this._patterns = patterns
|
|
|
|
this._subPatterns = subPatterns
|
2020-07-31 19:38:20 +03:00
|
|
|
Command.append(this)
|
2020-06-20 19:35:05 +03:00
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
def __str__(this):
|
|
|
|
str = f'{this.__class__.__name__}.{this.getName()}:\n'
|
|
|
|
for key, value in this._keywords.items():
|
|
|
|
str += f'\t{key}:\t{value}\n'
|
|
|
|
return str
|
2020-06-20 19:35:05 +03:00
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
######################################################################################
|
|
|
|
# CONTROL KEYWORDS #
|
|
|
|
######################################################################################
|
|
|
|
def getKeyword(this, string): # return position of the keyword
|
2020-06-20 23:34:38 +03:00
|
|
|
for weight, array in this._keywords.items():
|
2020-06-20 19:35:05 +03:00
|
|
|
index = 0
|
|
|
|
for word in array:
|
|
|
|
if string == word:
|
|
|
|
return (weight, index)
|
|
|
|
index += 1
|
2020-06-20 23:34:38 +03:00
|
|
|
return None # if not found
|
2020-06-25 00:59:11 +03:00
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
def removeKeyword(this, string):
|
2020-08-29 19:58:39 +03:00
|
|
|
position = this.getKeyword(string)
|
2020-06-20 23:34:38 +03:00
|
|
|
if(position): del this._keywords[ position[0] ][ position[1] ]
|
2020-06-25 00:59:11 +03:00
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
def addKeyword(this, weight, string): # add new keywords to end of the list
|
|
|
|
if this.getKeyword(string): return
|
2020-06-20 23:34:38 +03:00
|
|
|
if( this._keywords.get(weight) ): this._keywords[weight].append(string)
|
2020-07-31 19:38:20 +03:00
|
|
|
else: this._keywords[weight] = [string]
|
2020-06-25 00:59:11 +03:00
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
def changeKeyword(this, weight, name): # set new weight to keyword (find by name)
|
|
|
|
this.removeKeyword(name)
|
|
|
|
this.addKeyword(weight, name)
|
2020-06-20 19:35:05 +03:00
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
def checkContext(this, string): # return cmd if the string matches the cmd context
|
2020-07-31 23:15:53 +03:00
|
|
|
for pattern in this.getSubPatterns():
|
|
|
|
if match := re.search(re.compile(Command.compilePattern(pattern)), string):
|
|
|
|
return {
|
|
|
|
'cmd': this,
|
2020-08-29 19:58:39 +03:00
|
|
|
'params': {**match.groupdict(), 'string':string}, # return parans+initial text
|
2020-07-31 23:15:53 +03:00
|
|
|
}
|
2020-08-29 19:58:39 +03:00
|
|
|
raise Exception("Not Found") # raise exception if context not found
|
2020-07-31 23:15:53 +03:00
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
######################################################################################
|
|
|
|
# SETTERS #
|
|
|
|
######################################################################################
|
|
|
|
def setStart(this, function): # define start (required)
|
2020-06-20 23:34:38 +03:00
|
|
|
this.start = function
|
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
######################################################################################
|
|
|
|
# GETTERS #
|
|
|
|
######################################################################################
|
2020-06-20 23:34:38 +03:00
|
|
|
def getName(this):
|
|
|
|
return this._name
|
2020-06-25 00:59:11 +03:00
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
def getKeywords(this):
|
|
|
|
return this._keywords
|
2020-06-20 19:35:05 +03:00
|
|
|
|
2020-07-28 03:13:58 +03:00
|
|
|
def getPatterns(this):
|
|
|
|
return this._patterns
|
|
|
|
|
2020-07-31 23:15:53 +03:00
|
|
|
def getSubPatterns(this):
|
|
|
|
return this._subPatterns
|
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
######################################################################################
|
|
|
|
# ABSTRACT METHODS #
|
|
|
|
######################################################################################
|
2020-06-20 19:35:05 +03:00
|
|
|
@abstractmethod
|
2020-07-31 01:43:27 +03:00
|
|
|
def start(this, params): # main method
|
2020-06-20 19:35:05 +03:00
|
|
|
pass
|
2020-06-25 00:59:11 +03:00
|
|
|
|
2020-08-29 19:58:39 +03:00
|
|
|
######################################################################################
|
|
|
|
# STATIC METHODS #
|
|
|
|
######################################################################################
|
2020-06-20 23:34:38 +03:00
|
|
|
@staticmethod
|
|
|
|
def getList():
|
2020-08-29 19:58:39 +03:00
|
|
|
return Command._list # all commands
|
2020-06-20 23:34:38 +03:00
|
|
|
|
2020-07-31 19:38:20 +03:00
|
|
|
@staticmethod
|
2020-07-31 01:43:27 +03:00
|
|
|
def getRegexDict():
|
2020-08-29 19:58:39 +03:00
|
|
|
return Command._regex # all standart patterns
|
2020-07-31 01:43:27 +03:00
|
|
|
|
2020-07-31 19:38:20 +03:00
|
|
|
@staticmethod
|
2020-08-03 22:49:59 +03:00
|
|
|
def getEntity(key):
|
2020-08-29 19:58:39 +03:00
|
|
|
entity = Command._entities.get(key) # all linked $Pattern s
|
2020-08-03 22:49:59 +03:00
|
|
|
return entity()
|
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def append(obj): # add new command to list
|
2020-06-20 23:34:38 +03:00
|
|
|
Command._list.append(obj)
|
|
|
|
|
2020-07-31 01:43:27 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def getCommand(name): # get command by name
|
2020-07-31 19:38:20 +03:00
|
|
|
for obj in Command.getList():
|
|
|
|
if obj.getName() == name: return obj
|
2020-07-31 01:43:27 +03:00
|
|
|
|
2020-07-31 23:15:53 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def isRepeat(string): # return True if command is repeat-cmd
|
2020-08-03 22:49:59 +03:00
|
|
|
if re.search(re.compile(Command.getEntity('repeat')), string): return True
|
2020-07-31 23:15:53 +03:00
|
|
|
return False
|
|
|
|
|
2020-06-25 00:59:11 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def ratio(string, word): # get average distance of string and pattern
|
2020-06-25 00:59:11 +03:00
|
|
|
return ( fuzz.WRatio(string, word) + fuzz.ratio(string, word) ) / 2
|
|
|
|
|
2020-07-31 01:43:27 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def compilePattern(pattern): # transform my syntax to standart regexp
|
2020-07-31 01:43:27 +03:00
|
|
|
# transform patterns to regexp
|
|
|
|
for ptrn, regex in Command.getRegexDict().items():
|
|
|
|
pattern = re.sub(re.compile(ptrn), regex, pattern)
|
2020-08-29 19:58:39 +03:00
|
|
|
# find and replace links like $Pattern
|
2020-08-03 22:49:59 +03:00
|
|
|
link = re.search(re.compile('\$[a-z]+'), pattern)
|
|
|
|
if link: pattern = re.sub('\\'+link[0], f'(?P<{link[0][1:]}>{Command.getEntity( link[0][1:] )})', pattern)
|
2020-07-31 01:43:27 +03:00
|
|
|
# return compiled regexp
|
|
|
|
return pattern
|
|
|
|
|
2020-06-20 23:34:38 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def find(string): # find command by fuzzywuzzy
|
2020-06-22 17:01:44 +03:00
|
|
|
string = string.lower()
|
2020-06-20 23:34:38 +03:00
|
|
|
chances = {}
|
|
|
|
list = Command.getList()
|
2020-07-31 01:43:27 +03:00
|
|
|
# calculate chances of every command
|
2020-06-20 23:34:38 +03:00
|
|
|
for i, obj in enumerate( list ):
|
|
|
|
chances[i] = 0
|
2020-07-28 17:25:41 +03:00
|
|
|
k = 1 / ( sum( [int(w)*len(kw) for w, kw in obj.getKeywords().items()] ) or 1 )
|
2020-07-28 03:13:58 +03:00
|
|
|
for weight, kws in obj.getKeywords().items():
|
|
|
|
for kw in kws:
|
2020-07-28 17:25:41 +03:00
|
|
|
chances[i] += Command.ratio(string, kw) * weight * k
|
2020-07-31 01:43:27 +03:00
|
|
|
# find command with the biggest chance
|
2020-06-20 23:34:38 +03:00
|
|
|
if( sum( chances.values() ) ):
|
|
|
|
top = max( chances.values() ) / sum( chances.values() ) * 100
|
2020-07-31 01:43:27 +03:00
|
|
|
else: # if all chances is 0
|
2020-07-28 17:25:41 +03:00
|
|
|
return {
|
2020-07-31 19:38:20 +03:00
|
|
|
'cmd': Command.QA,
|
|
|
|
'params': {'string':string,},
|
|
|
|
}
|
2020-07-31 01:43:27 +03:00
|
|
|
#if( max( chances.values() ) < 800 or top < 50): return list[0]
|
|
|
|
# find top command obj
|
2020-06-20 23:34:38 +03:00
|
|
|
for i, chance in chances.items():
|
|
|
|
if chance == max( chances.values() ):
|
2020-07-28 17:25:41 +03:00
|
|
|
return {
|
2020-07-31 19:38:20 +03:00
|
|
|
'cmd': Command.QA, #dialog mode
|
|
|
|
'params': {'string':string,},
|
|
|
|
}
|
2020-07-19 22:27:01 +03:00
|
|
|
|
2020-07-28 03:13:58 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def reg_find(string): # find comman by pattern
|
2020-07-28 03:13:58 +03:00
|
|
|
string = string.lower()
|
|
|
|
list = Command.getList()
|
2020-07-31 19:38:20 +03:00
|
|
|
if not string: return {
|
|
|
|
'cmd': Command.getCommand('Hello'),
|
|
|
|
'params': {'string':string,},
|
2020-07-31 01:43:27 +03:00
|
|
|
}
|
|
|
|
# find command obj by pattern
|
2020-07-28 03:13:58 +03:00
|
|
|
for obj in list:
|
|
|
|
for pattern in obj.getPatterns():
|
2020-07-31 01:43:27 +03:00
|
|
|
if match := re.search(re.compile(Command.compilePattern(pattern)), string):
|
|
|
|
return {
|
2020-10-31 15:29:58 +02:00
|
|
|
'cmd': obj,
|
|
|
|
'params': {**match.groupdict(), 'string':string,},
|
|
|
|
}
|
2020-08-29 19:58:39 +03:00
|
|
|
# return Question-Answer system if command not found
|
2020-07-28 17:25:41 +03:00
|
|
|
return {
|
2020-07-31 19:38:20 +03:00
|
|
|
'cmd': Command.QA,
|
|
|
|
'params': {'string':string,},
|
2020-07-31 01:43:27 +03:00
|
|
|
}
|
2020-07-28 03:13:58 +03:00
|
|
|
|
2020-07-19 22:27:01 +03:00
|
|
|
@staticmethod
|
2020-08-29 19:58:39 +03:00
|
|
|
def background(answer = '', voice = ''): # make background cmd
|
2020-07-31 01:43:27 +03:00
|
|
|
def decorator(cmd): #wrapper of wrapper (decorator of decorator)
|
2020-07-19 22:27:01 +03:00
|
|
|
def wrapper(text):
|
|
|
|
finish_event = Event()
|
|
|
|
thread = RThread(target=cmd, args=(text, finish_event))
|
|
|
|
thread.start()
|
|
|
|
return {
|
|
|
|
'type': 'background',
|
|
|
|
'text': answer,
|
2020-07-28 17:25:41 +03:00
|
|
|
'voice': voice,
|
2020-07-19 22:27:01 +03:00
|
|
|
'thread': {
|
|
|
|
'thread': thread,
|
|
|
|
'finish_event': finish_event,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return wrapper
|
|
|
|
return decorator
|