mycorrhizal 0.1.2__py3-none-any.whl → 0.2.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/_version.py +1 -1
- mycorrhizal/common/__init__.py +15 -3
- mycorrhizal/common/cache.py +114 -0
- mycorrhizal/common/compilation.py +263 -0
- mycorrhizal/common/interface_detection.py +159 -0
- mycorrhizal/common/interfaces.py +3 -50
- mycorrhizal/common/mermaid.py +124 -0
- mycorrhizal/common/wrappers.py +1 -1
- mycorrhizal/hypha/core/builder.py +11 -1
- mycorrhizal/hypha/core/runtime.py +242 -107
- mycorrhizal/mycelium/__init__.py +174 -0
- mycorrhizal/mycelium/core.py +619 -0
- mycorrhizal/mycelium/exceptions.py +30 -0
- mycorrhizal/mycelium/hypha_bridge.py +1143 -0
- mycorrhizal/mycelium/instance.py +440 -0
- mycorrhizal/mycelium/pn_context.py +276 -0
- mycorrhizal/mycelium/runner.py +165 -0
- mycorrhizal/mycelium/spores_integration.py +655 -0
- mycorrhizal/mycelium/tree_builder.py +102 -0
- mycorrhizal/mycelium/tree_spec.py +197 -0
- mycorrhizal/rhizomorph/README.md +82 -33
- mycorrhizal/rhizomorph/core.py +287 -119
- mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
- mycorrhizal/{enoki → septum}/core.py +326 -100
- mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
- mycorrhizal/{enoki → septum}/util.py +44 -21
- mycorrhizal/spores/__init__.py +3 -3
- mycorrhizal/spores/core.py +149 -28
- mycorrhizal/spores/dsl/__init__.py +8 -8
- mycorrhizal/spores/dsl/hypha.py +3 -15
- mycorrhizal/spores/dsl/rhizomorph.py +3 -11
- mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
- mycorrhizal/spores/encoder/json.py +21 -12
- mycorrhizal/spores/extraction.py +14 -11
- mycorrhizal/spores/models.py +53 -20
- mycorrhizal-0.2.0.dist-info/METADATA +335 -0
- mycorrhizal-0.2.0.dist-info/RECORD +54 -0
- mycorrhizal-0.1.2.dist-info/METADATA +0 -198
- mycorrhizal-0.1.2.dist-info/RECORD +0 -39
- /mycorrhizal/{enoki → septum}/__init__.py +0 -0
- {mycorrhizal-0.1.2.dist-info → mycorrhizal-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spores Integration for Mycelium
|
|
4
|
+
|
|
5
|
+
This module provides spores logging integration for Mycelium trees.
|
|
6
|
+
It wraps the existing spores DSL adapters (HyphaAdapter, RhizomorphAdapter,
|
|
7
|
+
SeptumAdapter) to provide logging for Mycelium-specific patterns.
|
|
8
|
+
|
|
9
|
+
Key principle: Mycelium wraps base modules. Base modules do NOT know about Mycelium.
|
|
10
|
+
|
|
11
|
+
This integration layer:
|
|
12
|
+
- Uses existing spores adapters for Hypha, Rhizomorph, and Septum
|
|
13
|
+
- Provides Mycelium-specific logging via wrapper classes
|
|
14
|
+
- Does NOT modify the spores module
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import functools
|
|
21
|
+
import logging
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
24
|
+
|
|
25
|
+
from ..spores import (
|
|
26
|
+
get_config,
|
|
27
|
+
Event,
|
|
28
|
+
LogRecord,
|
|
29
|
+
Relationship,
|
|
30
|
+
EventAttributeValue,
|
|
31
|
+
ObjectAttributeValue,
|
|
32
|
+
generate_event_id,
|
|
33
|
+
)
|
|
34
|
+
from ..spores.extraction import (
|
|
35
|
+
extract_attributes_from_blackboard,
|
|
36
|
+
extract_objects_from_blackboard,
|
|
37
|
+
)
|
|
38
|
+
from ..spores.core import _send_log_record, get_object_cache
|
|
39
|
+
from ..spores.dsl import RhizomorphAdapter
|
|
40
|
+
from ..rhizomorph.core import Status
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ======================================================================================
|
|
47
|
+
# Mycelium-Specific Spores Integration
|
|
48
|
+
# ======================================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TreeSporesAdapter:
|
|
52
|
+
"""
|
|
53
|
+
Adapter for logging Mycelium tree execution with spores.
|
|
54
|
+
|
|
55
|
+
This wraps the existing spores adapters and provides Mycelium-specific
|
|
56
|
+
logging functionality without modifying the spores module.
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
```python
|
|
60
|
+
from mycorrhizal.mycelium.spores_integration import TreeSporesAdapter
|
|
61
|
+
|
|
62
|
+
adapter = TreeSporesAdapter(tree_name="MyTree")
|
|
63
|
+
|
|
64
|
+
@tree
|
|
65
|
+
def MyTree():
|
|
66
|
+
@Action(fsm=MyFSM)
|
|
67
|
+
@adapter.log_action(event_type="control_robot")
|
|
68
|
+
async def control(bb, tb, fsm_runner):
|
|
69
|
+
return Status.SUCCESS
|
|
70
|
+
```
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, tree_name: Optional[str] = None):
|
|
74
|
+
"""
|
|
75
|
+
Initialize the tree spores adapter.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
tree_name: Optional tree name for logging
|
|
79
|
+
"""
|
|
80
|
+
self._enabled = True
|
|
81
|
+
self._tree_name = tree_name
|
|
82
|
+
|
|
83
|
+
# Create base adapters for reuse
|
|
84
|
+
self._bt_adapter = RhizomorphAdapter()
|
|
85
|
+
|
|
86
|
+
def enable(self):
|
|
87
|
+
"""Enable logging for this adapter."""
|
|
88
|
+
self._enabled = True
|
|
89
|
+
self._bt_adapter.enable()
|
|
90
|
+
|
|
91
|
+
def disable(self):
|
|
92
|
+
"""Disable logging for this adapter."""
|
|
93
|
+
self._enabled = False
|
|
94
|
+
self._bt_adapter.disable()
|
|
95
|
+
|
|
96
|
+
def log_action(
|
|
97
|
+
self,
|
|
98
|
+
event_type: str,
|
|
99
|
+
attributes: Optional[Union[Dict[str, Any], List[str]]] = None,
|
|
100
|
+
log_fsm_state: bool = True,
|
|
101
|
+
log_status: bool = True,
|
|
102
|
+
log_blackboard: bool = True,
|
|
103
|
+
) -> Callable:
|
|
104
|
+
"""
|
|
105
|
+
Decorator to log Mycelium action execution.
|
|
106
|
+
|
|
107
|
+
Automatically captures:
|
|
108
|
+
- FSM state (if action has FSM integration)
|
|
109
|
+
- Action execution status
|
|
110
|
+
- Attributes from blackboard (with EventAttr annotations)
|
|
111
|
+
- Objects from blackboard (with ObjectRef annotations)
|
|
112
|
+
- Action name
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
event_type: Type of event to log
|
|
116
|
+
attributes: Static attributes or param names to extract
|
|
117
|
+
log_fsm_state: Whether to include FSM state in event attributes
|
|
118
|
+
log_status: Whether to include status in event attributes
|
|
119
|
+
log_blackboard: Whether to extract attributes from blackboard
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Decorator function
|
|
123
|
+
"""
|
|
124
|
+
def decorator(func: Callable) -> Callable:
|
|
125
|
+
@functools.wraps(func)
|
|
126
|
+
async def async_wrapper(bb: Any, tb: Any, fsm_runner: Any = None):
|
|
127
|
+
# Call original action function
|
|
128
|
+
result = await func(bb, tb, fsm_runner)
|
|
129
|
+
|
|
130
|
+
# Log event with full context
|
|
131
|
+
await _log_action_event(
|
|
132
|
+
func, bb, tb, fsm_runner, event_type,
|
|
133
|
+
attributes, log_fsm_state, log_status, log_blackboard, result,
|
|
134
|
+
self._tree_name,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
return async_wrapper # type: ignore
|
|
140
|
+
|
|
141
|
+
return decorator
|
|
142
|
+
|
|
143
|
+
def log_tree_tick(
|
|
144
|
+
self,
|
|
145
|
+
event_type: str = "tree_tick",
|
|
146
|
+
log_blackboard: bool = True,
|
|
147
|
+
log_fsm_states: bool = True,
|
|
148
|
+
) -> Callable:
|
|
149
|
+
"""
|
|
150
|
+
Decorator to log tree tick events.
|
|
151
|
+
|
|
152
|
+
Use this to wrap the TreeRunner.tick() method.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
event_type: Type of event to log
|
|
156
|
+
log_blackboard: Whether to snapshot blackboard state
|
|
157
|
+
log_fsm_states: Whether to include all FSM states
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Decorator function
|
|
161
|
+
"""
|
|
162
|
+
def decorator(func: Callable) -> Callable:
|
|
163
|
+
@functools.wraps(func)
|
|
164
|
+
async def async_wrapper(self, *args, **kwargs):
|
|
165
|
+
# Call original tick method
|
|
166
|
+
result = await func(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
# Log event
|
|
169
|
+
await _log_tree_tick_event(
|
|
170
|
+
self, event_type, log_blackboard, log_fsm_states, result,
|
|
171
|
+
self._tree_name,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
return async_wrapper # type: ignore
|
|
177
|
+
|
|
178
|
+
return decorator
|
|
179
|
+
|
|
180
|
+
def log_state_snapshot(
|
|
181
|
+
self,
|
|
182
|
+
snapshot_name: str,
|
|
183
|
+
include_blackboard: bool = True,
|
|
184
|
+
include_fsms: bool = True,
|
|
185
|
+
) -> Callable:
|
|
186
|
+
"""
|
|
187
|
+
Decorator to log state snapshots.
|
|
188
|
+
|
|
189
|
+
Creates object-type log entries capturing the complete
|
|
190
|
+
state of the tree at a point in time.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
snapshot_name: Name for this snapshot type
|
|
194
|
+
include_blackboard: Whether to include blackboard state
|
|
195
|
+
include_fsms: Whether to include FSM states
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Decorator function
|
|
199
|
+
"""
|
|
200
|
+
def decorator(func: Callable) -> Callable:
|
|
201
|
+
@functools.wraps(func)
|
|
202
|
+
async def async_wrapper(*args, **kwargs):
|
|
203
|
+
# Execute function
|
|
204
|
+
result = await func(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
# Log snapshot - find tree_instance from args
|
|
207
|
+
tree_instance = None
|
|
208
|
+
for arg in args:
|
|
209
|
+
if hasattr(arg, 'spec') and hasattr(arg, 'bb'):
|
|
210
|
+
tree_instance = arg
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if tree_instance:
|
|
214
|
+
await _log_state_snapshot(
|
|
215
|
+
tree_instance, snapshot_name,
|
|
216
|
+
include_blackboard, include_fsms,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
return async_wrapper # type: ignore
|
|
222
|
+
|
|
223
|
+
return decorator
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ======================================================================================
|
|
227
|
+
# Internal Logging Functions
|
|
228
|
+
# ======================================================================================
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def _log_action_event(
|
|
232
|
+
func: Callable,
|
|
233
|
+
bb: Any,
|
|
234
|
+
tb: Any,
|
|
235
|
+
fsm_runner: Any,
|
|
236
|
+
event_type: str,
|
|
237
|
+
attributes: Optional[Union[Dict[str, Any], List[str]]],
|
|
238
|
+
log_fsm_state: bool,
|
|
239
|
+
log_status: bool,
|
|
240
|
+
log_blackboard: bool,
|
|
241
|
+
result: Any,
|
|
242
|
+
tree_name: Optional[str],
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Log an action execution event."""
|
|
245
|
+
config = get_config()
|
|
246
|
+
if not config.enabled:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
timestamp = datetime.now()
|
|
251
|
+
|
|
252
|
+
# Build event attributes
|
|
253
|
+
event_attrs = {}
|
|
254
|
+
|
|
255
|
+
# Add tree name if available
|
|
256
|
+
if tree_name:
|
|
257
|
+
event_attrs["tree_name"] = EventAttributeValue(
|
|
258
|
+
name="tree_name",
|
|
259
|
+
value=tree_name,
|
|
260
|
+
type="string"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Add action name
|
|
264
|
+
event_attrs["action_name"] = EventAttributeValue(
|
|
265
|
+
name="action_name",
|
|
266
|
+
value=func.__name__,
|
|
267
|
+
type="string"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Add status if requested and result is a Status
|
|
271
|
+
if log_status and isinstance(result, Status):
|
|
272
|
+
event_attrs["status"] = EventAttributeValue(
|
|
273
|
+
name="status",
|
|
274
|
+
value=result.name,
|
|
275
|
+
type="string"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Add FSM state if available and requested
|
|
279
|
+
if log_fsm_state and fsm_runner is not None:
|
|
280
|
+
try:
|
|
281
|
+
current_state = fsm_runner.current_state
|
|
282
|
+
if current_state is not None:
|
|
283
|
+
state_name = current_state.name
|
|
284
|
+
event_attrs["fsm_state"] = EventAttributeValue(
|
|
285
|
+
name="fsm_state",
|
|
286
|
+
value=state_name,
|
|
287
|
+
type="string"
|
|
288
|
+
)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.debug(f"Could not extract FSM state: {e}")
|
|
291
|
+
|
|
292
|
+
# Extract from blackboard
|
|
293
|
+
if log_blackboard:
|
|
294
|
+
bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
|
|
295
|
+
# Convert EventAttributeValue to dict format for merging
|
|
296
|
+
for attr_name, attr_value in bb_attrs.items():
|
|
297
|
+
event_attrs[attr_name] = attr_value
|
|
298
|
+
|
|
299
|
+
# Extract objects from blackboard
|
|
300
|
+
bb_objects = extract_objects_from_blackboard(bb)
|
|
301
|
+
|
|
302
|
+
# Build relationships
|
|
303
|
+
relationships = {}
|
|
304
|
+
for obj in bb_objects:
|
|
305
|
+
# Determine qualifier from ObjectRef if available
|
|
306
|
+
qualifier = "context"
|
|
307
|
+
if hasattr(obj, '__dict__') and hasattr(type(obj), '__annotations__'):
|
|
308
|
+
for field_name, field_type in type(obj).__annotations__.items():
|
|
309
|
+
# Check for ObjectRef metadata
|
|
310
|
+
from ..spores.models import ObjectRef
|
|
311
|
+
if hasattr(field_type, '__metadata__'):
|
|
312
|
+
for meta in field_type.__metadata__:
|
|
313
|
+
if isinstance(meta, ObjectRef):
|
|
314
|
+
qualifier = meta.qualifier
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
relationships[obj.id] = Relationship(
|
|
318
|
+
object_id=obj.id,
|
|
319
|
+
qualifier=qualifier
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Build event
|
|
323
|
+
event = Event(
|
|
324
|
+
id=generate_event_id(),
|
|
325
|
+
type=event_type,
|
|
326
|
+
time=timestamp,
|
|
327
|
+
attributes=event_attrs,
|
|
328
|
+
relationships=relationships
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Send event
|
|
332
|
+
await _send_log_record(LogRecord(event=event))
|
|
333
|
+
|
|
334
|
+
# Send objects to cache
|
|
335
|
+
cache = get_object_cache()
|
|
336
|
+
for obj in bb_objects:
|
|
337
|
+
cache.contains_or_add(obj.id, obj)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Failed to log action event: {e}")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
async def _log_tree_tick_event(
|
|
344
|
+
tree_runner: Any,
|
|
345
|
+
event_type: str,
|
|
346
|
+
log_blackboard: bool,
|
|
347
|
+
log_fsm_states: bool,
|
|
348
|
+
result: Status,
|
|
349
|
+
tree_name: Optional[str],
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Log a tree tick event."""
|
|
352
|
+
config = get_config()
|
|
353
|
+
if not config.enabled:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
timestamp = datetime.now()
|
|
358
|
+
|
|
359
|
+
# Build event attributes
|
|
360
|
+
event_attrs = {}
|
|
361
|
+
|
|
362
|
+
# Add tree name
|
|
363
|
+
if tree_name:
|
|
364
|
+
event_attrs["tree_name"] = EventAttributeValue(
|
|
365
|
+
name="tree_name",
|
|
366
|
+
value=tree_name,
|
|
367
|
+
type="string"
|
|
368
|
+
)
|
|
369
|
+
elif hasattr(tree_runner, 'spec') and tree_runner.spec:
|
|
370
|
+
event_attrs["tree_name"] = EventAttributeValue(
|
|
371
|
+
name="tree_name",
|
|
372
|
+
value=tree_runner.spec.name,
|
|
373
|
+
type="string"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Add tick result
|
|
377
|
+
event_attrs["tick_result"] = EventAttributeValue(
|
|
378
|
+
name="tick_result",
|
|
379
|
+
value=result.name,
|
|
380
|
+
type="string"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Add blackboard snapshot
|
|
384
|
+
if log_blackboard and hasattr(tree_runner, 'bb'):
|
|
385
|
+
bb = tree_runner.bb
|
|
386
|
+
bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
|
|
387
|
+
for attr_name, attr_value in bb_attrs.items():
|
|
388
|
+
event_attrs[attr_name] = attr_value
|
|
389
|
+
|
|
390
|
+
# Add FSM states
|
|
391
|
+
if log_fsm_states and hasattr(tree_runner, 'instance'):
|
|
392
|
+
try:
|
|
393
|
+
fsm_states = {}
|
|
394
|
+
for fsm_name, fsm_runner in tree_runner.instance._fsm_runners.items():
|
|
395
|
+
current_state = fsm_runner.current_state
|
|
396
|
+
if current_state is not None:
|
|
397
|
+
fsm_states[fsm_name] = current_state.name
|
|
398
|
+
|
|
399
|
+
if fsm_states:
|
|
400
|
+
event_attrs["fsm_states"] = EventAttributeValue(
|
|
401
|
+
name="fsm_states",
|
|
402
|
+
value=str(fsm_states),
|
|
403
|
+
type="string"
|
|
404
|
+
)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.debug(f"Could not extract FSM states: {e}")
|
|
407
|
+
|
|
408
|
+
# Extract objects from blackboard
|
|
409
|
+
bb_objects = []
|
|
410
|
+
if hasattr(tree_runner, 'bb'):
|
|
411
|
+
bb_objects = extract_objects_from_blackboard(tree_runner.bb)
|
|
412
|
+
|
|
413
|
+
# Build relationships
|
|
414
|
+
relationships = {}
|
|
415
|
+
for obj in bb_objects:
|
|
416
|
+
relationships[obj.id] = Relationship(
|
|
417
|
+
object_id=obj.id,
|
|
418
|
+
qualifier="context"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Build event
|
|
422
|
+
event = Event(
|
|
423
|
+
id=generate_event_id(),
|
|
424
|
+
type=event_type,
|
|
425
|
+
time=timestamp,
|
|
426
|
+
attributes=event_attrs,
|
|
427
|
+
relationships=relationships
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Send event
|
|
431
|
+
await _send_log_record(LogRecord(event=event))
|
|
432
|
+
|
|
433
|
+
# Send objects to cache
|
|
434
|
+
cache = get_object_cache()
|
|
435
|
+
for obj in bb_objects:
|
|
436
|
+
cache.contains_or_add(obj.id, obj)
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Failed to log tree tick event: {e}")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
async def _log_state_snapshot(
|
|
443
|
+
tree_instance: Any,
|
|
444
|
+
snapshot_name: str,
|
|
445
|
+
include_blackboard: bool,
|
|
446
|
+
include_fsms: bool,
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Log a state snapshot as an object."""
|
|
449
|
+
config = get_config()
|
|
450
|
+
if not config.enabled:
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
timestamp = datetime.now()
|
|
455
|
+
|
|
456
|
+
# Build snapshot attributes
|
|
457
|
+
snapshot_attrs = {}
|
|
458
|
+
|
|
459
|
+
# Add tree name
|
|
460
|
+
if hasattr(tree_instance, 'spec') and tree_instance.spec:
|
|
461
|
+
snapshot_attrs["tree_name"] = ObjectAttributeValue(
|
|
462
|
+
name="tree_name",
|
|
463
|
+
value=tree_instance.spec.name,
|
|
464
|
+
type="string",
|
|
465
|
+
time=timestamp
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Add blackboard state
|
|
469
|
+
if include_blackboard and hasattr(tree_instance, 'bb'):
|
|
470
|
+
bb = tree_instance.bb
|
|
471
|
+
if hasattr(bb, 'model_dump'):
|
|
472
|
+
snapshot_attrs["blackboard"] = ObjectAttributeValue(
|
|
473
|
+
name="blackboard",
|
|
474
|
+
value=str(bb.model_dump()),
|
|
475
|
+
type="string",
|
|
476
|
+
time=timestamp
|
|
477
|
+
)
|
|
478
|
+
elif hasattr(bb, '__dict__'):
|
|
479
|
+
snapshot_attrs["blackboard"] = ObjectAttributeValue(
|
|
480
|
+
name="blackboard",
|
|
481
|
+
value=str(bb.__dict__),
|
|
482
|
+
type="string",
|
|
483
|
+
time=timestamp
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Add FSM states
|
|
487
|
+
if include_fsms and hasattr(tree_instance, '_fsm_runners'):
|
|
488
|
+
fsm_states = {}
|
|
489
|
+
for fsm_name, fsm_runner in tree_instance._fsm_runners.items():
|
|
490
|
+
current_state = fsm_runner.current_state
|
|
491
|
+
if current_state is not None:
|
|
492
|
+
fsm_states[fsm_name] = current_state.name
|
|
493
|
+
|
|
494
|
+
snapshot_attrs["fsm_states"] = ObjectAttributeValue(
|
|
495
|
+
name="fsm_states",
|
|
496
|
+
value=str(fsm_states),
|
|
497
|
+
type="string",
|
|
498
|
+
time=timestamp
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Create snapshot object
|
|
502
|
+
from ..spores.models import Object as SporesObject
|
|
503
|
+
import uuid
|
|
504
|
+
|
|
505
|
+
snapshot_obj = SporesObject(
|
|
506
|
+
id=str(uuid.uuid4()), # Generate unique ID for snapshot
|
|
507
|
+
type=f"StateSnapshot:{snapshot_name}",
|
|
508
|
+
attributes=snapshot_attrs
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Send object - type: ignore because pyright confuses Object kwarg with built-in
|
|
512
|
+
await _send_log_record(LogRecord(object=snapshot_obj)) # type: ignore[arg-type]
|
|
513
|
+
|
|
514
|
+
except Exception as e:
|
|
515
|
+
logger.error(f"Failed to log state snapshot: {e}")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def log_tree_event(
|
|
519
|
+
event_type: str,
|
|
520
|
+
tree_name: str,
|
|
521
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
522
|
+
) -> Callable:
|
|
523
|
+
"""
|
|
524
|
+
Decorator to log tree-level events.
|
|
525
|
+
|
|
526
|
+
Use this for logging custom events at the tree level.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
event_type: Type of event to log
|
|
530
|
+
tree_name: Name of the tree
|
|
531
|
+
attributes: Static attributes to include
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Decorator function
|
|
535
|
+
"""
|
|
536
|
+
def decorator(func: Callable) -> Callable:
|
|
537
|
+
@functools.wraps(func)
|
|
538
|
+
async def async_wrapper(*args, **kwargs):
|
|
539
|
+
result = await func(*args, **kwargs)
|
|
540
|
+
|
|
541
|
+
# Log event
|
|
542
|
+
config = get_config()
|
|
543
|
+
if config.enabled:
|
|
544
|
+
await _log_tree_event(func, tree_name, event_type, attributes, args, kwargs)
|
|
545
|
+
|
|
546
|
+
return result
|
|
547
|
+
|
|
548
|
+
@functools.wraps(func)
|
|
549
|
+
def sync_wrapper(*args, **kwargs):
|
|
550
|
+
result = func(*args, **kwargs)
|
|
551
|
+
|
|
552
|
+
# Schedule logging
|
|
553
|
+
config = get_config()
|
|
554
|
+
if config.enabled:
|
|
555
|
+
asyncio.create_task(_log_tree_event(
|
|
556
|
+
func, tree_name, event_type, attributes, args, kwargs
|
|
557
|
+
))
|
|
558
|
+
|
|
559
|
+
return result
|
|
560
|
+
|
|
561
|
+
if asyncio.iscoroutinefunction(func):
|
|
562
|
+
return async_wrapper # type: ignore
|
|
563
|
+
else:
|
|
564
|
+
return sync_wrapper # type: ignore
|
|
565
|
+
|
|
566
|
+
return decorator
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
async def _log_tree_event(
|
|
570
|
+
func: Callable,
|
|
571
|
+
tree_name: str,
|
|
572
|
+
event_type: str,
|
|
573
|
+
attributes: Optional[Dict[str, Any]],
|
|
574
|
+
args: tuple,
|
|
575
|
+
kwargs: dict,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Log a tree-level event."""
|
|
578
|
+
config = get_config()
|
|
579
|
+
if not config.enabled:
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
try:
|
|
583
|
+
timestamp = datetime.now()
|
|
584
|
+
|
|
585
|
+
# Build event attributes
|
|
586
|
+
event_attrs = {
|
|
587
|
+
"tree_name": EventAttributeValue(
|
|
588
|
+
name="tree_name",
|
|
589
|
+
value=tree_name,
|
|
590
|
+
type="string"
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
# Add static attributes
|
|
595
|
+
if attributes:
|
|
596
|
+
for key, value in attributes.items():
|
|
597
|
+
if callable(value):
|
|
598
|
+
# Extract bb from kwargs or args
|
|
599
|
+
bb = kwargs.get('bb') or (args[0] if args else None)
|
|
600
|
+
if bb:
|
|
601
|
+
result = value(bb)
|
|
602
|
+
event_attrs[key] = EventAttributeValue(
|
|
603
|
+
name=key,
|
|
604
|
+
value=str(result),
|
|
605
|
+
type="string"
|
|
606
|
+
)
|
|
607
|
+
else:
|
|
608
|
+
event_attrs[key] = EventAttributeValue(
|
|
609
|
+
name=key,
|
|
610
|
+
value=str(value),
|
|
611
|
+
type="string"
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Extract bb for object logging
|
|
615
|
+
bb = kwargs.get('bb') or (args[0] if args else None)
|
|
616
|
+
|
|
617
|
+
# Extract objects from blackboard
|
|
618
|
+
relationships = {}
|
|
619
|
+
event_objects = []
|
|
620
|
+
|
|
621
|
+
if bb:
|
|
622
|
+
bb_objects = extract_objects_from_blackboard(bb)
|
|
623
|
+
event_objects.extend(bb_objects)
|
|
624
|
+
|
|
625
|
+
for obj in bb_objects:
|
|
626
|
+
relationships[obj.id] = Relationship(
|
|
627
|
+
object_id=obj.id,
|
|
628
|
+
qualifier="context"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# Build event
|
|
632
|
+
event = Event(
|
|
633
|
+
id=generate_event_id(),
|
|
634
|
+
type=event_type,
|
|
635
|
+
time=timestamp,
|
|
636
|
+
attributes=event_attrs,
|
|
637
|
+
relationships=relationships
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
# Send event
|
|
641
|
+
await _send_log_record(LogRecord(event=event))
|
|
642
|
+
|
|
643
|
+
# Send objects to cache
|
|
644
|
+
cache = get_object_cache()
|
|
645
|
+
for obj in event_objects:
|
|
646
|
+
cache.contains_or_add(obj.id, obj)
|
|
647
|
+
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.error(f"Failed to log tree event: {e}")
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
__all__ = [
|
|
653
|
+
'TreeSporesAdapter',
|
|
654
|
+
'log_tree_event',
|
|
655
|
+
]
|