bspl 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bspl/__init__.py +2 -0
- bspl/adapter/__init__.py +3 -0
- bspl/adapter/core.py +624 -0
- bspl/adapter/emitter.py +253 -0
- bspl/adapter/event.py +28 -0
- bspl/adapter/jason.py +190 -0
- bspl/adapter/message.py +248 -0
- bspl/adapter/policies.py +264 -0
- bspl/adapter/policy.gr +32 -0
- bspl/adapter/receiver.py +134 -0
- bspl/adapter/scheduler.py +94 -0
- bspl/adapter/schema.py +41 -0
- bspl/adapter/statistics.py +79 -0
- bspl/adapter/store.py +316 -0
- bspl/generators/__init__.py +12 -0
- bspl/generators/asl.py +273 -0
- bspl/generators/mambo.py +75 -0
- bspl/generators/node_red.py +268 -0
- bspl/langshaw.py +664 -0
- bspl/main.py +132 -0
- bspl/parsers/bspl/__init__.py +128 -0
- bspl/parsers/bspl/bspl.gr +32 -0
- bspl/parsers/bspl/bspl_parser.py +322 -0
- bspl/parsers/bspl/build.py +24 -0
- bspl/parsers/langshaw/__init__.py +39 -0
- bspl/parsers/langshaw/build.py +24 -0
- bspl/parsers/langshaw/langshaw.gr +46 -0
- bspl/parsers/precedence/__init__.py +18 -0
- bspl/parsers/precedence/precedence.gr +33 -0
- bspl/protocol.py +733 -0
- bspl/utils.py +45 -0
- bspl/validation.py +107 -0
- bspl/verification/__init__.py +195 -0
- bspl/verification/logic.py +98 -0
- bspl/verification/lpaths.py +569 -0
- bspl/verification/mambo.py +415 -0
- bspl/verification/paths.py +882 -0
- bspl/verification/precedence.py +411 -0
- bspl/verification/refinement.py +153 -0
- bspl/verification/sat.py +485 -0
- bspl-1.0.0.dist-info/METADATA +372 -0
- bspl-1.0.0.dist-info/RECORD +46 -0
- bspl-1.0.0.dist-info/WHEEL +5 -0
- bspl-1.0.0.dist-info/entry_points.txt +2 -0
- bspl-1.0.0.dist-info/licenses/LICENSE +21 -0
- bspl-1.0.0.dist-info/top_level.txt +1 -0
bspl/__init__.py
ADDED
bspl/adapter/__init__.py
ADDED
bspl/adapter/core.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import aiorun
|
|
3
|
+
import logging
|
|
4
|
+
import json
|
|
5
|
+
import datetime
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import math
|
|
9
|
+
import socket
|
|
10
|
+
import inspect
|
|
11
|
+
import yaml
|
|
12
|
+
import agentspeak
|
|
13
|
+
import agentspeak.stdlib
|
|
14
|
+
import random
|
|
15
|
+
import colorama
|
|
16
|
+
from types import MethodType
|
|
17
|
+
from asyncio.queues import Queue
|
|
18
|
+
from .store import Store
|
|
19
|
+
from .message import Message
|
|
20
|
+
from functools import partial
|
|
21
|
+
from .emitter import Emitter
|
|
22
|
+
from .receiver import Receiver
|
|
23
|
+
from .scheduler import Scheduler, exponential
|
|
24
|
+
from .statistics import stats, increment
|
|
25
|
+
from .jason import Environment, Agent, Actions, actions
|
|
26
|
+
from .event import Event, ObservationEvent, ReceptionEvent, EmissionEvent, InitEvent
|
|
27
|
+
from . import policies
|
|
28
|
+
from ..protocol import Parameter
|
|
29
|
+
import bspl
|
|
30
|
+
import bspl.adapter.jason
|
|
31
|
+
import bspl.adapter.schema
|
|
32
|
+
from bspl.utils import identity
|
|
33
|
+
|
|
34
|
+
FORMAT = "%(asctime)-15s %(module)s: %(message)s"
|
|
35
|
+
logging.basicConfig(format=FORMAT, level=logging.INFO)
|
|
36
|
+
logger = logging.getLogger("bspl")
|
|
37
|
+
|
|
38
|
+
SUPERCRITICAL = logging.CRITICAL + 10 # don't want any logs
|
|
39
|
+
logging.getLogger("aiorun").setLevel(SUPERCRITICAL)
|
|
40
|
+
|
|
41
|
+
COLORS = [
|
|
42
|
+
(colorama.Back.GREEN, colorama.Fore.BLACK),
|
|
43
|
+
(colorama.Back.MAGENTA, colorama.Fore.BLACK),
|
|
44
|
+
(colorama.Back.YELLOW, colorama.Fore.BLACK),
|
|
45
|
+
(colorama.Back.BLUE, colorama.Fore.BLACK),
|
|
46
|
+
(colorama.Back.CYAN, colorama.Fore.BLACK),
|
|
47
|
+
(colorama.Back.RED, colorama.Fore.BLACK),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def select_endpoint(agent_endpoints, system_name):
|
|
52
|
+
"""
|
|
53
|
+
Select one endpoint from a list of agent endpoints using deterministic hash-based selection.
|
|
54
|
+
|
|
55
|
+
This function provides session affinity - the same system will always connect to the same
|
|
56
|
+
endpoint when multiple endpoints are available. This deterministic behavior aids in debugging
|
|
57
|
+
and ensures consistent message routing.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
agent_endpoints: Either a single endpoint tuple (host, port) or a list of such tuples
|
|
61
|
+
system_name: Name of the system, used as hash input for deterministic selection
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A single endpoint tuple (host, port)
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(agent_endpoints, list):
|
|
67
|
+
if len(agent_endpoints) == 1:
|
|
68
|
+
return agent_endpoints[0]
|
|
69
|
+
# Use hash of system name for deterministic selection
|
|
70
|
+
index = hash(system_name) % len(agent_endpoints)
|
|
71
|
+
return agent_endpoints[index]
|
|
72
|
+
else:
|
|
73
|
+
# Single endpoint (tuple)
|
|
74
|
+
return agent_endpoints
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Adapter:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
name,
|
|
81
|
+
systems,
|
|
82
|
+
agents,
|
|
83
|
+
emitter=Emitter(),
|
|
84
|
+
receiver=None,
|
|
85
|
+
color=None,
|
|
86
|
+
in_place=False,
|
|
87
|
+
debug=False,
|
|
88
|
+
**kwargs,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Initialize the agent adapter.
|
|
92
|
+
|
|
93
|
+
name: name of this agent
|
|
94
|
+
systems: a list of MAS to participate in
|
|
95
|
+
agents: a dictionary mapping agent names to endpoints
|
|
96
|
+
emitter: encodes messages for transmission over the network
|
|
97
|
+
receiver: reads messages from the network and decodes them
|
|
98
|
+
color: distinguish agent by color in console logs
|
|
99
|
+
in_place: detect completed forms instead of using return value
|
|
100
|
+
debug: turn on debug logging when True
|
|
101
|
+
"""
|
|
102
|
+
self.name = name
|
|
103
|
+
|
|
104
|
+
self.logger = logging.getLogger(f"bspl.adapter.{name}")
|
|
105
|
+
self.logger.propagate = False
|
|
106
|
+
color = color or (
|
|
107
|
+
COLORS[hash(name) % len(COLORS)]
|
|
108
|
+
if name
|
|
109
|
+
else COLORS[random.randint(0, len(COLORS) - 1)]
|
|
110
|
+
)
|
|
111
|
+
self.color = agentspeak.stdlib.COLORS[0] = color
|
|
112
|
+
reset = colorama.Fore.RESET + colorama.Back.RESET
|
|
113
|
+
formatter = logging.Formatter(
|
|
114
|
+
f"%(asctime)-15s ({''.join(self.color)}{name}{reset}): %(message)s"
|
|
115
|
+
)
|
|
116
|
+
handler = logging.StreamHandler()
|
|
117
|
+
handler.setFormatter(formatter)
|
|
118
|
+
self.logger.handlers.clear()
|
|
119
|
+
self.logger.addHandler(handler)
|
|
120
|
+
# Check for environment variable to enable debug mode
|
|
121
|
+
env_debug = os.environ.get("BSPL_ADAPTER_DEBUG", "").lower() in (
|
|
122
|
+
"1",
|
|
123
|
+
"true",
|
|
124
|
+
"yes",
|
|
125
|
+
"on",
|
|
126
|
+
)
|
|
127
|
+
if debug or env_debug:
|
|
128
|
+
logging.getLogger("bspl").setLevel(logging.DEBUG)
|
|
129
|
+
|
|
130
|
+
self.roles = {
|
|
131
|
+
r for s in systems.values() for r in s["roles"] if s["roles"][r] == name
|
|
132
|
+
}
|
|
133
|
+
self.protocols = [s["protocol"] for s in systems.values()]
|
|
134
|
+
self.systems = systems
|
|
135
|
+
self.agents = agents
|
|
136
|
+
self.addresses = self.agents[self.name]
|
|
137
|
+
self.reactors = {} # dict of message -> [handlers]
|
|
138
|
+
self.generators = {} # dict of (scheema tuples) -> [handlers]
|
|
139
|
+
self.history = Store(systems)
|
|
140
|
+
self.emitter = emitter
|
|
141
|
+
if receiver:
|
|
142
|
+
self.receivers = [receiver]
|
|
143
|
+
else:
|
|
144
|
+
self.receivers = []
|
|
145
|
+
for addr in self.addresses:
|
|
146
|
+
self.receivers.append(Receiver(addr))
|
|
147
|
+
self.schedulers = []
|
|
148
|
+
self.messages = {
|
|
149
|
+
message.qualified_name: message
|
|
150
|
+
for p in self.protocols
|
|
151
|
+
for name, message in p.messages.items()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for p in self.protocols:
|
|
155
|
+
self.inject(p)
|
|
156
|
+
|
|
157
|
+
self.events = Queue()
|
|
158
|
+
self.enabled_messages = Store(systems)
|
|
159
|
+
self.decision_handlers = {}
|
|
160
|
+
self._in_place = in_place
|
|
161
|
+
self.kwargs = kwargs
|
|
162
|
+
|
|
163
|
+
def debug(self, msg):
|
|
164
|
+
self.logger.debug(msg)
|
|
165
|
+
|
|
166
|
+
def info(self, msg):
|
|
167
|
+
self.logger.info(msg)
|
|
168
|
+
|
|
169
|
+
def warning(self, msg):
|
|
170
|
+
self.logger.warning(msg)
|
|
171
|
+
|
|
172
|
+
def inject(self, protocol):
|
|
173
|
+
"""Install helper methods into schema objects"""
|
|
174
|
+
|
|
175
|
+
from bspl.protocol import Message
|
|
176
|
+
|
|
177
|
+
Message.__call__ = bspl.adapter.schema.instantiate(self)
|
|
178
|
+
|
|
179
|
+
for m in protocol.messages.values():
|
|
180
|
+
m.match = MethodType(bspl.adapter.schema.match, m)
|
|
181
|
+
m.adapter = self
|
|
182
|
+
|
|
183
|
+
async def receive(self, data):
|
|
184
|
+
if not isinstance(data, dict):
|
|
185
|
+
self.warning("Data does not parse to a dictionary: {}".format(data))
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
schema = self.messages[data["schema"]]
|
|
189
|
+
message = Message(schema, data["payload"], meta=data.get("meta", {}))
|
|
190
|
+
message.meta["received"] = datetime.datetime.now()
|
|
191
|
+
if self.history.is_duplicate(message):
|
|
192
|
+
self.debug("Duplicate message: {}".format(message))
|
|
193
|
+
increment("dups")
|
|
194
|
+
# Don't react to duplicate messages
|
|
195
|
+
# message.duplicate = True
|
|
196
|
+
# await self.react(message)
|
|
197
|
+
elif self.history.check_integrity(message):
|
|
198
|
+
self.debug("Received message: {}".format(message))
|
|
199
|
+
increment("receptions")
|
|
200
|
+
self.history.add(message)
|
|
201
|
+
await self.signal(ReceptionEvent(message))
|
|
202
|
+
|
|
203
|
+
async def send(self, *messages):
|
|
204
|
+
def prep(message):
|
|
205
|
+
# Handle multiple recipients by creating copies for each destination
|
|
206
|
+
prepared_messages = []
|
|
207
|
+
|
|
208
|
+
if hasattr(message, "_dest") and message._dest:
|
|
209
|
+
# Backwards compatibility: single dest already set
|
|
210
|
+
prepared_messages.append(message)
|
|
211
|
+
else:
|
|
212
|
+
system = self.systems[message.system]
|
|
213
|
+
# Create a copy for each recipient
|
|
214
|
+
for recipient_role in message.schema.recipients:
|
|
215
|
+
recipient_agent = system["roles"][recipient_role]
|
|
216
|
+
agent_endpoints = self.agents[recipient_agent]
|
|
217
|
+
|
|
218
|
+
# Handle multiple endpoints per agent using deterministic selection
|
|
219
|
+
endpoint = select_endpoint(agent_endpoints, message.system)
|
|
220
|
+
|
|
221
|
+
# Create message copy with specific destination
|
|
222
|
+
msg_copy = Message(
|
|
223
|
+
message.schema,
|
|
224
|
+
message.payload.copy(),
|
|
225
|
+
message.meta.copy(),
|
|
226
|
+
message.acknowledged,
|
|
227
|
+
endpoint,
|
|
228
|
+
message.adapter,
|
|
229
|
+
message.system,
|
|
230
|
+
)
|
|
231
|
+
prepared_messages.append(msg_copy)
|
|
232
|
+
|
|
233
|
+
return prepared_messages
|
|
234
|
+
|
|
235
|
+
# Flatten the list of message copies from prep
|
|
236
|
+
all_prepared = []
|
|
237
|
+
for m in messages:
|
|
238
|
+
if not self.history.is_duplicate(m):
|
|
239
|
+
prepared = prep(m)
|
|
240
|
+
all_prepared.extend(prepared)
|
|
241
|
+
emissions = set(all_prepared)
|
|
242
|
+
if len(emissions) < len(messages):
|
|
243
|
+
self.info(
|
|
244
|
+
f"Skipped {len(messages) - len(emissions)} duplicate messages: {set(messages).difference(emissions)}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if self.history.check_emissions(emissions):
|
|
248
|
+
self.debug(f"Sending {emissions}")
|
|
249
|
+
for m in emissions:
|
|
250
|
+
increment("emissions")
|
|
251
|
+
increment("observations")
|
|
252
|
+
self.history.add(m)
|
|
253
|
+
if len(emissions) > 1 and hasattr(self.emitter, "bulk_send"):
|
|
254
|
+
self.debug(f"bulk sending {len(emissions)} messages")
|
|
255
|
+
await self.emitter.bulk_send(emissions)
|
|
256
|
+
else:
|
|
257
|
+
for m in emissions:
|
|
258
|
+
await self.emitter.send(m)
|
|
259
|
+
await self.signal(EmissionEvent(emissions))
|
|
260
|
+
|
|
261
|
+
def register_reactor(self, schema, handler, index=None):
|
|
262
|
+
if schema in self.reactors:
|
|
263
|
+
rs = self.reactors[schema]
|
|
264
|
+
if handler not in rs:
|
|
265
|
+
rs.insert(index if index is not None else len(rs), handler)
|
|
266
|
+
else:
|
|
267
|
+
self.reactors[schema] = [handler]
|
|
268
|
+
|
|
269
|
+
def register_reactors(self, handler, schemas=[]):
|
|
270
|
+
for s in schemas:
|
|
271
|
+
self.register_reactor(s, handler)
|
|
272
|
+
|
|
273
|
+
def clear_reactors(self, *schemas):
|
|
274
|
+
for s in schemas:
|
|
275
|
+
self.reactors[s] = []
|
|
276
|
+
|
|
277
|
+
def reaction(self, *schemas):
|
|
278
|
+
"""
|
|
279
|
+
Decorator for declaring reactor handler.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
@adapter.reaction(MessageSchema)
|
|
283
|
+
async def handle_message(message):
|
|
284
|
+
'do stuff'
|
|
285
|
+
"""
|
|
286
|
+
return partial(self.register_reactors, schemas=schemas)
|
|
287
|
+
|
|
288
|
+
async def react(self, message):
|
|
289
|
+
"""
|
|
290
|
+
Handle emission/reception of message by invoking corresponding reactors.
|
|
291
|
+
"""
|
|
292
|
+
reactors = self.reactors.get(message.schema)
|
|
293
|
+
if reactors:
|
|
294
|
+
for r in reactors:
|
|
295
|
+
message.adapter = self
|
|
296
|
+
await r(message)
|
|
297
|
+
|
|
298
|
+
def enabled(self, *schemas, **options):
|
|
299
|
+
"""
|
|
300
|
+
Decorator for declaring enabled message generators.
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
@adapter.enabled(MessageSchema)
|
|
304
|
+
async def generate_message(msg):
|
|
305
|
+
msg.bind("param", value)
|
|
306
|
+
return msg
|
|
307
|
+
"""
|
|
308
|
+
return partial(self.register_generators, schemas=schemas, options=options)
|
|
309
|
+
|
|
310
|
+
def register_generators(self, handler, schemas, options={}):
|
|
311
|
+
if schemas in self.generators:
|
|
312
|
+
gs = self.generators[schemas]
|
|
313
|
+
if handler not in gs:
|
|
314
|
+
gs.insert(index if index is not None else len(gs), handler)
|
|
315
|
+
else:
|
|
316
|
+
self.generators[schemas] = [handler]
|
|
317
|
+
|
|
318
|
+
async def handle_enabled(self, message):
|
|
319
|
+
"""
|
|
320
|
+
Handle newly observed message by checking for newly enabled messages.
|
|
321
|
+
|
|
322
|
+
1. Cycle through all registered schema tuples
|
|
323
|
+
2. Check if all messages in tuple are enabled
|
|
324
|
+
3. If so, invoke the handlers in sequence
|
|
325
|
+
4. Continue until a message is returned
|
|
326
|
+
5. Break loop after the first handler returns a message, and send it
|
|
327
|
+
|
|
328
|
+
Note: sending a message triggers the loop again
|
|
329
|
+
"""
|
|
330
|
+
for tup in self.generators.keys():
|
|
331
|
+
for group in zip(*(schema.match(message) for schema in tup)):
|
|
332
|
+
for handler in self.generators[tup]:
|
|
333
|
+
partials = [m.partial() for m in group]
|
|
334
|
+
# assume it returns only one message for now
|
|
335
|
+
msg = await handler(*partials)
|
|
336
|
+
if self._in_place:
|
|
337
|
+
instances = []
|
|
338
|
+
for m in partials:
|
|
339
|
+
self.debug(f"Checking {m}")
|
|
340
|
+
if m.instances:
|
|
341
|
+
instances.extend(m.instances)
|
|
342
|
+
m.instances.clear()
|
|
343
|
+
if instances:
|
|
344
|
+
self.debug(f"found instances: {instances}")
|
|
345
|
+
await self.send(*instances)
|
|
346
|
+
# short circuit on first message(s) to send
|
|
347
|
+
return
|
|
348
|
+
elif msg:
|
|
349
|
+
await self.send(msg)
|
|
350
|
+
# short circuit on first message to send
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
def decision(
|
|
354
|
+
self, handler=None, event=None, filter=None, received=None, sent=None, **kwargs
|
|
355
|
+
):
|
|
356
|
+
"""
|
|
357
|
+
Decorator for declaring decision handlers.
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
@adapter.decision
|
|
361
|
+
async def decide(enabled)
|
|
362
|
+
for m in enabled:
|
|
363
|
+
if m.schema is Quote:
|
|
364
|
+
m.bind("price", 10)
|
|
365
|
+
return m
|
|
366
|
+
"""
|
|
367
|
+
fn = identity
|
|
368
|
+
if event != None:
|
|
369
|
+
if isinstance(event, str):
|
|
370
|
+
prev = fn
|
|
371
|
+
fn = lambda e: (prev(e) and (hasattr(e, "type") and e.type == event))
|
|
372
|
+
elif issubclass(event, Event):
|
|
373
|
+
prev = fn
|
|
374
|
+
fn = lambda e: (prev(e) and isinstance(e, event))
|
|
375
|
+
elif isinstance(event, bspl.protocol.Message):
|
|
376
|
+
schema = event
|
|
377
|
+
prev = fn
|
|
378
|
+
fn = lambda e: (
|
|
379
|
+
prev(e)
|
|
380
|
+
and isinstance(e, ObservationEvent)
|
|
381
|
+
and any(m.schema == event for m in e.messages)
|
|
382
|
+
)
|
|
383
|
+
if received != None:
|
|
384
|
+
schema = received
|
|
385
|
+
prev = fn
|
|
386
|
+
fn = lambda e: (
|
|
387
|
+
prev(e)
|
|
388
|
+
and isinstance(e, ReceptionEvent)
|
|
389
|
+
and any(m.schema == event for m in e.messages)
|
|
390
|
+
)
|
|
391
|
+
if sent != None:
|
|
392
|
+
schema = sent
|
|
393
|
+
prev = fn
|
|
394
|
+
fn = lambda e: (
|
|
395
|
+
prev(e)
|
|
396
|
+
and isinstance(e, EmissionEvent)
|
|
397
|
+
and any(m.schema == event for m in e.messages)
|
|
398
|
+
)
|
|
399
|
+
if filter != None:
|
|
400
|
+
prev = fn
|
|
401
|
+
fn = lambda e: (prev(e) and filter(e))
|
|
402
|
+
|
|
403
|
+
if kwargs:
|
|
404
|
+
|
|
405
|
+
def match(event):
|
|
406
|
+
for k in kwargs:
|
|
407
|
+
if k in event:
|
|
408
|
+
return event[k] == kwargs[k]
|
|
409
|
+
|
|
410
|
+
prev = fn
|
|
411
|
+
fn = lambda e: (prev(e) and match(e))
|
|
412
|
+
|
|
413
|
+
def register(handler):
|
|
414
|
+
if fn not in self.decision_handlers:
|
|
415
|
+
self.decision_handlers[fn] = {handler}
|
|
416
|
+
else:
|
|
417
|
+
self.decision_handlers[fn].add(handler)
|
|
418
|
+
|
|
419
|
+
if handler != None:
|
|
420
|
+
register(handler)
|
|
421
|
+
else:
|
|
422
|
+
return register
|
|
423
|
+
|
|
424
|
+
def add_policies(self, *ps, when=None):
|
|
425
|
+
s = None
|
|
426
|
+
if when:
|
|
427
|
+
s = Scheduler(when)
|
|
428
|
+
self.schedulers.append(s)
|
|
429
|
+
for policy in ps:
|
|
430
|
+
policy.install(self, s)
|
|
431
|
+
|
|
432
|
+
def load_policies(self, spec):
|
|
433
|
+
if type(spec) is str:
|
|
434
|
+
spec = yaml.full_load(spec)
|
|
435
|
+
if any(r.name in spec for r in self.roles):
|
|
436
|
+
for r in self.roles:
|
|
437
|
+
if r.name in spec:
|
|
438
|
+
for condition, ps in spec[r.name].items():
|
|
439
|
+
self.add_policies(*ps, when=condition)
|
|
440
|
+
else:
|
|
441
|
+
# Assume the file contains policies only for agent
|
|
442
|
+
for condition, ps in spec.items():
|
|
443
|
+
self.add_policies(*ps, when=condition)
|
|
444
|
+
|
|
445
|
+
def load_policy_file(self, path):
|
|
446
|
+
with open(path) as file:
|
|
447
|
+
spec = yaml.full_load(file)
|
|
448
|
+
self.load_policies(spec)
|
|
449
|
+
|
|
450
|
+
def start(self, *tasks, use_uvloop=True):
|
|
451
|
+
if use_uvloop:
|
|
452
|
+
try:
|
|
453
|
+
import uvloop
|
|
454
|
+
except:
|
|
455
|
+
use_uvloop = False
|
|
456
|
+
|
|
457
|
+
async def main():
|
|
458
|
+
self.events = Queue()
|
|
459
|
+
loop = asyncio.get_running_loop()
|
|
460
|
+
loop.create_task(self.update_loop())
|
|
461
|
+
|
|
462
|
+
for r in self.receivers:
|
|
463
|
+
await r.task(self)
|
|
464
|
+
|
|
465
|
+
if hasattr(self.emitter, "task"):
|
|
466
|
+
await self.emitter.task()
|
|
467
|
+
|
|
468
|
+
for s in self.schedulers:
|
|
469
|
+
# todo: add stop event support
|
|
470
|
+
loop.create_task(s.task(self))
|
|
471
|
+
|
|
472
|
+
await self.signal(InitEvent())
|
|
473
|
+
|
|
474
|
+
for t in tasks:
|
|
475
|
+
loop.create_task(t)
|
|
476
|
+
|
|
477
|
+
self.running = True
|
|
478
|
+
aiorun.run(main(), stop_on_unhandled_errors=True, use_uvloop=use_uvloop)
|
|
479
|
+
|
|
480
|
+
async def stop(self):
|
|
481
|
+
await self.receiver.stop()
|
|
482
|
+
await self.emitter.stop()
|
|
483
|
+
self.running = False
|
|
484
|
+
|
|
485
|
+
async def signal(self, event):
|
|
486
|
+
"""
|
|
487
|
+
Publish an event for triggering the update loop
|
|
488
|
+
"""
|
|
489
|
+
if not hasattr(self, "events"):
|
|
490
|
+
self.events = Queue()
|
|
491
|
+
if isinstance(event, str):
|
|
492
|
+
event = Event(event)
|
|
493
|
+
await self.events.put(event)
|
|
494
|
+
|
|
495
|
+
async def update(self):
|
|
496
|
+
event = await self.events.get()
|
|
497
|
+
emissions = await self.process(event)
|
|
498
|
+
if emissions:
|
|
499
|
+
await self.send(*emissions)
|
|
500
|
+
|
|
501
|
+
async def update_loop(self):
|
|
502
|
+
while self.running:
|
|
503
|
+
await self.update()
|
|
504
|
+
|
|
505
|
+
async def process(self, event):
|
|
506
|
+
"""
|
|
507
|
+
Process a single functional step in a decision loop
|
|
508
|
+
|
|
509
|
+
(state, observations) -> (state, enabled, event) -> (state, emissions) -> state
|
|
510
|
+
- state :: the local state, history of observed messages + other local information
|
|
511
|
+
- event :: an object representing the new information that triggered the processing loop; could be an observed message or a signal from the agent internals or environment
|
|
512
|
+
- enabled :: a set of all currently enabled messages, indexed by their keys; the enabled set is incrementally constructed and stored in the state
|
|
513
|
+
- emissions :: a list of message instance for sending
|
|
514
|
+
|
|
515
|
+
State can be threaded through the entire loop to make it more purely functional, or left implicit (e.g. a property of the adapter) for simplicity
|
|
516
|
+
Events need a specific structure;
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
emissions = []
|
|
520
|
+
|
|
521
|
+
if isinstance(event, ObservationEvent):
|
|
522
|
+
# Update the enabled messages if there was an emission or reception
|
|
523
|
+
observations = event.messages
|
|
524
|
+
for m in observations:
|
|
525
|
+
self.debug(f"observing: {m}")
|
|
526
|
+
if "trace" in self.kwargs:
|
|
527
|
+
# if tracing is enabled, log the observation
|
|
528
|
+
if event.type == "reception":
|
|
529
|
+
self.info(f"Received: {m}")
|
|
530
|
+
elif event.type == "emission":
|
|
531
|
+
self.info(f"Sent: {m}")
|
|
532
|
+
if hasattr(self, "bdi"):
|
|
533
|
+
self.bdi.observe(m)
|
|
534
|
+
# wake up bdi logic
|
|
535
|
+
self.environment.wake_signal.set()
|
|
536
|
+
await self.react(m)
|
|
537
|
+
await self.handle_enabled(m)
|
|
538
|
+
event = self.compute_enabled(observations)
|
|
539
|
+
elif isinstance(event, InitEvent):
|
|
540
|
+
self.construct_initiators()
|
|
541
|
+
|
|
542
|
+
for fn in self.decision_handlers:
|
|
543
|
+
if fn(event):
|
|
544
|
+
for d in self.decision_handlers[fn]:
|
|
545
|
+
s = inspect.signature(d).parameters
|
|
546
|
+
result = None
|
|
547
|
+
if len(s) == 1:
|
|
548
|
+
result = await d(self.enabled_messages)
|
|
549
|
+
elif len(s) == 2:
|
|
550
|
+
result = await d(self.enabled_messages, event)
|
|
551
|
+
|
|
552
|
+
if self._in_place:
|
|
553
|
+
instances = []
|
|
554
|
+
for m in self.enabled_messages.messages():
|
|
555
|
+
if m.instances:
|
|
556
|
+
instances.extend(m.instances)
|
|
557
|
+
m.instances.clear()
|
|
558
|
+
emissions.extend(instances)
|
|
559
|
+
elif result:
|
|
560
|
+
# Handle both single messages and lists/collections
|
|
561
|
+
if (
|
|
562
|
+
hasattr(result, "__iter__")
|
|
563
|
+
and not isinstance(result, (str, dict))
|
|
564
|
+
and not hasattr(result, "schema")
|
|
565
|
+
):
|
|
566
|
+
emissions.extend(result)
|
|
567
|
+
else:
|
|
568
|
+
emissions.append(result)
|
|
569
|
+
|
|
570
|
+
if hasattr(self, "bdi"):
|
|
571
|
+
emissions.extend(
|
|
572
|
+
bspl.adapter.jason.bdi_handler(self.bdi, self.enabled_messages, event)
|
|
573
|
+
)
|
|
574
|
+
self.environment.wake_signal.set()
|
|
575
|
+
return emissions
|
|
576
|
+
|
|
577
|
+
def construct_initiators(self):
|
|
578
|
+
# Add initioators
|
|
579
|
+
for sID, s in self.systems.items():
|
|
580
|
+
for m in s["protocol"].initiators():
|
|
581
|
+
if m.sender in self.roles:
|
|
582
|
+
p = m().partial()
|
|
583
|
+
p.meta["system"] = sID
|
|
584
|
+
self.enabled_messages.add(p)
|
|
585
|
+
|
|
586
|
+
def compute_enabled(self, observations):
|
|
587
|
+
"""
|
|
588
|
+
Compute updates to the enabled set based on a list of an observations
|
|
589
|
+
"""
|
|
590
|
+
# clear out matching keys from enabled set
|
|
591
|
+
removed = set()
|
|
592
|
+
for msg in observations:
|
|
593
|
+
context = self.enabled_messages.context(msg)
|
|
594
|
+
removed.update(context.messages())
|
|
595
|
+
context.clear()
|
|
596
|
+
|
|
597
|
+
added = set()
|
|
598
|
+
for o in observations:
|
|
599
|
+
for schema in self.systems[o.system]["protocol"].messages.values():
|
|
600
|
+
if schema.sender in self.roles:
|
|
601
|
+
added.update(schema.match(o))
|
|
602
|
+
for m in added:
|
|
603
|
+
self.debug(f"new enabled message: {m}")
|
|
604
|
+
self.enabled_messages.add(m.partial())
|
|
605
|
+
removed.difference_update(added)
|
|
606
|
+
|
|
607
|
+
return {"added": added, "removed": removed, "observations": observations}
|
|
608
|
+
|
|
609
|
+
@property
|
|
610
|
+
def environment(self):
|
|
611
|
+
if not hasattr(self, "_env"):
|
|
612
|
+
self._env = Environment()
|
|
613
|
+
# enable asynchronous processing of environment
|
|
614
|
+
self.schedulers.append(self._env)
|
|
615
|
+
return self._env
|
|
616
|
+
|
|
617
|
+
def load_asl(self, path, rootdir=None):
|
|
618
|
+
actions = Actions(bspl.adapter.jason.actions)
|
|
619
|
+
with open(path) as source:
|
|
620
|
+
self.bdi = self.environment.build_agent(
|
|
621
|
+
source, actions, agent_cls=bspl.adapter.jason.Agent
|
|
622
|
+
)
|
|
623
|
+
self.bdi.name = self.name or self.bdi.name
|
|
624
|
+
self.bdi.bind(self)
|