mycorrhizal 0.1.0__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 -0
- 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 +56 -8
- mycorrhizal/hypha/core/runtime.py +242 -107
- mycorrhizal/hypha/core/specs.py +19 -3
- 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 +308 -82
- 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 +72 -19
- mycorrhizal/spores/core.py +907 -75
- 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 +75 -20
- mycorrhizal/spores/transport/__init__.py +9 -2
- mycorrhizal/spores/transport/base.py +36 -17
- mycorrhizal/spores/transport/file.py +126 -0
- mycorrhizal-0.2.0.dist-info/METADATA +335 -0
- mycorrhizal-0.2.0.dist-info/RECORD +54 -0
- mycorrhizal-0.1.0.dist-info/METADATA +0 -198
- mycorrhizal-0.1.0.dist-info/RECORD +0 -37
- /mycorrhizal/{enoki → septum}/__init__.py +0 -0
- {mycorrhizal-0.1.0.dist-info → mycorrhizal-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PNContext - Context object for BT-in-PN integration.
|
|
4
|
+
|
|
5
|
+
This module provides the PNContext class that gets injected into the blackboard
|
|
6
|
+
during BT execution for BT-in-PN transitions. It provides methods for routing
|
|
7
|
+
tokens, rejecting tokens, and accessing PN runtime information.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, List, Optional, TYPE_CHECKING
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..hypha.core.runtime import NetRuntime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PlacesContext:
|
|
19
|
+
"""
|
|
20
|
+
Simple namespace for accessing output places by name.
|
|
21
|
+
|
|
22
|
+
This provides a clean API like `pn_ctx.places.critical_queue` instead of
|
|
23
|
+
needing to use string keys.
|
|
24
|
+
"""
|
|
25
|
+
_place_refs: dict
|
|
26
|
+
|
|
27
|
+
def __getattr__(self, name: str) -> Any:
|
|
28
|
+
"""Get a place reference by name."""
|
|
29
|
+
if name in self._place_refs:
|
|
30
|
+
return self._place_refs[name]
|
|
31
|
+
raise AttributeError(f"No place named '{name}' available for routing")
|
|
32
|
+
|
|
33
|
+
def __contains__(self, name: str) -> bool:
|
|
34
|
+
"""Check if a place is available."""
|
|
35
|
+
return name in self._place_refs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PNContext:
|
|
39
|
+
"""
|
|
40
|
+
Context object for BT-in-PN integration.
|
|
41
|
+
|
|
42
|
+
This is injected into the blackboard as `pn_ctx` during BT execution
|
|
43
|
+
for BT-in-PN transitions. It provides:
|
|
44
|
+
|
|
45
|
+
- Access to current token(s) being processed
|
|
46
|
+
- Access to output places for routing
|
|
47
|
+
- Methods for routing, rejecting, deferring tokens
|
|
48
|
+
- Access to timebase
|
|
49
|
+
|
|
50
|
+
Example usage in BT action:
|
|
51
|
+
```python
|
|
52
|
+
@bt.action
|
|
53
|
+
async def route_critical(bb):
|
|
54
|
+
token = bb.pn_ctx.current_token
|
|
55
|
+
pn_ctx = bb.pn_ctx
|
|
56
|
+
|
|
57
|
+
if token.priority == TaskPriority.CRITICAL:
|
|
58
|
+
pn_ctx.route_to(pn_ctx.places.critical_queue)
|
|
59
|
+
return Status.SUCCESS
|
|
60
|
+
return Status.FAILURE
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
net_runtime: "NetRuntime",
|
|
67
|
+
output_places: List[Any],
|
|
68
|
+
tokens: List[Any],
|
|
69
|
+
timebase: Any,
|
|
70
|
+
token_origins: Optional[dict] = None,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Initialize PNContext.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
net_runtime: The PN net runtime instance
|
|
77
|
+
output_places: List of output PlaceRef objects for this transition
|
|
78
|
+
tokens: Tokens being processed (single token for token mode, all for batch mode)
|
|
79
|
+
timebase: The timebase for timing operations
|
|
80
|
+
token_origins: Optional dict mapping tokens to their source place tuples
|
|
81
|
+
"""
|
|
82
|
+
self._net_runtime = net_runtime
|
|
83
|
+
self._timebase = timebase
|
|
84
|
+
|
|
85
|
+
# Store tokens
|
|
86
|
+
self._tokens = tokens if tokens else []
|
|
87
|
+
self._current_index = 0 # For iterating through tokens in token mode
|
|
88
|
+
|
|
89
|
+
# Store token origins for proper rejection handling
|
|
90
|
+
self._token_origins: dict = token_origins if token_origins else {}
|
|
91
|
+
|
|
92
|
+
# Build output places mapping
|
|
93
|
+
self._output_place_refs: dict[str, Any] = {}
|
|
94
|
+
for place_ref in output_places:
|
|
95
|
+
# Use the local name from the place reference
|
|
96
|
+
place_name = place_ref.local_name
|
|
97
|
+
self._output_place_refs[place_name] = place_ref
|
|
98
|
+
|
|
99
|
+
# Create places namespace
|
|
100
|
+
self.places = PlacesContext(self._output_place_refs)
|
|
101
|
+
|
|
102
|
+
# Track routing decisions
|
|
103
|
+
self._routing_decisions: List[tuple[Any, Any]] = [] # (token, place_ref) pairs
|
|
104
|
+
self._rejected_tokens: List[tuple[Any, str]] = [] # (token, reason) pairs
|
|
105
|
+
self._deferred_tokens: List[Any] = [] # tokens to defer
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def current_token(self) -> Any:
|
|
109
|
+
"""
|
|
110
|
+
Get the current token being processed (token mode).
|
|
111
|
+
|
|
112
|
+
In token mode, returns the single token for this BT execution.
|
|
113
|
+
In batch mode, returns None (use `tokens` instead).
|
|
114
|
+
"""
|
|
115
|
+
if len(self._tokens) == 1:
|
|
116
|
+
return self._tokens[0]
|
|
117
|
+
elif self._tokens and self._current_index < len(self._tokens):
|
|
118
|
+
return self._tokens[self._current_index]
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def tokens(self) -> List[Any]:
|
|
123
|
+
"""
|
|
124
|
+
Get all tokens being processed (batch mode).
|
|
125
|
+
|
|
126
|
+
In batch mode, returns all tokens for this BT execution.
|
|
127
|
+
In token mode, returns a list with one token.
|
|
128
|
+
"""
|
|
129
|
+
return self._tokens
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def timebase(self) -> Any:
|
|
133
|
+
"""Get the timebase for timing operations."""
|
|
134
|
+
return self._timebase
|
|
135
|
+
|
|
136
|
+
def route_to(self, place: Any, token: Any = None) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Route a token to a specific output place.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
place: The place reference (from pn_ctx.places.<name>)
|
|
142
|
+
token: The token to route. If None, uses current_token
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
```python
|
|
146
|
+
pn_ctx.route_to(pn_ctx.places.critical_queue)
|
|
147
|
+
pn_ctx.route_to(pn_ctx.places.validated, token=enriched_token)
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
if token is None:
|
|
151
|
+
token = self.current_token
|
|
152
|
+
|
|
153
|
+
if token is None:
|
|
154
|
+
raise ValueError("No token to route (current_token is None)")
|
|
155
|
+
|
|
156
|
+
# Store routing decision for later execution by PN runtime
|
|
157
|
+
self._routing_decisions.append((token, place))
|
|
158
|
+
|
|
159
|
+
def route_all_to(self, place: Any) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Route all tokens to a specific output place (batch mode).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
place: The place reference (from pn_ctx.places.<name>)
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
```python
|
|
168
|
+
# Route all high-priority tasks to fast queue
|
|
169
|
+
pn_ctx.route_all_to(pn_ctx.places.fast_queue)
|
|
170
|
+
```
|
|
171
|
+
"""
|
|
172
|
+
for token in self._tokens:
|
|
173
|
+
self._routing_decisions.append((token, place))
|
|
174
|
+
|
|
175
|
+
def reject_token(self, reason: str = "", token: Any = None) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Reject a token (returns it to input place).
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
reason: Optional reason for rejection
|
|
181
|
+
token: The token to reject. If None, uses current_token
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
```python
|
|
185
|
+
if not token.valid:
|
|
186
|
+
pn_ctx.reject_token("Invalid task data")
|
|
187
|
+
return Status.SUCCESS
|
|
188
|
+
```
|
|
189
|
+
"""
|
|
190
|
+
if token is None:
|
|
191
|
+
token = self.current_token
|
|
192
|
+
|
|
193
|
+
if token is None:
|
|
194
|
+
raise ValueError("No token to reject (current_token is None)")
|
|
195
|
+
|
|
196
|
+
# Store rejection decision
|
|
197
|
+
self._rejected_tokens.append((token, reason))
|
|
198
|
+
|
|
199
|
+
def defer_token(self, token: Any = None) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Defer processing of a token (keeps it in input place for later).
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
token: The token to defer. If None, uses current_token
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
```python
|
|
208
|
+
if external_system_not_ready:
|
|
209
|
+
pn_ctx.defer_token()
|
|
210
|
+
return Status.SUCCESS
|
|
211
|
+
```
|
|
212
|
+
"""
|
|
213
|
+
if token is None:
|
|
214
|
+
token = self.current_token
|
|
215
|
+
|
|
216
|
+
if token is None:
|
|
217
|
+
raise ValueError("No token to defer (current_token is None)")
|
|
218
|
+
|
|
219
|
+
# Store deferral decision
|
|
220
|
+
self._deferred_tokens.append(token)
|
|
221
|
+
|
|
222
|
+
def get_routing_decisions(self) -> List[tuple[Any, Any]]:
|
|
223
|
+
"""
|
|
224
|
+
Get all routing decisions made by the BT.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of (token, place_ref) tuples
|
|
228
|
+
|
|
229
|
+
This is used by the PN runtime to execute the routing after BT completes.
|
|
230
|
+
"""
|
|
231
|
+
return self._routing_decisions
|
|
232
|
+
|
|
233
|
+
def get_rejected_tokens(self) -> List[tuple[Any, str]]:
|
|
234
|
+
"""
|
|
235
|
+
Get all rejected tokens with reasons.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of (token, reason) tuples
|
|
239
|
+
|
|
240
|
+
This is used by the PN runtime to return tokens to input places.
|
|
241
|
+
"""
|
|
242
|
+
return self._rejected_tokens
|
|
243
|
+
|
|
244
|
+
def get_deferred_tokens(self) -> List[Any]:
|
|
245
|
+
"""
|
|
246
|
+
Get all deferred tokens.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of tokens to defer (keep in input)
|
|
250
|
+
|
|
251
|
+
This is used by the PN runtime to keep tokens in their input places.
|
|
252
|
+
"""
|
|
253
|
+
return self._deferred_tokens
|
|
254
|
+
|
|
255
|
+
def has_decisions(self) -> bool:
|
|
256
|
+
"""Check if any routing decisions were made."""
|
|
257
|
+
return bool(self._routing_decisions or self._rejected_tokens or self._deferred_tokens)
|
|
258
|
+
|
|
259
|
+
def get_token_origin(self, token: Any) -> Optional[tuple]:
|
|
260
|
+
"""
|
|
261
|
+
Get the origin place tuple for a token.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
token: The token to look up
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The place tuple (e.g., ('NetName', 'place_name')) or None if not found
|
|
268
|
+
|
|
269
|
+
Note:
|
|
270
|
+
token_origins maps id(token) -> (token, place_parts)
|
|
271
|
+
"""
|
|
272
|
+
# Look up by id(token)
|
|
273
|
+
token_info = self._token_origins.get(id(token))
|
|
274
|
+
if token_info:
|
|
275
|
+
return token_info[1] # Return place_parts
|
|
276
|
+
return None
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tree runner - public API for running Mycelium trees.
|
|
4
|
+
|
|
5
|
+
This module provides the TreeRunner class which is the main public API
|
|
6
|
+
for creating and running Mycelium trees.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Callable, Union
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from ..rhizomorph.core import Status
|
|
15
|
+
from .tree_spec import MyceliumTreeSpec
|
|
16
|
+
from .instance import TreeInstance
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_spec(tree_input: Union[Callable, MyceliumTreeSpec]) -> MyceliumTreeSpec:
|
|
20
|
+
"""Extract MyceliumTreeSpec from various input types."""
|
|
21
|
+
if isinstance(tree_input, MyceliumTreeSpec):
|
|
22
|
+
return tree_input
|
|
23
|
+
|
|
24
|
+
# Assume it's a decorated tree function
|
|
25
|
+
if hasattr(tree_input, '_mycelium_spec'):
|
|
26
|
+
return tree_input._mycelium_spec # type: ignore[attr-defined]
|
|
27
|
+
|
|
28
|
+
raise TypeError(
|
|
29
|
+
f"Expected @tree decorated function or MyceliumTreeSpec, got {type(tree_input)}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TreeRunner:
|
|
34
|
+
"""
|
|
35
|
+
Public API for running a Mycelium tree.
|
|
36
|
+
|
|
37
|
+
The runner creates a TreeInstance with FSM instances and provides
|
|
38
|
+
methods for ticking, running, and inspecting the tree.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> @tree
|
|
42
|
+
>>> def RobotController():
|
|
43
|
+
>>> @fsm(initial=Idle)
|
|
44
|
+
>>> def robot():
|
|
45
|
+
>>> pass
|
|
46
|
+
>>>
|
|
47
|
+
>>> @Action
|
|
48
|
+
>>> async def run(bb, tb, tree):
|
|
49
|
+
>>> await tree.fsms.robot.send_message(Events.START)
|
|
50
|
+
>>> return Status.SUCCESS
|
|
51
|
+
>>>
|
|
52
|
+
>>> @root
|
|
53
|
+
>>> @Sequence
|
|
54
|
+
>>> def main():
|
|
55
|
+
>>> yield run
|
|
56
|
+
>>>
|
|
57
|
+
>>> # Use it
|
|
58
|
+
>>> bb = RobotBlackboard(task_queue=["task1"])
|
|
59
|
+
>>> tb = MonotonicClock()
|
|
60
|
+
>>> runner = TreeRunner(RobotController, bb=bb, tb=tb)
|
|
61
|
+
>>>
|
|
62
|
+
>>> # Run ticks
|
|
63
|
+
>>> result = await runner.tick()
|
|
64
|
+
>>>
|
|
65
|
+
>>> # Or run to completion
|
|
66
|
+
>>> await runner.run(max_ticks=10)
|
|
67
|
+
>>>
|
|
68
|
+
>>> # Get FSM
|
|
69
|
+
>>> fsm = runner.fsms.robot
|
|
70
|
+
>>>
|
|
71
|
+
>>> # Generate diagram
|
|
72
|
+
>>> print(runner.to_mermaid())
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
tree: Union[Callable, MyceliumTreeSpec],
|
|
78
|
+
bb: BaseModel,
|
|
79
|
+
tb: Any,
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Create a new TreeRunner.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
tree: @tree decorated function or MyceliumTreeSpec
|
|
86
|
+
bb: Blackboard (shared state)
|
|
87
|
+
tb: Timebase (shared timing)
|
|
88
|
+
"""
|
|
89
|
+
self.spec = _get_spec(tree)
|
|
90
|
+
self.bb = bb
|
|
91
|
+
self.tb = tb
|
|
92
|
+
|
|
93
|
+
# Create the tree instance
|
|
94
|
+
self.instance = TreeInstance(
|
|
95
|
+
spec=self.spec,
|
|
96
|
+
bb=self.bb,
|
|
97
|
+
tb=self.tb,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def fsms(self):
|
|
102
|
+
"""
|
|
103
|
+
Access FSM instances.
|
|
104
|
+
|
|
105
|
+
Provides attribute and dict-style access:
|
|
106
|
+
runner.fsms.robot
|
|
107
|
+
runner.fsms['robot']
|
|
108
|
+
"""
|
|
109
|
+
return self.instance.fsms
|
|
110
|
+
|
|
111
|
+
async def tick(self) -> Status:
|
|
112
|
+
"""
|
|
113
|
+
Execute one tick of the tree.
|
|
114
|
+
|
|
115
|
+
This will auto-tick all FSMs with auto_tick=True, then
|
|
116
|
+
execute the BT tree once.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Status from the tree tick
|
|
120
|
+
"""
|
|
121
|
+
return await self.instance.tick()
|
|
122
|
+
|
|
123
|
+
async def run(self, max_ticks: int = 100) -> Status:
|
|
124
|
+
"""
|
|
125
|
+
Run the tree until completion or max_ticks reached.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
max_ticks: Maximum number of ticks to execute
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Final status
|
|
132
|
+
"""
|
|
133
|
+
for _ in range(max_ticks):
|
|
134
|
+
result = await self.tick()
|
|
135
|
+
if result in (Status.SUCCESS, Status.FAILURE, Status.ERROR):
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
return Status.RUNNING
|
|
139
|
+
|
|
140
|
+
def reset(self) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Reset the tree instance.
|
|
143
|
+
|
|
144
|
+
Creates new FSM instances and clears any state.
|
|
145
|
+
"""
|
|
146
|
+
# Create new instance
|
|
147
|
+
self.instance = TreeInstance(
|
|
148
|
+
spec=self.spec,
|
|
149
|
+
bb=self.bb,
|
|
150
|
+
tb=self.tb,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def to_mermaid(self) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Generate a unified Mermaid diagram.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Mermaid diagram code showing BT and all FSMs
|
|
159
|
+
"""
|
|
160
|
+
diagram = self.instance.to_mermaid()
|
|
161
|
+
# Wrap in mermaid code block
|
|
162
|
+
return f"```mermaid\n{diagram}\n```"
|
|
163
|
+
|
|
164
|
+
def __repr__(self) -> str:
|
|
165
|
+
return f"TreeRunner(tree={self.spec.name}, fsms={list(self.spec.fsms.keys())})"
|