dissyslab 1.0.2__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.
- dissyslab/__init__.py +25 -0
- dissyslab/blocks/__init__.py +32 -0
- dissyslab/blocks/fanin.py +90 -0
- dissyslab/blocks/fanout.py +69 -0
- dissyslab/blocks/merge_synch.py +73 -0
- dissyslab/blocks/role.py +113 -0
- dissyslab/blocks/sink.py +77 -0
- dissyslab/blocks/source.py +119 -0
- dissyslab/blocks/split.py +110 -0
- dissyslab/blocks/transform.py +82 -0
- dissyslab/builder.py +222 -0
- dissyslab/cli.py +239 -0
- dissyslab/components/__init__.py +24 -0
- dissyslab/components/sinks/__init__.py +13 -0
- dissyslab/components/sinks/console_display.py +66 -0
- dissyslab/components/sinks/demo_email_alerter.py +98 -0
- dissyslab/components/sinks/discard.py +28 -0
- dissyslab/components/sinks/file_system.py +70 -0
- dissyslab/components/sinks/gmail_sink.py +135 -0
- dissyslab/components/sinks/intelligence_display.py +123 -0
- dissyslab/components/sinks/llm_builders.py +36 -0
- dissyslab/components/sinks/mcp_sink.py +204 -0
- dissyslab/components/sinks/photo_dashboard.py +222 -0
- dissyslab/components/sinks/replay_csv_in.py +72 -0
- dissyslab/components/sinks/rl_dashboard.py +165 -0
- dissyslab/components/sinks/sine_mixture_source.py +160 -0
- dissyslab/components/sinks/sink_jsonl_recorder.py +58 -0
- dissyslab/components/sinks/sink_list_collector.py +33 -0
- dissyslab/components/sinks/sink_simple_file.py +38 -0
- dissyslab/components/sinks/test_llm_enricher.py +129 -0
- dissyslab/components/sinks/test_webhook.py +14 -0
- dissyslab/components/sinks/webhook_sink.py +186 -0
- dissyslab/components/sources/__init__.py +16 -0
- dissyslab/components/sources/bluesky_jetstream_source.py +235 -0
- dissyslab/components/sources/calendar_source.py +290 -0
- dissyslab/components/sources/cartpole_source.py +238 -0
- dissyslab/components/sources/clock_source.py +96 -0
- dissyslab/components/sources/demo_bluesky_jetstream.py +401 -0
- dissyslab/components/sources/demo_job_source.py +42 -0
- dissyslab/components/sources/demo_rss_source.py +292 -0
- dissyslab/components/sources/file_source.py +273 -0
- dissyslab/components/sources/generated/__init__.py +0 -0
- dissyslab/components/sources/generated/stocks_source.py +45 -0
- dissyslab/components/sources/generated/weather_source.py +111 -0
- dissyslab/components/sources/gmail_source.py +249 -0
- dissyslab/components/sources/image_folder_source.py +147 -0
- dissyslab/components/sources/list_source.py +30 -0
- dissyslab/components/sources/mcp_source.py +272 -0
- dissyslab/components/sources/natural_numbers_source.py +40 -0
- dissyslab/components/sources/rss_normalizer.py +328 -0
- dissyslab/components/sources/rss_source.py +196 -0
- dissyslab/components/sources/test_bluesky.py +16 -0
- dissyslab/components/sources/web_scraper.py +463 -0
- dissyslab/components/transformers/__init__.py +17 -0
- dissyslab/components/transformers/ai_agent.py +154 -0
- dissyslab/components/transformers/composition_analyzer.py +199 -0
- dissyslab/components/transformers/demo_ai_agent.py +230 -0
- dissyslab/components/transformers/demo_jobs.py +141 -0
- dissyslab/components/transformers/demo_salary.py +122 -0
- dissyslab/components/transformers/demo_sentiment.py +102 -0
- dissyslab/components/transformers/demo_spam.py +140 -0
- dissyslab/components/transformers/demo_topic.py +160 -0
- dissyslab/components/transformers/demo_urgency.py +149 -0
- dissyslab/components/transformers/exposure_analyzer.py +177 -0
- dissyslab/components/transformers/learning_curve_analyzer.py +187 -0
- dissyslab/components/transformers/policy_analyzer.py +168 -0
- dissyslab/components/transformers/prompts.py +563 -0
- dissyslab/components/transformers/reward_analyzer.py +141 -0
- dissyslab/components/transformers/sharpness_analyzer.py +149 -0
- dissyslab/components/transformers/stateful_agent.py +170 -0
- dissyslab/composed_agent.py +212 -0
- dissyslab/core.py +320 -0
- dissyslab/network.py +759 -0
- dissyslab/office/__init__.py +17 -0
- dissyslab/office/make_network.py +368 -0
- dissyslab/office/make_office.py +296 -0
- dissyslab/office/office_compiler.py +230 -0
- dissyslab/office/utils.py +916 -0
- dissyslab/os_agent.py +186 -0
- dissyslab/py.typed +0 -0
- dissyslab/utils/__init__.py +31 -0
- dissyslab/utils/get_credentials.py +172 -0
- dissyslab/utils/visualize.py +491 -0
- dissyslab-1.0.2.dist-info/METADATA +188 -0
- dissyslab-1.0.2.dist-info/RECORD +89 -0
- dissyslab-1.0.2.dist-info/WHEEL +5 -0
- dissyslab-1.0.2.dist-info/entry_points.txt +2 -0
- dissyslab-1.0.2.dist-info/licenses/LICENSE +15 -0
- dissyslab-1.0.2.dist-info/top_level.txt +1 -0
dissyslab/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# dissyslab/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
DisSysLab - Distributed Systems Teaching Framework
|
|
4
|
+
|
|
5
|
+
Public API exports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dissyslab.core import Agent, ExceptionThread
|
|
9
|
+
from dissyslab.network import Network
|
|
10
|
+
from dissyslab.builder import network, PortReference
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
# Core
|
|
14
|
+
'Agent',
|
|
15
|
+
'ExceptionThread',
|
|
16
|
+
|
|
17
|
+
# Network
|
|
18
|
+
'Network',
|
|
19
|
+
'network',
|
|
20
|
+
|
|
21
|
+
# Builder
|
|
22
|
+
'PortReference',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
__version__ = '1.0.2'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# dissyslab/blocks/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Standard agent blocks for building distributed systems.
|
|
4
|
+
|
|
5
|
+
Available blocks:
|
|
6
|
+
- Source: Generate messages (no inputs)
|
|
7
|
+
- Transform: Process messages (one input, one output)
|
|
8
|
+
- Sink: Consume messages (one input, no outputs)
|
|
9
|
+
- Broadcast: Fanout - copy message to multiple outputs
|
|
10
|
+
- MergeAsynch: Fanin - merge multiple inputs into one stream
|
|
11
|
+
- Split: Content-based routing to multiple outputs
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dissyslab.blocks.source import Source
|
|
15
|
+
from dissyslab.blocks.transform import Transform
|
|
16
|
+
from dissyslab.blocks.sink import Sink
|
|
17
|
+
from dissyslab.blocks.fanout import Broadcast
|
|
18
|
+
from dissyslab.blocks.fanin import MergeAsynch
|
|
19
|
+
from dissyslab.blocks.merge_synch import MergeSynch
|
|
20
|
+
from dissyslab.blocks.split import Split
|
|
21
|
+
from dissyslab.blocks.role import Role
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Source",
|
|
25
|
+
"Transform",
|
|
26
|
+
"Sink",
|
|
27
|
+
"Broadcast",
|
|
28
|
+
"MergeAsynch",
|
|
29
|
+
"MergeSynch",
|
|
30
|
+
"Split",
|
|
31
|
+
"Role",
|
|
32
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# dissyslab/blocks/fanin.py
|
|
2
|
+
"""
|
|
3
|
+
Merge Agents: Combine multiple inputs into single output (fanin).
|
|
4
|
+
|
|
5
|
+
MergeAsynch is the recommended merge for most use cases. Termination is
|
|
6
|
+
signaled by os_agent via _Shutdown, handled transparently by recv().
|
|
7
|
+
No STOP coordination needed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
from dissyslab.core import Agent, _ShutdownSignal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MergeAsynch(Agent):
|
|
18
|
+
"""
|
|
19
|
+
MergeAsynch agent: combines multiple inputs (fanin, non-deterministic).
|
|
20
|
+
|
|
21
|
+
Multiple inputs, single output. Receives from whichever input has
|
|
22
|
+
a message available first. Fast but non-deterministic order.
|
|
23
|
+
|
|
24
|
+
**Ports:**
|
|
25
|
+
- Inports: ["in_0", "in_1", ..., "in_{n-1}"]
|
|
26
|
+
- Outports: ["out_"]
|
|
27
|
+
|
|
28
|
+
**Termination:**
|
|
29
|
+
Termination is detected by os_agent and signaled via _Shutdown on
|
|
30
|
+
each inport, handled transparently by recv(). No STOP coordination
|
|
31
|
+
needed — worker threads exit cleanly via _ShutdownSignal.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *, num_inputs: int, name: Optional[str] = None):
|
|
35
|
+
if num_inputs < 1:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"MergeAsynch requires at least 1 input, got {num_inputs}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
inports = [f"in_{i}" for i in range(num_inputs)]
|
|
41
|
+
super().__init__(name=name, inports=inports, outports=["out_"])
|
|
42
|
+
self.num_inputs = num_inputs
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def default_inport(self) -> Optional[str]:
|
|
46
|
+
"""No default input (multiple inputs - ambiguous)."""
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def default_outport(self) -> str:
|
|
51
|
+
"""Default output port for edge syntax."""
|
|
52
|
+
return "out_"
|
|
53
|
+
|
|
54
|
+
def _worker(self, port: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Worker thread for one input port.
|
|
57
|
+
Forwards messages to out_ until _ShutdownSignal is raised by recv().
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
while True:
|
|
61
|
+
msg = self.recv(port)
|
|
62
|
+
self.send(msg, "out_")
|
|
63
|
+
except _ShutdownSignal:
|
|
64
|
+
pass # clean exit — os_agent declared termination
|
|
65
|
+
|
|
66
|
+
def run(self) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Spawn one worker thread per input port and wait for all to finish.
|
|
69
|
+
|
|
70
|
+
Workers exit when recv() raises _ShutdownSignal on _Shutdown receipt.
|
|
71
|
+
"""
|
|
72
|
+
threads = []
|
|
73
|
+
for p in self.inports:
|
|
74
|
+
t = threading.Thread(
|
|
75
|
+
target=self._worker,
|
|
76
|
+
args=(p,),
|
|
77
|
+
name=f"merge_worker_{p}",
|
|
78
|
+
daemon=False
|
|
79
|
+
)
|
|
80
|
+
t.start()
|
|
81
|
+
threads.append(t)
|
|
82
|
+
|
|
83
|
+
for t in threads:
|
|
84
|
+
t.join()
|
|
85
|
+
|
|
86
|
+
def __repr__(self) -> str:
|
|
87
|
+
return f"<MergeAsynch name={self.name} inputs={self.num_inputs}>"
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return f"MergeAsynch({self.num_inputs} inputs)"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# dissyslab/blocks/fanout.py
|
|
2
|
+
"""
|
|
3
|
+
Broadcast Agent: Copies messages to multiple outputs (fanout).
|
|
4
|
+
|
|
5
|
+
Broadcast agents are automatically inserted by the framework when one
|
|
6
|
+
agent connects to multiple receivers. Termination is signaled by os_agent
|
|
7
|
+
via _Shutdown, handled transparently by recv().
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import copy
|
|
13
|
+
|
|
14
|
+
from dissyslab.core import Agent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Broadcast(Agent):
|
|
18
|
+
"""
|
|
19
|
+
Broadcast agent: copies messages to multiple outputs (fanout).
|
|
20
|
+
|
|
21
|
+
Single input, multiple outputs. Receives message and sends a deep
|
|
22
|
+
copy to each output port.
|
|
23
|
+
|
|
24
|
+
**Ports:**
|
|
25
|
+
- Inports: ["in_"]
|
|
26
|
+
- Outports: ["out_0", "out_1", ..., "out_{n-1}"]
|
|
27
|
+
|
|
28
|
+
**Termination:**
|
|
29
|
+
Termination is detected by os_agent and signaled via _Shutdown,
|
|
30
|
+
which recv() handles transparently by raising _ShutdownSignal.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *, num_outputs: int, name: Optional[str] = None):
|
|
34
|
+
if num_outputs < 1:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Broadcast requires at least 1 output, got {num_outputs}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
outports = [f"out_{i}" for i in range(num_outputs)]
|
|
40
|
+
super().__init__(name=name, inports=["in_"], outports=outports)
|
|
41
|
+
self.num_outputs = num_outputs
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def default_inport(self) -> str:
|
|
45
|
+
"""Default input port for edge syntax."""
|
|
46
|
+
return "in_"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def default_outport(self) -> Optional[str]:
|
|
50
|
+
"""No default output (multiple outputs - ambiguous)."""
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def run(self) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Broadcast messages to all outputs.
|
|
56
|
+
|
|
57
|
+
recv() intercepts _Shutdown and raises _ShutdownSignal,
|
|
58
|
+
which unwinds this loop cleanly.
|
|
59
|
+
"""
|
|
60
|
+
while True:
|
|
61
|
+
msg = self.recv("in_")
|
|
62
|
+
for i in range(self.num_outputs):
|
|
63
|
+
self.send(copy.deepcopy(msg), f"out_{i}")
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return f"<Broadcast name={self.name} outputs={self.num_outputs}>"
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
return f"Broadcast({self.num_outputs} outputs)"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# dissyslab/blocks/merge_synch.py
|
|
2
|
+
"""
|
|
3
|
+
Merge Agents: Combine multiple inputs into single output.
|
|
4
|
+
|
|
5
|
+
MergeSynch: Synchronous merge in round-robin order (use sparingly).
|
|
6
|
+
For most use cases, prefer MergeAsynch in fanin.py.
|
|
7
|
+
|
|
8
|
+
Termination is signaled by os_agent via _Shutdown, handled transparently
|
|
9
|
+
by recv(). No explicit STOP handling needed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from dissyslab.core import Agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MergeSynch(Agent):
|
|
19
|
+
"""
|
|
20
|
+
MergeSynch agent: synchronously combines multiple inputs in round-robin order.
|
|
21
|
+
|
|
22
|
+
**WARNING: Use only when deterministic ordering is required AND all inputs
|
|
23
|
+
produce messages at similar rates. For most use cases, prefer MergeAsynch.**
|
|
24
|
+
|
|
25
|
+
**Ports:**
|
|
26
|
+
- Inports: ["in_0", "in_1", ..., "in_{n-1}"]
|
|
27
|
+
- Outports: ["out_"]
|
|
28
|
+
|
|
29
|
+
**Termination:**
|
|
30
|
+
Termination is detected by os_agent and signaled via _Shutdown,
|
|
31
|
+
which recv() handles transparently by raising _ShutdownSignal.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *, num_inputs: int, name: Optional[str] = None):
|
|
35
|
+
if num_inputs < 1:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"MergeSynch requires at least 1 input, got {num_inputs}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
inports = [f"in_{i}" for i in range(num_inputs)]
|
|
41
|
+
super().__init__(name=name, inports=inports, outports=["out_"])
|
|
42
|
+
self.num_inputs = num_inputs
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def default_inport(self) -> Optional[str]:
|
|
46
|
+
"""No default input (multiple inputs - ambiguous)."""
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def default_outport(self) -> str:
|
|
51
|
+
"""Default output port for edge syntax."""
|
|
52
|
+
return "out_"
|
|
53
|
+
|
|
54
|
+
def run(self) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Synchronous merge loop: collect batches in round-robin order.
|
|
57
|
+
|
|
58
|
+
Collects one message from each input in order, sends as a list.
|
|
59
|
+
recv() intercepts _Shutdown and raises _ShutdownSignal,
|
|
60
|
+
which unwinds this loop cleanly.
|
|
61
|
+
"""
|
|
62
|
+
while True:
|
|
63
|
+
batch = []
|
|
64
|
+
for inport in self.inports:
|
|
65
|
+
msg = self.recv(inport)
|
|
66
|
+
batch.append(msg)
|
|
67
|
+
self.send(batch, "out_")
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return f"<MergeSynch name={self.name} inputs={self.num_inputs}>"
|
|
71
|
+
|
|
72
|
+
def __str__(self) -> str:
|
|
73
|
+
return f"MergeSynch({self.num_inputs} inputs)"
|
dissyslab/blocks/role.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# dissyslab/blocks/role.py
|
|
2
|
+
"""
|
|
3
|
+
Role Agent: Routes messages based on status strings.
|
|
4
|
+
|
|
5
|
+
A Role agent is a generalization of Split where:
|
|
6
|
+
- The function returns an arbitrary list of (message, status) pairs
|
|
7
|
+
- Status strings determine which outport each message goes to
|
|
8
|
+
- The number of output messages is independent of the number of outports
|
|
9
|
+
|
|
10
|
+
Termination is signaled by os_agent via _Shutdown, handled transparently
|
|
11
|
+
by recv(). No explicit STOP handling needed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from typing import Callable, Any, Optional, List, Tuple
|
|
16
|
+
import traceback
|
|
17
|
+
|
|
18
|
+
from dissyslab.core import Agent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Role(Agent):
|
|
22
|
+
"""
|
|
23
|
+
Role agent: routes messages based on status strings.
|
|
24
|
+
|
|
25
|
+
Single input, multiple outputs. The function returns either:
|
|
26
|
+
(1) an arbitrary list of (message, status) pairs, or
|
|
27
|
+
(2) a list of messages without explicit status values — coerced to "all", or
|
|
28
|
+
(3) a single message (not a list) — treated as [(message, "all")], or
|
|
29
|
+
(4) None — message is dropped.
|
|
30
|
+
|
|
31
|
+
**Ports:**
|
|
32
|
+
- Inports: ["in_"]
|
|
33
|
+
- Outports: ["out_0", "out_1", ..., "out_{n-1}"]
|
|
34
|
+
where n = len(statuses)
|
|
35
|
+
|
|
36
|
+
**Termination:**
|
|
37
|
+
Termination is detected by os_agent and signaled via _Shutdown,
|
|
38
|
+
which recv() handles transparently by raising _ShutdownSignal.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
fn: Callable[[Any], List[Tuple[Any, str]]],
|
|
45
|
+
statuses: List[str],
|
|
46
|
+
name: Optional[str] = None
|
|
47
|
+
):
|
|
48
|
+
if not callable(fn):
|
|
49
|
+
raise TypeError(
|
|
50
|
+
f"Role fn must be callable, got {type(fn).__name__}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if not statuses:
|
|
54
|
+
statuses = ["all"]
|
|
55
|
+
|
|
56
|
+
if len(set(statuses)) != len(statuses):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Role statuses must be unique, got duplicates: {statuses}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._status_to_port: dict = {
|
|
62
|
+
status: f"out_{i}" for i, status in enumerate(statuses)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
outports = [f"out_{i}" for i in range(len(statuses))]
|
|
66
|
+
|
|
67
|
+
super().__init__(name=name, inports=["in_"], outports=outports)
|
|
68
|
+
self._fn = fn
|
|
69
|
+
self.statuses = list(statuses)
|
|
70
|
+
|
|
71
|
+
def run(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Process messages and route by status.
|
|
74
|
+
|
|
75
|
+
recv() intercepts _Shutdown and raises _ShutdownSignal,
|
|
76
|
+
which unwinds this loop cleanly.
|
|
77
|
+
"""
|
|
78
|
+
while True:
|
|
79
|
+
msg = self.recv("in_")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
results = self._fn(msg)
|
|
83
|
+
|
|
84
|
+
if results is None:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if not isinstance(results, (list, tuple)):
|
|
88
|
+
results = [(results, "all")]
|
|
89
|
+
elif results and not isinstance(results[0], (list, tuple)):
|
|
90
|
+
results = [(item, "all") for item in results]
|
|
91
|
+
|
|
92
|
+
for out_msg, status in results:
|
|
93
|
+
if status not in self._status_to_port:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Role '{self.name}' returned undeclared status "
|
|
96
|
+
f"'{status}'. Declared statuses: {self.statuses}"
|
|
97
|
+
)
|
|
98
|
+
self.send(out_msg, self._status_to_port[status])
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"[Role '{self.name}'] Error in fn: {e}")
|
|
102
|
+
print(traceback.format_exc())
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
fn_name = getattr(self._fn, "__name__", repr(self._fn))
|
|
107
|
+
return (
|
|
108
|
+
f"<Role name={self.name} fn={fn_name} "
|
|
109
|
+
f"statuses={self.statuses}>"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return f"Role({self.statuses})"
|
dissyslab/blocks/sink.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# dissyslab/blocks/sink.py
|
|
2
|
+
"""
|
|
3
|
+
Sink Agent: Consumes messages for side effects.
|
|
4
|
+
|
|
5
|
+
Sinks have one input and no outputs. They are terminal nodes that call
|
|
6
|
+
fn(msg, **params) for each message to perform side effects like printing,
|
|
7
|
+
saving, or collecting. Termination is signaled by os_agent via _Shutdown,
|
|
8
|
+
which is handled transparently by recv().
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from typing import Any, Callable, Optional, Dict
|
|
13
|
+
import traceback
|
|
14
|
+
|
|
15
|
+
from dissyslab.core import Agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Sink(Agent):
|
|
19
|
+
"""
|
|
20
|
+
Sink agent: consumes messages for side effects.
|
|
21
|
+
|
|
22
|
+
Single input, no outputs. Terminal node that calls fn(msg, **params)
|
|
23
|
+
for each message. Used for actions like printing, saving, or sending.
|
|
24
|
+
|
|
25
|
+
**Ports:**
|
|
26
|
+
- Inports: ["in_"]
|
|
27
|
+
- Outports: [] (no outputs)
|
|
28
|
+
|
|
29
|
+
**Termination:**
|
|
30
|
+
Termination is detected by os_agent and signaled via _Shutdown,
|
|
31
|
+
which recv() handles transparently by raising _ShutdownSignal.
|
|
32
|
+
No explicit STOP handling needed.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
fn: Callable[..., None],
|
|
39
|
+
name: Optional[str] = None,
|
|
40
|
+
params: Optional[Dict[str, Any]] = None
|
|
41
|
+
):
|
|
42
|
+
if not callable(fn):
|
|
43
|
+
raise TypeError(
|
|
44
|
+
f"Sink fn must be callable, got {type(fn).__name__}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
super().__init__(name=name, inports=["in_"], outports=[])
|
|
48
|
+
self._fn = fn
|
|
49
|
+
self._params = params or {}
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def default_inport(self) -> str:
|
|
53
|
+
"""Default input port for edge syntax."""
|
|
54
|
+
return "in_"
|
|
55
|
+
|
|
56
|
+
def run(self) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Process messages until _Shutdown is received.
|
|
59
|
+
|
|
60
|
+
recv() intercepts _Shutdown and raises _ShutdownSignal,
|
|
61
|
+
which unwinds this loop cleanly.
|
|
62
|
+
"""
|
|
63
|
+
while True:
|
|
64
|
+
msg = self.recv("in_")
|
|
65
|
+
try:
|
|
66
|
+
self._fn(msg, **self._params)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"[Sink '{self.name}'] Error in fn: {e}")
|
|
69
|
+
print(traceback.format_exc())
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
fn_name = getattr(self._fn, "__name__", repr(self._fn))
|
|
74
|
+
return f"<Sink name={self.name} fn={fn_name}>"
|
|
75
|
+
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return "Sink"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# dissyslab/blocks/source.py
|
|
2
|
+
"""
|
|
3
|
+
Source Agent: Repeatedly calls a function to generate messages.
|
|
4
|
+
|
|
5
|
+
Sources have no inputs and generate data by calling fn() repeatedly until
|
|
6
|
+
it returns None. When exhausted, the source sends a termination message to
|
|
7
|
+
os_agent with its final sent counts. Termination is detected by os_agent —
|
|
8
|
+
sources do not send STOP signals.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import inspect
|
|
13
|
+
import traceback
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Callable, Optional
|
|
16
|
+
|
|
17
|
+
from dissyslab.core import Agent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Source(Agent):
|
|
21
|
+
"""
|
|
22
|
+
Source Agent: Repeatedly calls a function to generate messages.
|
|
23
|
+
|
|
24
|
+
**Ports:**
|
|
25
|
+
- Inports: [] (no inputs - sources generate data)
|
|
26
|
+
- Outports: ["out_"] (emits generated messages)
|
|
27
|
+
|
|
28
|
+
**Function Requirements:**
|
|
29
|
+
The fn callable must:
|
|
30
|
+
- Return a message (any type) on each call
|
|
31
|
+
- Return None when exhausted (no more messages)
|
|
32
|
+
- Maintain its own state between calls (if needed)
|
|
33
|
+
|
|
34
|
+
Generator functions are also accepted — Source wraps them automatically
|
|
35
|
+
so that each call to fn() advances the generator by one step.
|
|
36
|
+
|
|
37
|
+
**Termination:**
|
|
38
|
+
When fn() returns None, the source sends a termination message to
|
|
39
|
+
os_agent containing its final sent counts, then returns from run().
|
|
40
|
+
os_agent uses this message to detect when all sources are done.
|
|
41
|
+
|
|
42
|
+
**Optional Rate Limiting:**
|
|
43
|
+
The interval parameter adds a delay between messages:
|
|
44
|
+
- interval=0 (default): emit as fast as possible
|
|
45
|
+
- interval=1.0: emit one message per second
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
fn: Callable[[], Optional[Any]],
|
|
52
|
+
name: Optional[str] = None,
|
|
53
|
+
interval: float = 0
|
|
54
|
+
):
|
|
55
|
+
if not callable(fn):
|
|
56
|
+
raise TypeError(
|
|
57
|
+
"Source fn must be callable with signature: fn() -> Optional[message]"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
super().__init__(name=name, inports=[], outports=["out_"])
|
|
61
|
+
|
|
62
|
+
# If fn is a generator function, wrap it so each call returns one item.
|
|
63
|
+
if inspect.isgeneratorfunction(fn):
|
|
64
|
+
_gen = fn()
|
|
65
|
+
def fn(): return next(_gen, None)
|
|
66
|
+
|
|
67
|
+
self._fn = fn
|
|
68
|
+
self._interval = interval
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def default_outport(self) -> str:
|
|
72
|
+
"""Default output port for edge syntax."""
|
|
73
|
+
return "out_"
|
|
74
|
+
|
|
75
|
+
def _send_termination(self) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Send final sent counts to os_agent.
|
|
78
|
+
Called when source is exhausted or encounters an error.
|
|
79
|
+
"""
|
|
80
|
+
self.send_os({
|
|
81
|
+
"agent": self.name,
|
|
82
|
+
"sent": dict(self.sent),
|
|
83
|
+
"received": {},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
def run(self) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Main processing loop for the Source agent.
|
|
89
|
+
|
|
90
|
+
Repeatedly calls self._fn() to get messages and emits them.
|
|
91
|
+
When fn() returns None or an exception occurs, sends termination
|
|
92
|
+
message to os_agent and returns.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
while True:
|
|
96
|
+
msg = self._fn()
|
|
97
|
+
|
|
98
|
+
# None means the source is exhausted
|
|
99
|
+
if msg is None:
|
|
100
|
+
self._send_termination()
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
self.send(msg, "out_")
|
|
104
|
+
|
|
105
|
+
if self._interval > 0:
|
|
106
|
+
time.sleep(self._interval)
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"[Source '{self.name}'] Error in fn: {e}")
|
|
110
|
+
print(traceback.format_exc())
|
|
111
|
+
self._send_termination()
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
fn_name = getattr(self._fn, "__name__", repr(self._fn))
|
|
115
|
+
interval_str = f", interval={self._interval}" if self._interval > 0 else ""
|
|
116
|
+
return f"<Source name={self.name} fn={fn_name}{interval_str}>"
|
|
117
|
+
|
|
118
|
+
def __str__(self) -> str:
|
|
119
|
+
return "Source"
|