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,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())})"