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.
Files changed (46) hide show
  1. bspl/__init__.py +2 -0
  2. bspl/adapter/__init__.py +3 -0
  3. bspl/adapter/core.py +624 -0
  4. bspl/adapter/emitter.py +253 -0
  5. bspl/adapter/event.py +28 -0
  6. bspl/adapter/jason.py +190 -0
  7. bspl/adapter/message.py +248 -0
  8. bspl/adapter/policies.py +264 -0
  9. bspl/adapter/policy.gr +32 -0
  10. bspl/adapter/receiver.py +134 -0
  11. bspl/adapter/scheduler.py +94 -0
  12. bspl/adapter/schema.py +41 -0
  13. bspl/adapter/statistics.py +79 -0
  14. bspl/adapter/store.py +316 -0
  15. bspl/generators/__init__.py +12 -0
  16. bspl/generators/asl.py +273 -0
  17. bspl/generators/mambo.py +75 -0
  18. bspl/generators/node_red.py +268 -0
  19. bspl/langshaw.py +664 -0
  20. bspl/main.py +132 -0
  21. bspl/parsers/bspl/__init__.py +128 -0
  22. bspl/parsers/bspl/bspl.gr +32 -0
  23. bspl/parsers/bspl/bspl_parser.py +322 -0
  24. bspl/parsers/bspl/build.py +24 -0
  25. bspl/parsers/langshaw/__init__.py +39 -0
  26. bspl/parsers/langshaw/build.py +24 -0
  27. bspl/parsers/langshaw/langshaw.gr +46 -0
  28. bspl/parsers/precedence/__init__.py +18 -0
  29. bspl/parsers/precedence/precedence.gr +33 -0
  30. bspl/protocol.py +733 -0
  31. bspl/utils.py +45 -0
  32. bspl/validation.py +107 -0
  33. bspl/verification/__init__.py +195 -0
  34. bspl/verification/logic.py +98 -0
  35. bspl/verification/lpaths.py +569 -0
  36. bspl/verification/mambo.py +415 -0
  37. bspl/verification/paths.py +882 -0
  38. bspl/verification/precedence.py +411 -0
  39. bspl/verification/refinement.py +153 -0
  40. bspl/verification/sat.py +485 -0
  41. bspl-1.0.0.dist-info/METADATA +372 -0
  42. bspl-1.0.0.dist-info/RECORD +46 -0
  43. bspl-1.0.0.dist-info/WHEEL +5 -0
  44. bspl-1.0.0.dist-info/entry_points.txt +2 -0
  45. bspl-1.0.0.dist-info/licenses/LICENSE +21 -0
  46. bspl-1.0.0.dist-info/top_level.txt +1 -0
bspl/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.0"
2
+ from .parsers.bspl import load_file
@@ -0,0 +1,3 @@
1
+ from .scheduler import Scheduler
2
+ from .core import Adapter
3
+ from .policies import Remind, Forward, Send
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)