mycorrhizal 0.1.2__py3-none-any.whl → 0.2.1__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 (41) hide show
  1. mycorrhizal/_version.py +1 -1
  2. mycorrhizal/common/__init__.py +15 -3
  3. mycorrhizal/common/cache.py +114 -0
  4. mycorrhizal/common/compilation.py +263 -0
  5. mycorrhizal/common/interface_detection.py +159 -0
  6. mycorrhizal/common/interfaces.py +3 -50
  7. mycorrhizal/common/mermaid.py +124 -0
  8. mycorrhizal/common/wrappers.py +1 -1
  9. mycorrhizal/hypha/core/builder.py +11 -1
  10. mycorrhizal/hypha/core/runtime.py +242 -107
  11. mycorrhizal/mycelium/__init__.py +174 -0
  12. mycorrhizal/mycelium/core.py +619 -0
  13. mycorrhizal/mycelium/exceptions.py +30 -0
  14. mycorrhizal/mycelium/hypha_bridge.py +1143 -0
  15. mycorrhizal/mycelium/instance.py +440 -0
  16. mycorrhizal/mycelium/pn_context.py +276 -0
  17. mycorrhizal/mycelium/runner.py +165 -0
  18. mycorrhizal/mycelium/spores_integration.py +655 -0
  19. mycorrhizal/mycelium/tree_builder.py +102 -0
  20. mycorrhizal/mycelium/tree_spec.py +197 -0
  21. mycorrhizal/rhizomorph/README.md +82 -33
  22. mycorrhizal/rhizomorph/core.py +287 -119
  23. mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
  24. mycorrhizal/{enoki → septum}/core.py +326 -100
  25. mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
  26. mycorrhizal/{enoki → septum}/util.py +44 -21
  27. mycorrhizal/spores/__init__.py +3 -3
  28. mycorrhizal/spores/core.py +149 -28
  29. mycorrhizal/spores/dsl/__init__.py +8 -8
  30. mycorrhizal/spores/dsl/hypha.py +3 -15
  31. mycorrhizal/spores/dsl/rhizomorph.py +3 -11
  32. mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
  33. mycorrhizal/spores/encoder/json.py +21 -12
  34. mycorrhizal/spores/extraction.py +14 -11
  35. mycorrhizal/spores/models.py +53 -20
  36. mycorrhizal-0.2.1.dist-info/METADATA +335 -0
  37. mycorrhizal-0.2.1.dist-info/RECORD +54 -0
  38. mycorrhizal-0.1.2.dist-info/METADATA +0 -198
  39. mycorrhizal-0.1.2.dist-info/RECORD +0 -39
  40. /mycorrhizal/{enoki → septum}/__init__.py +0 -0
  41. {mycorrhizal-0.1.2.dist-info → mycorrhizal-0.2.1.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
+ ]