-
Notifications
You must be signed in to change notification settings - Fork 84
/
RuleEngine.py
380 lines (350 loc) · 13.3 KB
/
RuleEngine.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# Copyright (C) 2009 Eric C. Schug
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
'''
Provided Extended Wrapping of CLIPS objects to be more python like
Constructs are build using native Python types which are forwarded to
the CLIPS engine as needed. Automatic forwarding of changes in the python
versions, however two way forwarding is limited do limitations of the CLIPS engine,
so all changes must originate within python.
'''
#TODO: list of stuff
# * map id back to rule and call function
# * add wrapping of rule conditions
# * add wrapping to support COOL types
# * add wrappers to support misc functions
# * add wrappers to support globals
# * add wrappers to support instances
# standard imports
import types
import copy
import re
# extra imports
import clips
#from string import Template
if 0:
#TODO: autogenerate script
from Cheetah.Template import Template
#define autocode templates for CLIPS
clips_tmpl = Template.compile('''
#def Module
(defmodule $name (export ?ALL))
#end def Module
#def Rule
(defrule $modulename::$name "blabla"
$conditions
=>
(python-call rule_autocallback $unique_id $rule_vars )
; (printout t 'in $modulename::$name ' crlf )
)
#end def Rule
#def Template
(deftemplate $modulename::$name
(slot unique_id (default-dynamic (gensym*)) )
#for $slotname,$slot in $exportslots.iteritems()
($slot.slot_style $slotname
#if $slot.default!=None
(default $slot.default)
#end if
#if $slot.clipstype!=None
(type $slot.clipstype)
#end if
)
#end for
)
#end def Template
#def Fact
(assert $name
#for $name, $value in $slots
($name $value)
#end for
)
#end def Fact
''')
def attribute_propdef(props, propdef):
'''
Simplifies declaration of properties or attributes and converts to dict of dicts
@type props: list (or tuple) of (propname, default) tuples
@param props: list of allowed attributes for propdef, attribute order is important
@param propdef: the propery definitions for the attributes
propdef = 'x' # export attribute x
propdef = 'x,y' # export attributes x and y
propdef = {'x': 'str'} #export attribute x which is a string
propdef = {'x':('str','value')} # and has a default value of 'value'
propdef = {'x':('type':'str', 'default':'value')} # and bla, bla
@rtype: dict of dicts
'''
#TODO: may need make a copy of propdef
if(isinstance(propdef, types.StringTypes)):
# if string split
propdef = [item.strip() for item in propdef.split(',')]
if(isinstance(propdef, (types.TupleType, types.ListType))):
# if list convert to dict
propdef = dict([(item, {}) for item in propdef])
if(not isinstance(propdef, types.DictionaryType)):
raise TypeError('Property definition must be a string or dictionary not a %s'
% type(propdef))
npropdef={}
for name, ipropvalues in propdef.iteritems():
#if(isinstance(ipropvalues, types.StringTypes)):
# #don't see a need to parse string for this
# ipropvalues = (ipropvalues, )
if(isinstance(ipropvalues, (types.TupleType, types.ListType))):
#if list convert to dict
prop_keys=[item[0] for item in props]
ipropvalues = dict(zip(prop_keys[:len(ipropvalues)], ipropvalues))
elif(not isinstance(ipropvalues, types.DictionaryType)):
raise TypeError('Property definition for %s must be a string or dictionary not a %s'
% (name, type(ipropvalues)))
else:
a = set(propdef.keys()).difference(props)
if(a):
raise AttributeError('Property definition for %s contains one or more unknown properties %s' % (name, ', '.join(a)) )
nprops=dict(props)
nprops.update(ipropvalues)
npropdef[name]=nprops
#print npropdef
return npropdef
def rule_autocallback(id, *matchargs):
print 'executing rule_callback', id, matchargs
rule=Rule.rule_index(id)
rule.function(*matchargs)
#print rule
clips.RegisterPythonFunction(rule_callback, 'rule_autocallback')
class Rule(object):
'CLIPS rule definition'
_rules_list=[]
@staticmethod
def append_rule(rule):
'append rule to internal list'
Rule._rules_list.append(rule)
@staticmethod
def rule_id(rule):
'get unique id for the rule'
return Rule._rules_list.index(rule)
@staticmethod
def remove_rule(rule):
'remove rule from internal list'
Rule._rules_list.remove(rule)
@staticmethod
def rule_index(id):
return Rule._rules_list[id]
def __init__(self, name, conditions, function, rule_vars = None):
Rule.append_rule(self)
self.name = name
self.conditions = conditions
self.function = function
if(rule_vars):
self.rule_vars = rule_vars
else:
self.rule_vars = ' '.join( re.findall(r'(\?\w+)',self.conditions))
def get_id(self):
'Return a unique id for this rule'
return Rule.rule_id(self)
def clips_init(self, modulename):
namespace={'modulename':modulename}
self.unique_id=self.get_id()
namespace.update(self.__dict__)
#print namespace
cheetah=clips_tmpl(namespaces=namespace)
strdef=cheetah.Rule()
return strdef
def __str__(self):
return 'Rule %s %s %s' % (self.name, self.conditions, self.function)
class CLIPSobj(object):
'Base Class for CLIPS Constructs'
def module_by_name(self, name):
return self.engine.get_module(name)
def __init__(self, name=None):
# name specified or inherited
self.name = (name or self.__class__.__name__)
#allow late binding of module
self._module = None
self._rules = {}
self.engine = None
#TODO: defrule = classmethod, staticmethod or instance
def new_rule(self, name, conditions, function, rule_vars=None):
''' declare a CLIPS rule which executes a function when a set conditions
are satisfied
@param name: rule name for referencing
@param conditions: rules activation conditions
@param function: the python function to execute
@type function: any python callable
@param rule_args: rule variables to pass to function
'''
#allow rules to be define from any construct so that
#rules requiring then can be deleted when the construct is deleted
#and making it more OO
if(self.module):
#TODO: check for overwrites of Rules
pass
self._rules[name] = Rule(name, conditions, function, rule_vars)
def clips_init(self, modulename):
'forwarding of definition to clips'
#module = module_by_name(self.modulename)
strdef=''
for name, item in self._rules.iteritems():
strdef+=item.clips_init(modulename)
return strdef
def get_module(self):
'return module'
return self._module
def set_module(self, module):
'set module to connect to'
self._module = module
module = property(get_module, set_module, doc='CLIPS module to bind to')
def __del__(self):
# delete rules
pass
class BaseModule(CLIPSobj):
modulename = '*'
def __init__(self, name):
CLIPSobj.__init__(self, name=name)
self.name = name
#self.code_tmpl = clipstemps['module']
self._constructs=[]
def __del__(self):
CLIPSobj.__del__(self)
def add_construct(self, construct):
self._constructs.append(construct)
def get_module(self):
'return module (itself)'
return self
module = property(get_module, doc='override always return self')
def clips_init(self):
'forwarding of definition to clips'
#return self.template(di)
namespace={}
namespace.update(self.__dict__)
#print namespace
cheetah=clips_tmpl(namespaces=namespace)
strdef=cheetah.Module()
strdef+='; Rules from %s\n\n' % self.name
strdef+=CLIPSobj.clips_init(self, self.name)
for item in self._constructs:
strdef+=item.clips_init(self.name)
return strdef
#clips.Build('' % self.name)
clips_typemap={
types.StringType:'STRING',
types.IntType:'INTEGER',
types.LongType:'INTEGER',
types.FloatType:'FLOAT',
}
class SlotDef:
'Defines CLIPS Slot'
def __init__(self, dictdef):
self.slot_style='slot'
for name, value in dictdef.iteritems():
setattr(self, name, value)
if(isinstance(self.type, (types.ListType, types.TupleType))):
self.clipstype=[self.map_type(itype) for itype in self.type]
else:
self.clipstype=self.map_type(self.type)
def map_type(self, typedef):
if(clips_typemap.has_key(typedef)):
return clips_typemap[typedef]
elif(typedef == types.ListType):
self.slot_style='multislot'
return None
else:
return None
class BaseTemplate(CLIPSobj):
'''
Base class for wrapping CLIPS template
Access Template slots via dictionary calls e.g.
print fact['slotname']
fact['slotname'] = value
exportslots = 'x' # export attribute x
exportslots = 'x,y' # export attributes x and y
exportslots = {'x': 'str'} #export attribute x which is a string
exportslots = {'x':('str','value')} # and has a default value of 'value'
exportslots = {'x':('type':'str', 'default':'value')} # and bla, bla
'''
exportslots = None
def __init__(self, exportslots=None, name=None):
CLIPSobj.__init__(self, name=name)
# exportslots specified or inherited
#TODO: if specified as empty [] should be empty
self.exportslots = (exportslots or self.exportslots or {})
#verify slots definition and convert dict of dicts
slot_props=(('type', types.NoneType), ('default', None), ('constraint', None), ('doc', None))
slotsdef = attribute_propdef(slot_props, self.exportslots)
#print slotsdef
self.exportslots=dict([(name, SlotDef(slot)) for name, slot in slotsdef.iteritems()])
self.clips_id = None
# slots are dict of self and not se
#self._slots = {}
#for name, islotdef in slotsdef.iteritems():
# if(callable(islotdef['default'])):
# #TODO: late binding of arguments
# value = islotdef['default']()
# else:
# value = islotdef['default']
# self[name] = value
#for name, value in slots_init.iteritems():
# self[name] = value
def __del__(self):
CLIPSobj.__del__(self)
def __getitem__(self, name):
'returns the slot by name'
return self._slots[name]
def __setitem__(self, name, value):
'sets the slot by name'
self._slots[name] = value
if(self.clips_id != None):
#notification to clips engine
clips.SendCommand('(modify %s (%s %s) )' % (self.clips_id, name, value))
def clips_init(self, modulename):
'forwarding of definition to clips'
namespace={'modulename':modulename}
namespace.update(self.__dict__)
#print namespace
cheetah=clips_tmpl(namespaces=namespace)
strdef=cheetah.Template()
strdef+='; Rules from %s::%s\n\n' % (modulename, self.name)
strdef+=CLIPSobj.clips_init(self, modulename)
return strdef
class Engine(object):
'''Stores Modules
'''
def __init__(self ):
# should be one of
#_engine = clips
#_engine = clips.Environment()
self._engine = clips
self._modules = {}
def add_module(self, module):
self._modules[module.name] = module
def new_module(self, name, props=None):
mod=BaseModule(name)
self._modules[name] = mod
return mod
def add_construct(self, construct, modulename='MAIN'):
if(not isinstance(construct, CLIPSobj)):
raise TypeError("construct must be a CLIPSobj not a %s" % type(construct))
if(isinstance(construct, BaseModule)):
self.add_module(construct)
else:
self._modules[modulename].add_construct(construct)
def __get_item__(self, name):
return self._modules[name]
def remove_module(self, name):
del self._modules[name]
def clips_init(self):
strdef=''
#strdef+='(deffunction python-call (?name $?args) (printout t "python-call" ?name " " $?args crlf))\n\n'
for name, mod in self._modules.iteritems():
strdef+=mod.clips_init()
return strdef