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,514 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spores Adapter for Enoki (State Machines)
|
|
4
|
+
|
|
5
|
+
Provides logging integration for Enoki state machines.
|
|
6
|
+
Extracts context information and automatically creates event/object logs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import functools
|
|
13
|
+
import inspect
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
from ...spores import (
|
|
19
|
+
get_config,
|
|
20
|
+
Event,
|
|
21
|
+
LogRecord,
|
|
22
|
+
Relationship,
|
|
23
|
+
EventAttributeValue,
|
|
24
|
+
generate_event_id,
|
|
25
|
+
)
|
|
26
|
+
from ...spores.extraction import (
|
|
27
|
+
extract_attributes_from_blackboard,
|
|
28
|
+
extract_objects_from_blackboard,
|
|
29
|
+
)
|
|
30
|
+
from ...spores.core import _send_log_record, get_object_cache
|
|
31
|
+
from ...enoki.core import SharedContext, TransitionType
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EnokiAdapter:
|
|
38
|
+
"""
|
|
39
|
+
Adapter for Enoki state machine logging.
|
|
40
|
+
|
|
41
|
+
Provides decorators and helpers for logging:
|
|
42
|
+
- State entry/exit/timeout events
|
|
43
|
+
- State execution events
|
|
44
|
+
- Transition events
|
|
45
|
+
- Message/context object lifecycle tracking
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
```python
|
|
49
|
+
from mycorrhizal.spores.dsl import EnokiAdapter
|
|
50
|
+
|
|
51
|
+
adapter = EnokiAdapter()
|
|
52
|
+
|
|
53
|
+
@enoki.state()
|
|
54
|
+
def MyState():
|
|
55
|
+
@enoki.on_state
|
|
56
|
+
@adapter.log_state(event_type="state_execute")
|
|
57
|
+
async def on_state(ctx: SharedContext):
|
|
58
|
+
# Event automatically logged with:
|
|
59
|
+
# - state_name attribute
|
|
60
|
+
# - attributes from ctx.common (with EventAttr annotations)
|
|
61
|
+
# - relationships to objects (with ObjectRef annotations)
|
|
62
|
+
return Events.DONE
|
|
63
|
+
|
|
64
|
+
@enoki.on_enter
|
|
65
|
+
@adapter.log_state_lifecycle(event_type="state_enter")
|
|
66
|
+
async def on_enter(ctx: SharedContext):
|
|
67
|
+
pass
|
|
68
|
+
```
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self):
|
|
72
|
+
"""Initialize the Enoki adapter."""
|
|
73
|
+
self._enabled = True
|
|
74
|
+
|
|
75
|
+
def enable(self):
|
|
76
|
+
"""Enable logging for this adapter."""
|
|
77
|
+
self._enabled = True
|
|
78
|
+
|
|
79
|
+
def disable(self):
|
|
80
|
+
"""Disable logging for this adapter."""
|
|
81
|
+
self._enabled = False
|
|
82
|
+
|
|
83
|
+
def log_state(
|
|
84
|
+
self,
|
|
85
|
+
event_type: str,
|
|
86
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
87
|
+
log_state_name: bool = True,
|
|
88
|
+
log_transition: bool = True,
|
|
89
|
+
) -> Callable:
|
|
90
|
+
"""
|
|
91
|
+
Decorator to log Enoki state execution.
|
|
92
|
+
|
|
93
|
+
Automatically captures:
|
|
94
|
+
- State name
|
|
95
|
+
- Transition result (if log_transition=True)
|
|
96
|
+
- Attributes from ctx.common (with EventAttr annotations)
|
|
97
|
+
- Objects from ctx.common (with ObjectRef annotations)
|
|
98
|
+
- Message information (if present in ctx)
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
event_type: Type of event to log
|
|
102
|
+
attributes: Static attributes to include
|
|
103
|
+
log_state_name: Whether to include state name in attributes
|
|
104
|
+
log_transition: Whether to include transition result
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Decorator function
|
|
108
|
+
"""
|
|
109
|
+
def decorator(func: Callable) -> Callable:
|
|
110
|
+
@functools.wraps(func)
|
|
111
|
+
async def async_wrapper(ctx: SharedContext):
|
|
112
|
+
# Call original state handler
|
|
113
|
+
result = await func(ctx)
|
|
114
|
+
|
|
115
|
+
# Log event
|
|
116
|
+
await _log_state_event(
|
|
117
|
+
func, ctx, event_type, attributes, log_state_name, log_transition, result
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
@functools.wraps(func)
|
|
123
|
+
def sync_wrapper(ctx: SharedContext):
|
|
124
|
+
# Call original state handler
|
|
125
|
+
result = func(ctx)
|
|
126
|
+
|
|
127
|
+
# Schedule logging
|
|
128
|
+
asyncio.create_task(_log_state_event(
|
|
129
|
+
func, ctx, event_type, attributes, log_state_name, log_transition, result
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
if asyncio.iscoroutinefunction(func):
|
|
135
|
+
return async_wrapper # type: ignore
|
|
136
|
+
else:
|
|
137
|
+
return sync_wrapper # type: ignore
|
|
138
|
+
|
|
139
|
+
return decorator
|
|
140
|
+
|
|
141
|
+
def log_state_lifecycle(
|
|
142
|
+
self,
|
|
143
|
+
event_type: str,
|
|
144
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
145
|
+
) -> Callable:
|
|
146
|
+
"""
|
|
147
|
+
Decorator to log state lifecycle events (on_enter, on_leave, on_timeout).
|
|
148
|
+
|
|
149
|
+
Use this for logging lifecycle hooks rather than main state execution.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
event_type: Type of event to log (e.g., "state_enter", "state_leave")
|
|
153
|
+
attributes: Static attributes to include
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Decorator function
|
|
157
|
+
"""
|
|
158
|
+
def decorator(func: Callable) -> Callable:
|
|
159
|
+
@functools.wraps(func)
|
|
160
|
+
async def async_wrapper(ctx: SharedContext):
|
|
161
|
+
# Log before execution
|
|
162
|
+
config = get_config()
|
|
163
|
+
if config.enabled:
|
|
164
|
+
await _log_lifecycle_event(func, ctx, event_type, attributes, "enter")
|
|
165
|
+
|
|
166
|
+
# Call original lifecycle handler
|
|
167
|
+
result = await func(ctx)
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
@functools.wraps(func)
|
|
172
|
+
def sync_wrapper(ctx: SharedContext):
|
|
173
|
+
# Schedule logging
|
|
174
|
+
config = get_config()
|
|
175
|
+
if config.enabled:
|
|
176
|
+
asyncio.create_task(_log_lifecycle_event(
|
|
177
|
+
func, ctx, event_type, attributes, "enter"
|
|
178
|
+
))
|
|
179
|
+
|
|
180
|
+
return func(ctx)
|
|
181
|
+
|
|
182
|
+
if asyncio.iscoroutinefunction(func):
|
|
183
|
+
return async_wrapper # type: ignore
|
|
184
|
+
else:
|
|
185
|
+
return sync_wrapper # type: ignore
|
|
186
|
+
|
|
187
|
+
return decorator
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def _log_state_event(
|
|
191
|
+
func: Callable,
|
|
192
|
+
ctx: SharedContext,
|
|
193
|
+
event_type: str,
|
|
194
|
+
attributes: Optional[Dict[str, Any]],
|
|
195
|
+
log_state_name: bool,
|
|
196
|
+
log_transition: bool,
|
|
197
|
+
result: Any,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Log a state execution event."""
|
|
200
|
+
config = get_config()
|
|
201
|
+
if not config.enabled:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
timestamp = datetime.now()
|
|
206
|
+
|
|
207
|
+
# Build event attributes
|
|
208
|
+
event_attrs = {}
|
|
209
|
+
|
|
210
|
+
# Add state name if requested
|
|
211
|
+
if log_state_name:
|
|
212
|
+
# Try to get state name from function or context
|
|
213
|
+
state_name = func.__name__
|
|
214
|
+
if hasattr(ctx, 'current_state') and ctx.current_state:
|
|
215
|
+
state_name = getattr(ctx.current_state, 'name', state_name)
|
|
216
|
+
|
|
217
|
+
event_attrs["state_name"] = EventAttributeValue(
|
|
218
|
+
name="state_name",
|
|
219
|
+
value=state_name,
|
|
220
|
+
time=timestamp
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Add transition result if requested
|
|
224
|
+
if log_transition and isinstance(result, TransitionType):
|
|
225
|
+
event_attrs["transition"] = EventAttributeValue(
|
|
226
|
+
name="transition",
|
|
227
|
+
value=result.name,
|
|
228
|
+
time=timestamp
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Extract from ctx.common (blackboard)
|
|
232
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
233
|
+
bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
|
|
234
|
+
event_attrs.update(bb_attrs)
|
|
235
|
+
|
|
236
|
+
# Add message if present
|
|
237
|
+
if hasattr(ctx, 'msg') and ctx.msg is not None:
|
|
238
|
+
event_attrs["message_type"] = EventAttributeValue(
|
|
239
|
+
name="message_type",
|
|
240
|
+
value=type(ctx.msg).__name__,
|
|
241
|
+
time=timestamp
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Extract objects from ctx.common
|
|
245
|
+
relationships = {}
|
|
246
|
+
event_objects = []
|
|
247
|
+
|
|
248
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
249
|
+
bb_objects = extract_objects_from_blackboard(ctx.common)
|
|
250
|
+
event_objects.extend(bb_objects)
|
|
251
|
+
|
|
252
|
+
for obj in bb_objects:
|
|
253
|
+
relationships[obj.id] = Relationship(
|
|
254
|
+
object_id=obj.id,
|
|
255
|
+
qualifier="context"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Build event
|
|
259
|
+
event = Event(
|
|
260
|
+
id=generate_event_id(),
|
|
261
|
+
type=event_type,
|
|
262
|
+
time=timestamp,
|
|
263
|
+
attributes=event_attrs,
|
|
264
|
+
relationships=relationships
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Send event
|
|
268
|
+
await _send_log_record(LogRecord(event=event))
|
|
269
|
+
|
|
270
|
+
# Send objects to cache
|
|
271
|
+
cache = get_object_cache()
|
|
272
|
+
for obj in event_objects:
|
|
273
|
+
cache.contains_or_add(obj.id, obj)
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error(f"Failed to log state event: {e}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def _log_lifecycle_event(
|
|
280
|
+
func: Callable,
|
|
281
|
+
ctx: SharedContext,
|
|
282
|
+
event_type: str,
|
|
283
|
+
attributes: Optional[Dict[str, Any]],
|
|
284
|
+
phase: str,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Log a state lifecycle event."""
|
|
287
|
+
config = get_config()
|
|
288
|
+
if not config.enabled:
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
timestamp = datetime.now()
|
|
293
|
+
|
|
294
|
+
# Build event attributes
|
|
295
|
+
event_attrs = {
|
|
296
|
+
"lifecycle_method": EventAttributeValue(
|
|
297
|
+
name="lifecycle_method",
|
|
298
|
+
value=func.__name__,
|
|
299
|
+
time=timestamp
|
|
300
|
+
),
|
|
301
|
+
"phase": EventAttributeValue(
|
|
302
|
+
name="phase",
|
|
303
|
+
value=phase,
|
|
304
|
+
time=timestamp
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Add state name
|
|
309
|
+
state_name = func.__name__
|
|
310
|
+
if hasattr(ctx, 'current_state') and ctx.current_state:
|
|
311
|
+
state_name = getattr(ctx.current_state, 'name', state_name)
|
|
312
|
+
|
|
313
|
+
event_attrs["state_name"] = EventAttributeValue(
|
|
314
|
+
name="state_name",
|
|
315
|
+
value=state_name,
|
|
316
|
+
time=timestamp
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Add static attributes
|
|
320
|
+
if attributes:
|
|
321
|
+
for key, value in attributes.items():
|
|
322
|
+
if callable(value):
|
|
323
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
324
|
+
result = value(ctx.common)
|
|
325
|
+
event_attrs[key] = EventAttributeValue(
|
|
326
|
+
name=key,
|
|
327
|
+
value=str(result),
|
|
328
|
+
time=timestamp
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
event_attrs[key] = EventAttributeValue(
|
|
332
|
+
name=key,
|
|
333
|
+
value=str(value),
|
|
334
|
+
time=timestamp
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Extract from ctx.common
|
|
338
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
339
|
+
bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
|
|
340
|
+
event_attrs.update(bb_attrs)
|
|
341
|
+
|
|
342
|
+
# Extract objects from ctx.common
|
|
343
|
+
relationships = {}
|
|
344
|
+
event_objects = []
|
|
345
|
+
|
|
346
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
347
|
+
bb_objects = extract_objects_from_blackboard(ctx.common)
|
|
348
|
+
event_objects.extend(bb_objects)
|
|
349
|
+
|
|
350
|
+
for obj in bb_objects:
|
|
351
|
+
relationships[obj.id] = Relationship(
|
|
352
|
+
object_id=obj.id,
|
|
353
|
+
qualifier="context"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Build event
|
|
357
|
+
event = Event(
|
|
358
|
+
id=generate_event_id(),
|
|
359
|
+
type=event_type,
|
|
360
|
+
time=timestamp,
|
|
361
|
+
attributes=event_attrs,
|
|
362
|
+
relationships=relationships
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Send event
|
|
366
|
+
await _send_log_record(LogRecord(event=event))
|
|
367
|
+
|
|
368
|
+
# Send objects to cache
|
|
369
|
+
cache = get_object_cache()
|
|
370
|
+
for obj in event_objects:
|
|
371
|
+
cache.contains_or_add(obj.id, obj)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"Failed to log lifecycle event: {e}")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def log_message(
|
|
378
|
+
event_type: str = "message_received",
|
|
379
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
380
|
+
) -> Callable:
|
|
381
|
+
"""
|
|
382
|
+
Decorator to log message handling events.
|
|
383
|
+
|
|
384
|
+
Use this for logging when messages are processed by states.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
event_type: Type of event to log
|
|
388
|
+
attributes: Static attributes to include
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Decorator function
|
|
392
|
+
"""
|
|
393
|
+
def decorator(func: Callable) -> Callable:
|
|
394
|
+
@functools.wraps(func)
|
|
395
|
+
async def async_wrapper(ctx: SharedContext):
|
|
396
|
+
# Log message
|
|
397
|
+
config = get_config()
|
|
398
|
+
if config.enabled and hasattr(ctx, 'msg') and ctx.msg is not None:
|
|
399
|
+
await _log_message_event(func, ctx, event_type, attributes)
|
|
400
|
+
|
|
401
|
+
return await func(ctx)
|
|
402
|
+
|
|
403
|
+
@functools.wraps(func)
|
|
404
|
+
def sync_wrapper(ctx: SharedContext):
|
|
405
|
+
# Schedule logging
|
|
406
|
+
config = get_config()
|
|
407
|
+
if config.enabled and hasattr(ctx, 'msg') and ctx.msg is not None:
|
|
408
|
+
asyncio.create_task(_log_message_event(
|
|
409
|
+
func, ctx, event_type, attributes
|
|
410
|
+
))
|
|
411
|
+
|
|
412
|
+
return func(ctx)
|
|
413
|
+
|
|
414
|
+
if asyncio.iscoroutinefunction(func):
|
|
415
|
+
return async_wrapper # type: ignore
|
|
416
|
+
else:
|
|
417
|
+
return sync_wrapper # type: ignore
|
|
418
|
+
|
|
419
|
+
return decorator
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
async def _log_message_event(
|
|
423
|
+
func: Callable,
|
|
424
|
+
ctx: SharedContext,
|
|
425
|
+
event_type: str,
|
|
426
|
+
attributes: Optional[Dict[str, Any]],
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Log a message event."""
|
|
429
|
+
config = get_config()
|
|
430
|
+
if not config.enabled:
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
timestamp = datetime.now()
|
|
435
|
+
|
|
436
|
+
# Build event attributes
|
|
437
|
+
event_attrs = {
|
|
438
|
+
"message_type": EventAttributeValue(
|
|
439
|
+
name="message_type",
|
|
440
|
+
value=type(ctx.msg).__name__,
|
|
441
|
+
time=timestamp
|
|
442
|
+
),
|
|
443
|
+
"handler": EventAttributeValue(
|
|
444
|
+
name="handler",
|
|
445
|
+
value=func.__name__,
|
|
446
|
+
time=timestamp
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# Add state name
|
|
451
|
+
if hasattr(ctx, 'current_state') and ctx.current_state:
|
|
452
|
+
state_name = getattr(ctx.current_state, 'name', 'unknown')
|
|
453
|
+
event_attrs["state_name"] = EventAttributeValue(
|
|
454
|
+
name="state_name",
|
|
455
|
+
value=state_name,
|
|
456
|
+
time=timestamp
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Add static attributes
|
|
460
|
+
if attributes:
|
|
461
|
+
for key, value in attributes.items():
|
|
462
|
+
if callable(value):
|
|
463
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
464
|
+
result = value(ctx.common)
|
|
465
|
+
event_attrs[key] = EventAttributeValue(
|
|
466
|
+
name=key,
|
|
467
|
+
value=str(result),
|
|
468
|
+
time=timestamp
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
event_attrs[key] = EventAttributeValue(
|
|
472
|
+
name=key,
|
|
473
|
+
value=str(value),
|
|
474
|
+
time=timestamp
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Extract from ctx.common
|
|
478
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
479
|
+
bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
|
|
480
|
+
event_attrs.update(bb_attrs)
|
|
481
|
+
|
|
482
|
+
# Extract objects from ctx.common
|
|
483
|
+
relationships = {}
|
|
484
|
+
event_objects = []
|
|
485
|
+
|
|
486
|
+
if hasattr(ctx, 'common') and ctx.common:
|
|
487
|
+
bb_objects = extract_objects_from_blackboard(ctx.common)
|
|
488
|
+
event_objects.extend(bb_objects)
|
|
489
|
+
|
|
490
|
+
for obj in bb_objects:
|
|
491
|
+
relationships[obj.id] = Relationship(
|
|
492
|
+
object_id=obj.id,
|
|
493
|
+
qualifier="context"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Build event
|
|
497
|
+
event = Event(
|
|
498
|
+
id=generate_event_id(),
|
|
499
|
+
type=event_type,
|
|
500
|
+
time=timestamp,
|
|
501
|
+
attributes=event_attrs,
|
|
502
|
+
relationships=relationships
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Send event
|
|
506
|
+
await _send_log_record(LogRecord(event=event))
|
|
507
|
+
|
|
508
|
+
# Send objects to cache
|
|
509
|
+
cache = get_object_cache()
|
|
510
|
+
for obj in event_objects:
|
|
511
|
+
cache.contains_or_add(obj.id, obj)
|
|
512
|
+
|
|
513
|
+
except Exception as e:
|
|
514
|
+
logger.error(f"Failed to log message event: {e}")
|