mycorrhizal 0.1.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.
- mycorrhizal/__init__.py +3 -0
- mycorrhizal/common/__init__.py +68 -0
- mycorrhizal/common/interface_builder.py +203 -0
- mycorrhizal/common/interfaces.py +412 -0
- mycorrhizal/common/timebase.py +99 -0
- mycorrhizal/common/wrappers.py +532 -0
- mycorrhizal/enoki/__init__.py +0 -0
- mycorrhizal/enoki/core.py +1545 -0
- mycorrhizal/enoki/testing_utils.py +529 -0
- mycorrhizal/enoki/util.py +220 -0
- mycorrhizal/hypha/__init__.py +0 -0
- mycorrhizal/hypha/core/__init__.py +107 -0
- mycorrhizal/hypha/core/builder.py +404 -0
- mycorrhizal/hypha/core/runtime.py +890 -0
- mycorrhizal/hypha/core/specs.py +234 -0
- mycorrhizal/hypha/util.py +38 -0
- mycorrhizal/rhizomorph/README.md +220 -0
- mycorrhizal/rhizomorph/__init__.py +0 -0
- mycorrhizal/rhizomorph/core.py +1729 -0
- mycorrhizal/rhizomorph/util.py +45 -0
- mycorrhizal/spores/__init__.py +124 -0
- mycorrhizal/spores/cache.py +208 -0
- mycorrhizal/spores/core.py +419 -0
- mycorrhizal/spores/dsl/__init__.py +48 -0
- mycorrhizal/spores/dsl/enoki.py +514 -0
- mycorrhizal/spores/dsl/hypha.py +399 -0
- mycorrhizal/spores/dsl/rhizomorph.py +351 -0
- mycorrhizal/spores/encoder/__init__.py +11 -0
- mycorrhizal/spores/encoder/base.py +42 -0
- mycorrhizal/spores/encoder/json.py +159 -0
- mycorrhizal/spores/extraction.py +484 -0
- mycorrhizal/spores/models.py +288 -0
- mycorrhizal/spores/transport/__init__.py +10 -0
- mycorrhizal/spores/transport/base.py +46 -0
- mycorrhizal-0.1.0.dist-info/METADATA +198 -0
- mycorrhizal-0.1.0.dist-info/RECORD +37 -0
- mycorrhizal-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spores Data Extraction
|
|
4
|
+
|
|
5
|
+
Extract attributes and objects from function context for event logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Dict, List, Optional, Union, Callable, get_type_hints, Annotated, get_origin, get_args
|
|
14
|
+
from dataclasses import fields
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
Event, Relationship, EventAttributeValue, Object, ObjectAttributeValue,
|
|
18
|
+
ObjectRef, ObjectScope, EventAttr,
|
|
19
|
+
attribute_value_from_python, object_attribute_from_python,
|
|
20
|
+
generate_object_id
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# Attribute Extraction
|
|
29
|
+
# ============================================================================
|
|
30
|
+
|
|
31
|
+
def extract_attributes_from_params(
|
|
32
|
+
func: Callable,
|
|
33
|
+
args: tuple,
|
|
34
|
+
kwargs: dict,
|
|
35
|
+
timestamp: datetime
|
|
36
|
+
) -> Dict[str, EventAttributeValue]:
|
|
37
|
+
"""
|
|
38
|
+
Extract event attributes from function parameters.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
func: The decorated function
|
|
42
|
+
args: Positional arguments passed to function
|
|
43
|
+
kwargs: Keyword arguments passed to function
|
|
44
|
+
timestamp: Event timestamp for attribute values
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary of attribute name -> EventAttributeValue
|
|
48
|
+
"""
|
|
49
|
+
attributes = {}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Get function signature
|
|
53
|
+
sig = inspect.signature(func)
|
|
54
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
55
|
+
bound_args.apply_defaults()
|
|
56
|
+
|
|
57
|
+
# Extract from each parameter
|
|
58
|
+
for param_name, param_value in bound_args.arguments.items():
|
|
59
|
+
# Skip certain parameter names
|
|
60
|
+
if param_name in ('self', 'cls', 'bb', 'ctx', 'timebase', 'consumed'):
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Extract attribute value
|
|
64
|
+
attr = attribute_value_from_python(param_value, timestamp)
|
|
65
|
+
attributes[param_name] = attr
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to extract attributes from params: {e}")
|
|
69
|
+
|
|
70
|
+
return attributes
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_attributes_from_dict(
|
|
74
|
+
data: Dict[str, Any],
|
|
75
|
+
timestamp: datetime
|
|
76
|
+
) -> Dict[str, EventAttributeValue]:
|
|
77
|
+
"""
|
|
78
|
+
Extract event attributes from a dictionary.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
data: Dictionary with attribute values
|
|
82
|
+
timestamp: Event timestamp for attribute values
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary of attribute name -> EventAttributeValue
|
|
86
|
+
"""
|
|
87
|
+
attributes = {}
|
|
88
|
+
|
|
89
|
+
for key, value in data.items():
|
|
90
|
+
attr = attribute_value_from_python(value, timestamp)
|
|
91
|
+
attributes[key] = attr
|
|
92
|
+
|
|
93
|
+
return attributes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def extract_attributes_from_blackboard(
|
|
97
|
+
bb: Any,
|
|
98
|
+
timestamp: datetime
|
|
99
|
+
) -> Dict[str, EventAttributeValue]:
|
|
100
|
+
"""
|
|
101
|
+
Extract attributes from blackboard fields marked with EventAttr.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
bb: Blackboard object
|
|
105
|
+
timestamp: Event timestamp for attribute values
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary of attribute name -> EventAttributeValue
|
|
109
|
+
"""
|
|
110
|
+
attributes = {}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Get the class type hints - try __annotations__ first for Pydantic models
|
|
114
|
+
cls = type(bb)
|
|
115
|
+
|
|
116
|
+
# Pydantic models preserve Annotated in __annotations__
|
|
117
|
+
if hasattr(cls, '__annotations__'):
|
|
118
|
+
hints = cls.__annotations__
|
|
119
|
+
else:
|
|
120
|
+
hints = get_type_hints(cls)
|
|
121
|
+
|
|
122
|
+
for field_name, field_type in hints.items():
|
|
123
|
+
# Check if this is Annotated
|
|
124
|
+
if get_origin(field_type) is Annotated:
|
|
125
|
+
args = get_args(field_type)
|
|
126
|
+
if len(args) >= 2:
|
|
127
|
+
# args[0] is the actual type, args[1:] are metadata
|
|
128
|
+
for metadata in args[1:]:
|
|
129
|
+
# Check if it's an EventAttr (class or instance)
|
|
130
|
+
if metadata is EventAttr or isinstance(metadata, EventAttr):
|
|
131
|
+
# Get the value from blackboard
|
|
132
|
+
if hasattr(bb, field_name):
|
|
133
|
+
value = getattr(bb, field_name)
|
|
134
|
+
# EventAttr might be a class or instance
|
|
135
|
+
if isinstance(metadata, EventAttr):
|
|
136
|
+
attr_name = metadata.name or field_name
|
|
137
|
+
else:
|
|
138
|
+
attr_name = field_name
|
|
139
|
+
attr = attribute_value_from_python(value, timestamp)
|
|
140
|
+
attributes[attr_name] = attr
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Failed to extract attributes from blackboard: {e}")
|
|
144
|
+
|
|
145
|
+
return attributes
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def evaluate_computed_attributes(
|
|
149
|
+
attr_spec: Dict[str, Any],
|
|
150
|
+
bb: Any,
|
|
151
|
+
timestamp: datetime
|
|
152
|
+
) -> Dict[str, EventAttributeValue]:
|
|
153
|
+
"""
|
|
154
|
+
Evaluate computed/callable attributes.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
attr_spec: Attribute specification (can contain callables)
|
|
158
|
+
bb: Blackboard object for evaluation context
|
|
159
|
+
timestamp: Event timestamp
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary of evaluated attributes
|
|
163
|
+
"""
|
|
164
|
+
attributes = {}
|
|
165
|
+
|
|
166
|
+
for key, value in attr_spec.items():
|
|
167
|
+
if callable(value):
|
|
168
|
+
# Call the function with blackboard
|
|
169
|
+
try:
|
|
170
|
+
result = value(bb)
|
|
171
|
+
attr = attribute_value_from_python(result, timestamp)
|
|
172
|
+
attributes[key] = attr
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Failed to compute attribute {key}: {e}")
|
|
175
|
+
else:
|
|
176
|
+
# Static value
|
|
177
|
+
attr = attribute_value_from_python(value, timestamp)
|
|
178
|
+
attributes[key] = attr
|
|
179
|
+
|
|
180
|
+
return attributes
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ============================================================================
|
|
184
|
+
# Object Extraction
|
|
185
|
+
# ============================================================================
|
|
186
|
+
|
|
187
|
+
def extract_objects_from_blackboard(
|
|
188
|
+
bb: Any,
|
|
189
|
+
object_names: Optional[List[str]] = None
|
|
190
|
+
) -> List[Object]:
|
|
191
|
+
"""
|
|
192
|
+
Extract OCEL objects from blackboard fields marked with ObjectRef.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
bb: Blackboard object
|
|
196
|
+
object_names: Optional list of specific field names to extract.
|
|
197
|
+
If None, extracts all global-scope objects.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of OCEL Object instances
|
|
201
|
+
"""
|
|
202
|
+
objects = []
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Get the class type hints - try __annotations__ first for Pydantic models
|
|
206
|
+
cls = type(bb)
|
|
207
|
+
|
|
208
|
+
# Pydantic models preserve Annotated in __annotations__
|
|
209
|
+
if hasattr(cls, '__annotations__'):
|
|
210
|
+
hints = cls.__annotations__
|
|
211
|
+
else:
|
|
212
|
+
hints = get_type_hints(cls)
|
|
213
|
+
|
|
214
|
+
for field_name, field_type in hints.items():
|
|
215
|
+
# Check if this is Annotated
|
|
216
|
+
if get_origin(field_type) is Annotated:
|
|
217
|
+
args = get_args(field_type)
|
|
218
|
+
if len(args) >= 2:
|
|
219
|
+
# args[0] is the actual type, args[1:] are metadata
|
|
220
|
+
for metadata in args[1:]:
|
|
221
|
+
# Check if it's an ObjectRef (instance of frozen dataclass)
|
|
222
|
+
if isinstance(metadata, ObjectRef):
|
|
223
|
+
# Check if we should extract this object
|
|
224
|
+
if object_names is not None and field_name not in object_names:
|
|
225
|
+
# Only extract specified objects
|
|
226
|
+
if metadata.scope != ObjectScope.GLOBAL:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
if isinstance(metadata.scope, str):
|
|
230
|
+
scope = ObjectScope(metadata.scope)
|
|
231
|
+
else:
|
|
232
|
+
scope = metadata.scope
|
|
233
|
+
|
|
234
|
+
if scope == ObjectScope.EVENT and field_name not in (object_names or []):
|
|
235
|
+
# Event-scope but not requested
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Get the object value
|
|
239
|
+
if hasattr(bb, field_name):
|
|
240
|
+
obj_value = getattr(bb, field_name)
|
|
241
|
+
if obj_value is None:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Convert to OCEL Object
|
|
245
|
+
ocel_obj = convert_to_ocel_object(
|
|
246
|
+
obj_value,
|
|
247
|
+
metadata.qualifier
|
|
248
|
+
)
|
|
249
|
+
if ocel_obj:
|
|
250
|
+
objects.append(ocel_obj)
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(f"Failed to extract objects from blackboard: {e}")
|
|
254
|
+
|
|
255
|
+
return objects
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def convert_to_ocel_object(
|
|
259
|
+
obj: Any,
|
|
260
|
+
qualifier: str = "related"
|
|
261
|
+
) -> Optional[Object]:
|
|
262
|
+
"""
|
|
263
|
+
Convert a Python object to an OCEL Object.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
obj: The Python object to convert
|
|
267
|
+
qualifier: Relationship qualifier for this object
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
OCEL Object instance, or None if conversion fails
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
timestamp = datetime.now()
|
|
274
|
+
|
|
275
|
+
# Generate object ID
|
|
276
|
+
obj_id = generate_object_id(obj)
|
|
277
|
+
|
|
278
|
+
# Get object type
|
|
279
|
+
obj_type = getattr(obj, '_spores_object_type', type(obj).__name__)
|
|
280
|
+
|
|
281
|
+
# Extract attributes
|
|
282
|
+
attributes = {}
|
|
283
|
+
|
|
284
|
+
if hasattr(obj, '__dict__'):
|
|
285
|
+
# Extract from instance dict
|
|
286
|
+
for key, value in obj.__dict__.items():
|
|
287
|
+
if not key.startswith('_'):
|
|
288
|
+
attr = object_attribute_from_python(value, timestamp)
|
|
289
|
+
attr = attr.__class__(
|
|
290
|
+
name=key,
|
|
291
|
+
value=attr.value,
|
|
292
|
+
time=attr.time
|
|
293
|
+
)
|
|
294
|
+
attributes[key] = attr
|
|
295
|
+
|
|
296
|
+
elif hasattr(obj, '__dataclass_fields__'):
|
|
297
|
+
# Extract dataclass fields
|
|
298
|
+
for field_info in fields(obj):
|
|
299
|
+
key = field_info.name
|
|
300
|
+
value = getattr(obj, key)
|
|
301
|
+
attr = object_attribute_from_python(value, timestamp)
|
|
302
|
+
attr = attr.__class__(
|
|
303
|
+
name=key,
|
|
304
|
+
value=attr.value,
|
|
305
|
+
time=attr.time
|
|
306
|
+
)
|
|
307
|
+
attributes[key] = attr
|
|
308
|
+
|
|
309
|
+
# Create OCEL Object
|
|
310
|
+
return Object(
|
|
311
|
+
id=obj_id,
|
|
312
|
+
type=obj_type,
|
|
313
|
+
attributes=attributes,
|
|
314
|
+
relationships={}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.error(f"Failed to convert object to OCEL: {e}")
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def extract_objects_from_spec(
|
|
323
|
+
object_spec: Dict[str, tuple],
|
|
324
|
+
timestamp: datetime
|
|
325
|
+
) -> List[Object]:
|
|
326
|
+
"""
|
|
327
|
+
Extract objects from a manual specification.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
object_spec: Dict of {name: (qualifier, object)} or {name: object}
|
|
331
|
+
timestamp: Timestamp for object attributes
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
List of OCEL Object instances
|
|
335
|
+
"""
|
|
336
|
+
objects = []
|
|
337
|
+
|
|
338
|
+
for name, spec in object_spec.items():
|
|
339
|
+
try:
|
|
340
|
+
if isinstance(spec, tuple) and len(spec) == 2:
|
|
341
|
+
qualifier, obj_value = spec
|
|
342
|
+
else:
|
|
343
|
+
qualifier = "related"
|
|
344
|
+
obj_value = spec
|
|
345
|
+
|
|
346
|
+
if obj_value is None:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
# Convert to OCEL Object
|
|
350
|
+
obj_id = generate_object_id(obj_value)
|
|
351
|
+
obj_type = getattr(obj_value, '_spores_object_type', type(obj_value).__name__)
|
|
352
|
+
|
|
353
|
+
# Extract attributes
|
|
354
|
+
attributes = {}
|
|
355
|
+
if hasattr(obj_value, '__dict__'):
|
|
356
|
+
for key, value in obj_value.__dict__.items():
|
|
357
|
+
if not key.startswith('_'):
|
|
358
|
+
attr = object_attribute_from_python(value, timestamp)
|
|
359
|
+
attr = attr.__class__(
|
|
360
|
+
name=key,
|
|
361
|
+
value=attr.value,
|
|
362
|
+
time=attr.time
|
|
363
|
+
)
|
|
364
|
+
attributes[key] = attr
|
|
365
|
+
|
|
366
|
+
ocel_obj = Object(
|
|
367
|
+
id=obj_id,
|
|
368
|
+
type=obj_type,
|
|
369
|
+
attributes=attributes,
|
|
370
|
+
relationships={}
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
objects.append(ocel_obj)
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
logger.error(f"Failed to extract object {name}: {e}")
|
|
377
|
+
|
|
378
|
+
return objects
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ============================================================================
|
|
382
|
+
# DSL-Specific Extraction
|
|
383
|
+
# ============================================================================
|
|
384
|
+
|
|
385
|
+
def extract_from_hypha_context(
|
|
386
|
+
consumed: list,
|
|
387
|
+
bb: Any,
|
|
388
|
+
timebase: Any,
|
|
389
|
+
timestamp: datetime
|
|
390
|
+
) -> tuple[Dict[str, EventAttributeValue], List[Object]]:
|
|
391
|
+
"""
|
|
392
|
+
Extract attributes and objects from Hypha transition context.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
consumed: List of consumed tokens
|
|
396
|
+
bb: Blackboard
|
|
397
|
+
timebase: Timebase
|
|
398
|
+
timestamp: Event timestamp
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Tuple of (attributes, objects)
|
|
402
|
+
"""
|
|
403
|
+
attributes = {}
|
|
404
|
+
objects = []
|
|
405
|
+
|
|
406
|
+
# Add token count
|
|
407
|
+
attributes["token_count"] = EventAttributeValue(
|
|
408
|
+
name="token_count",
|
|
409
|
+
value=str(len(consumed)),
|
|
410
|
+
time=timestamp
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Extract from blackboard
|
|
414
|
+
bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
|
|
415
|
+
attributes.update(bb_attrs)
|
|
416
|
+
|
|
417
|
+
bb_objects = extract_objects_from_blackboard(bb)
|
|
418
|
+
objects.extend(bb_objects)
|
|
419
|
+
|
|
420
|
+
# Extract objects from tokens
|
|
421
|
+
for token in consumed:
|
|
422
|
+
obj = convert_to_ocel_object(token, qualifier="input")
|
|
423
|
+
if obj:
|
|
424
|
+
objects.append(obj)
|
|
425
|
+
|
|
426
|
+
return attributes, objects
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def extract_from_rhizomorph_context(
|
|
430
|
+
bb: Any,
|
|
431
|
+
timestamp: datetime
|
|
432
|
+
) -> tuple[Dict[str, EventAttributeValue], List[Object]]:
|
|
433
|
+
"""
|
|
434
|
+
Extract attributes and objects from Rhizomorph action context.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
bb: Blackboard
|
|
438
|
+
timestamp: Event timestamp
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Tuple of (attributes, objects)
|
|
442
|
+
"""
|
|
443
|
+
# Extract from blackboard
|
|
444
|
+
attributes = extract_attributes_from_blackboard(bb, timestamp)
|
|
445
|
+
objects = extract_objects_from_blackboard(bb)
|
|
446
|
+
|
|
447
|
+
return attributes, objects
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def extract_from_enoki_context(
|
|
451
|
+
ctx: Any,
|
|
452
|
+
timestamp: datetime
|
|
453
|
+
) -> tuple[Dict[str, EventAttributeValue], List[Object]]:
|
|
454
|
+
"""
|
|
455
|
+
Extract attributes and objects from Enoki state context.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
ctx: SharedContext
|
|
459
|
+
timestamp: Event timestamp
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Tuple of (attributes, objects)
|
|
463
|
+
"""
|
|
464
|
+
attributes = {}
|
|
465
|
+
objects = []
|
|
466
|
+
|
|
467
|
+
# Add state name if available
|
|
468
|
+
if hasattr(ctx, 'current_state') and ctx.current_state:
|
|
469
|
+
state_name = getattr(ctx.current_state, 'name', str(ctx.current_state))
|
|
470
|
+
attributes["state_name"] = EventAttributeValue(
|
|
471
|
+
name="state_name",
|
|
472
|
+
value=state_name,
|
|
473
|
+
time=timestamp
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Extract from common (blackboard)
|
|
477
|
+
if hasattr(ctx, 'common'):
|
|
478
|
+
bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
|
|
479
|
+
attributes.update(bb_attrs)
|
|
480
|
+
|
|
481
|
+
bb_objects = extract_objects_from_blackboard(ctx.common)
|
|
482
|
+
objects.extend(bb_objects)
|
|
483
|
+
|
|
484
|
+
return attributes, objects
|