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,45 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Rhizomorph Behavior Tree Utilities
|
|
4
|
+
|
|
5
|
+
Utility functions for behavior tree operations like Mermaid diagram generation.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Any
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_mermaid(tree: SimpleNamespace) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Generate Mermaid diagram from a behavior tree namespace.
|
|
14
|
+
|
|
15
|
+
This is a utility wrapper that accesses the tree's internal to_mermaid method.
|
|
16
|
+
Use this when you have a tree namespace and want to generate a diagram.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
tree: The behavior tree namespace (created with @bt.tree decorator)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Mermaid diagram as a string
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
```python
|
|
26
|
+
from mycorrhizal.rhizomorph.core import bt
|
|
27
|
+
from mycorrhizal.rhizomorph.util import to_mermaid
|
|
28
|
+
|
|
29
|
+
@bt.tree
|
|
30
|
+
def MyTree():
|
|
31
|
+
# ... define tree ...
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# Generate diagram
|
|
35
|
+
mermaid_diagram = to_mermaid(MyTree)
|
|
36
|
+
print(mermaid_diagram)
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
# The tree namespace has a to_mermaid lambda attached by @bt.tree
|
|
40
|
+
if not hasattr(tree, "to_mermaid"):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"Tree namespace must have a 'to_mermaid' attribute. "
|
|
43
|
+
"Did you create this tree with @bt.tree decorator?"
|
|
44
|
+
)
|
|
45
|
+
return tree.to_mermaid()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spores - Event and Object Logging for Observability
|
|
4
|
+
|
|
5
|
+
OCEL (Object-Centric Event Log) compatible logging system for tracking events
|
|
6
|
+
and objects in Mycorrhizal systems. Provides automatic extraction of events
|
|
7
|
+
and objects from DSL execution.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from mycorrhizal.spores import configure, spore, EventAttr, ObjectRef
|
|
11
|
+
|
|
12
|
+
# Configure logging
|
|
13
|
+
configure(
|
|
14
|
+
enabled=True,
|
|
15
|
+
object_cache_size=100,
|
|
16
|
+
transport=FileTransport("logs/ocel.jsonl"),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Mark model fields for automatic extraction
|
|
20
|
+
class MissionContext(BaseModel):
|
|
21
|
+
mission_id: Annotated[str, EventAttr]
|
|
22
|
+
robot: Annotated[Robot, ObjectRef(qualifier="actor", scope=ObjectScope.GLOBAL)]
|
|
23
|
+
|
|
24
|
+
# Mark object types
|
|
25
|
+
@spore.object(object_type="Robot")
|
|
26
|
+
class Robot(BaseModel):
|
|
27
|
+
id: str
|
|
28
|
+
name: str
|
|
29
|
+
|
|
30
|
+
DSL Adapters:
|
|
31
|
+
HyphaAdapter - Log Petri net transitions with token relationships
|
|
32
|
+
RhizomorphAdapter - Log behavior tree node execution with status
|
|
33
|
+
EnokiAdapter - Log state machine execution and lifecycle events
|
|
34
|
+
|
|
35
|
+
Key Concepts:
|
|
36
|
+
Event - Something that happens at a point in time with attributes
|
|
37
|
+
Object - An entity with a type and attributes
|
|
38
|
+
Relationship - Link between events and objects (e.g., "actor", "target")
|
|
39
|
+
EventAttr - Mark blackboard fields for automatic event attribute extraction
|
|
40
|
+
ObjectRef - Mark blackboard fields as object references with scope
|
|
41
|
+
|
|
42
|
+
Object Scopes:
|
|
43
|
+
EVENT - Object exists only for this event (default)
|
|
44
|
+
GLOBAL - Object exists across all events (deduplicated by ID)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from .core import (
|
|
48
|
+
configure,
|
|
49
|
+
get_config,
|
|
50
|
+
get_object_cache,
|
|
51
|
+
spore,
|
|
52
|
+
SporesConfig,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
from .models import (
|
|
56
|
+
Event,
|
|
57
|
+
Object,
|
|
58
|
+
LogRecord,
|
|
59
|
+
Relationship,
|
|
60
|
+
EventAttributeValue,
|
|
61
|
+
ObjectAttributeValue,
|
|
62
|
+
ObjectRef,
|
|
63
|
+
ObjectScope,
|
|
64
|
+
EventAttr,
|
|
65
|
+
generate_event_id,
|
|
66
|
+
generate_object_id,
|
|
67
|
+
attribute_value_from_python,
|
|
68
|
+
object_attribute_from_python,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
from .cache import ObjectLRUCache
|
|
72
|
+
|
|
73
|
+
from .encoder import Encoder, JSONEncoder
|
|
74
|
+
from .transport import Transport
|
|
75
|
+
|
|
76
|
+
# DSL adapters
|
|
77
|
+
from .dsl import (
|
|
78
|
+
HyphaAdapter,
|
|
79
|
+
RhizomorphAdapter,
|
|
80
|
+
EnokiAdapter,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__version__ = "0.1.0"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = [
|
|
88
|
+
# Core API
|
|
89
|
+
'configure',
|
|
90
|
+
'get_config',
|
|
91
|
+
'get_object_cache',
|
|
92
|
+
'spore',
|
|
93
|
+
'SporesConfig',
|
|
94
|
+
|
|
95
|
+
# Models
|
|
96
|
+
'Event',
|
|
97
|
+
'Object',
|
|
98
|
+
'LogRecord',
|
|
99
|
+
'Relationship',
|
|
100
|
+
'EventAttributeValue',
|
|
101
|
+
'ObjectAttributeValue',
|
|
102
|
+
'ObjectRef',
|
|
103
|
+
'ObjectScope',
|
|
104
|
+
'EventAttr',
|
|
105
|
+
'generate_event_id',
|
|
106
|
+
'generate_object_id',
|
|
107
|
+
'attribute_value_from_python',
|
|
108
|
+
'object_attribute_from_python',
|
|
109
|
+
|
|
110
|
+
# Cache
|
|
111
|
+
'ObjectLRUCache',
|
|
112
|
+
|
|
113
|
+
# Encoder
|
|
114
|
+
'Encoder',
|
|
115
|
+
'JSONEncoder',
|
|
116
|
+
|
|
117
|
+
# Transport
|
|
118
|
+
'Transport',
|
|
119
|
+
|
|
120
|
+
# DSL Adapters
|
|
121
|
+
'HyphaAdapter',
|
|
122
|
+
'RhizomorphAdapter',
|
|
123
|
+
'EnokiAdapter',
|
|
124
|
+
]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spores Object Cache
|
|
4
|
+
|
|
5
|
+
LRU cache for tracking seen objects and logging them on eviction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from typing import Dict, Callable, Optional, TypeVar, Generic
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from .models import Object
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Generic types for cache
|
|
18
|
+
K = TypeVar('K') # Key type (object ID)
|
|
19
|
+
V = TypeVar('V') # Value type (Object)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class CacheEntry:
|
|
24
|
+
"""
|
|
25
|
+
An entry in the object cache.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
object: The OCEL Object
|
|
29
|
+
sight_count: How many times we've seen this object
|
|
30
|
+
first_sight_time: When we first saw the object
|
|
31
|
+
"""
|
|
32
|
+
object: Object
|
|
33
|
+
sight_count: int = 1
|
|
34
|
+
first_sight_time: Optional[float] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ObjectLRUCache(Generic[K, V]):
|
|
38
|
+
"""
|
|
39
|
+
LRU cache with eviction callback for object tracking.
|
|
40
|
+
|
|
41
|
+
When an object is first seen, it should be logged via on_first_sight.
|
|
42
|
+
When an object is evicted from the cache, it's logged via on_evict.
|
|
43
|
+
|
|
44
|
+
This ensures OCEL consumers see object evolution:
|
|
45
|
+
- First sight: Initial object state
|
|
46
|
+
- Eviction: Final state before being removed from cache
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
```python
|
|
50
|
+
def on_evict(object_id: str, obj: Object):
|
|
51
|
+
# Send object to transport
|
|
52
|
+
transport.send(LogRecord(object=obj))
|
|
53
|
+
|
|
54
|
+
cache = ObjectLRUCache(maxsize=128, on_evict=on_evict)
|
|
55
|
+
|
|
56
|
+
# Check if object exists, add if not
|
|
57
|
+
if not cache.contains_or_add(object_id, object):
|
|
58
|
+
# Object not in cache, already logged by on_first_sight
|
|
59
|
+
pass
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
maxsize: Maximum number of objects to cache
|
|
64
|
+
on_evict: Callback when object is evicted (object_id, object) -> None
|
|
65
|
+
on_first_sight: Optional callback when object is first seen (object_id, object) -> None
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
maxsize: int = 128,
|
|
71
|
+
on_evict: Optional[Callable[[K, Object], None]] = None,
|
|
72
|
+
on_first_sight: Optional[Callable[[K, Object], None]] = None
|
|
73
|
+
):
|
|
74
|
+
self.maxsize = maxsize
|
|
75
|
+
self.on_evict = on_evict
|
|
76
|
+
self.on_first_sight = on_first_sight
|
|
77
|
+
self._cache: OrderedDict[K, CacheEntry] = OrderedDict()
|
|
78
|
+
|
|
79
|
+
def _evict_if_needed(self) -> None:
|
|
80
|
+
"""Evict oldest entry if cache is full."""
|
|
81
|
+
if len(self._cache) >= self.maxsize:
|
|
82
|
+
# FIFO from OrderedDict (oldest first)
|
|
83
|
+
object_id, entry = self._cache.popitem(last=False)
|
|
84
|
+
if self.on_evict:
|
|
85
|
+
self.on_evict(object_id, entry.object)
|
|
86
|
+
|
|
87
|
+
def get(self, key: K) -> Optional[Object]:
|
|
88
|
+
"""
|
|
89
|
+
Get an object from the cache without updating its position.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
key: The object ID
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The Object if found, None otherwise
|
|
96
|
+
"""
|
|
97
|
+
entry = self._cache.get(key)
|
|
98
|
+
return entry.object if entry else None
|
|
99
|
+
|
|
100
|
+
def get_and_touch(self, key: K) -> Optional[Object]:
|
|
101
|
+
"""
|
|
102
|
+
Get an object from the cache and mark it as recently used.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
key: The object ID
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The Object if found, None otherwise
|
|
109
|
+
"""
|
|
110
|
+
entry = self._cache.get(key)
|
|
111
|
+
if entry:
|
|
112
|
+
# Move to end (most recently used)
|
|
113
|
+
self._cache.move_to_end(key)
|
|
114
|
+
return entry.object
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def contains_or_add(self, key: K, obj: Object) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Check if key exists, add if not.
|
|
120
|
+
|
|
121
|
+
This is the primary method for object tracking:
|
|
122
|
+
- If key exists: mark as recently used, return True
|
|
123
|
+
- If key doesn't exist: add object, potentially evict, call on_first_sight, return False
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
key: The object ID
|
|
127
|
+
obj: The OCEL Object to add if not present
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if object was already in cache, False if it was just added
|
|
131
|
+
"""
|
|
132
|
+
if key in self._cache:
|
|
133
|
+
# Already seen, mark as recently used
|
|
134
|
+
self._cache.move_to_end(key)
|
|
135
|
+
entry = self._cache[key]
|
|
136
|
+
entry.sight_count += 1
|
|
137
|
+
return True
|
|
138
|
+
else:
|
|
139
|
+
# First sight
|
|
140
|
+
self._evict_if_needed()
|
|
141
|
+
|
|
142
|
+
entry = CacheEntry(object=obj)
|
|
143
|
+
self._cache[key] = entry
|
|
144
|
+
|
|
145
|
+
if self.on_first_sight:
|
|
146
|
+
self.on_first_sight(key, obj)
|
|
147
|
+
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def add(self, key: K, obj: Object) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Add an object to the cache (or update if exists).
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
key: The object ID
|
|
156
|
+
obj: The OCEL Object
|
|
157
|
+
"""
|
|
158
|
+
if key in self._cache:
|
|
159
|
+
# Update existing entry
|
|
160
|
+
entry = self._cache[key]
|
|
161
|
+
entry.object = obj
|
|
162
|
+
entry.sight_count += 1
|
|
163
|
+
self._cache.move_to_end(key)
|
|
164
|
+
else:
|
|
165
|
+
# New entry
|
|
166
|
+
self._evict_if_needed()
|
|
167
|
+
|
|
168
|
+
entry = CacheEntry(object=obj)
|
|
169
|
+
self._cache[key] = entry
|
|
170
|
+
|
|
171
|
+
if self.on_first_sight:
|
|
172
|
+
self.on_first_sight(key, obj)
|
|
173
|
+
|
|
174
|
+
def remove(self, key: K) -> Optional[Object]:
|
|
175
|
+
"""
|
|
176
|
+
Remove an object from the cache.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
key: The object ID
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The removed Object, or None if key wasn't in cache
|
|
183
|
+
"""
|
|
184
|
+
return self._cache.pop(key, None)
|
|
185
|
+
|
|
186
|
+
def clear(self) -> None:
|
|
187
|
+
"""Clear all objects from the cache without logging."""
|
|
188
|
+
self._cache.clear()
|
|
189
|
+
|
|
190
|
+
def __len__(self) -> int:
|
|
191
|
+
"""Return the number of objects in the cache."""
|
|
192
|
+
return len(self._cache)
|
|
193
|
+
|
|
194
|
+
def __contains__(self, key: K) -> bool:
|
|
195
|
+
"""Check if key is in cache."""
|
|
196
|
+
return key in self._cache
|
|
197
|
+
|
|
198
|
+
def keys(self) -> list[K]:
|
|
199
|
+
"""Return all keys in the cache."""
|
|
200
|
+
return list(self._cache.keys())
|
|
201
|
+
|
|
202
|
+
def values(self) -> list[Object]:
|
|
203
|
+
"""Return all objects in the cache."""
|
|
204
|
+
return [entry.object for entry in self._cache.values()]
|
|
205
|
+
|
|
206
|
+
def items(self) -> list[tuple[K, Object]]:
|
|
207
|
+
"""Return all (key, object) pairs in the cache."""
|
|
208
|
+
return [(key, entry.object) for key, entry in self._cache.items()]
|