sbp-client 0.1.0__tar.gz

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.
@@ -0,0 +1,38 @@
1
+ # Dependencies
2
+ node_modules/
3
+ dist/
4
+ *.egg-info/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+
10
+ # Build outputs
11
+ build/
12
+ *.pyc
13
+ *.pyo
14
+ *.so
15
+
16
+ # IDE
17
+ .idea/
18
+ .vscode/
19
+ *.swp
20
+ *.swo
21
+ .DS_Store
22
+
23
+ # Environment
24
+ .env
25
+ .env.local
26
+ *.local
27
+
28
+ # Logs
29
+ *.log
30
+ npm-debug.log*
31
+
32
+ # Test coverage
33
+ coverage/
34
+ htmlcov/
35
+ .coverage
36
+
37
+ # Package locks (keep package-lock.json for npm)
38
+ # poetry.lock
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: sbp-client
3
+ Version: 0.1.0
4
+ Summary: Stigmergic Blackboard Protocol - Python Client SDK
5
+ Project-URL: Homepage, https://github.com/sbp-protocol/sbp
6
+ Project-URL: Documentation, https://sbp.dev
7
+ Project-URL: Repository, https://github.com/sbp-protocol/sbp
8
+ Author: SBP Contributors
9
+ License: MIT
10
+ Keywords: blackboard,coordination,multi-agent,sbp,stigmergy
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.26.0
21
+ Requires-Dist: pydantic>=2.5.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # SBP Python Client
30
+
31
+ Python SDK for the Stigmergic Blackboard Protocol.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install sbp-client
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from sbp import SbpClient, ThresholdCondition
43
+
44
+ # Connect to the blackboard
45
+ with SbpClient("http://localhost:3000") as client:
46
+ # Emit a pheromone
47
+ client.emit(
48
+ trail="market.signals",
49
+ type="volatility",
50
+ intensity=0.8,
51
+ payload={"symbol": "BTC-USD", "vix": 45.2},
52
+ )
53
+
54
+ # Sniff the environment
55
+ result = client.sniff(trails=["market.signals"])
56
+ for p in result.pheromones:
57
+ print(f"{p.trail}/{p.type}: {p.current_intensity:.2f}")
58
+ ```
59
+
60
+ ## Local Mode (No Server Required)
61
+
62
+ You can run SBP entirely in-memory within a single process. This is useful for testing, simulations, or simple multi-agent scripts where you don't want to manage a separate server process.
63
+
64
+ ```python
65
+ from sbp import SbpClient, SbpAgent
66
+
67
+ # Client in local mode
68
+ with SbpClient(local=True) as client:
69
+ client.emit("local.test", "signal", 0.9)
70
+
71
+ # Agent in local mode
72
+ # Note: In a single process, all local=True instances share the same blackboard state.
73
+ agent = SbpAgent("my-agent", local=True)
74
+
75
+ @agent.when("local.test", "signal", value=0.5)
76
+ async def handle(trigger):
77
+ print("Received signal locally!")
78
+ ```
79
+
80
+ ## Async Usage
81
+
82
+ ```python
83
+ import asyncio
84
+ from sbp import AsyncSbpClient, ThresholdCondition
85
+
86
+ async def main():
87
+ async with AsyncSbpClient() as client:
88
+ # Emit
89
+ await client.emit("signals", "event", 0.7)
90
+
91
+ # Register a scent (trigger condition)
92
+ await client.register_scent(
93
+ "my-scent",
94
+ condition=ThresholdCondition(
95
+ trail="signals",
96
+ signal_type="event",
97
+ aggregation="max",
98
+ operator=">=",
99
+ value=0.5,
100
+ ),
101
+ )
102
+
103
+ # Subscribe to triggers via WebSocket
104
+ async def on_trigger(trigger):
105
+ print(f"Triggered! {trigger.scent_id}")
106
+
107
+ await client.subscribe("my-scent", on_trigger)
108
+
109
+ asyncio.run(main())
110
+ ```
111
+
112
+ ## Declarative Agent Framework
113
+
114
+ ```python
115
+ from sbp import SbpAgent, TriggerPayload, run_agent
116
+
117
+ agent = SbpAgent("my-agent", "http://localhost:3000")
118
+
119
+ @agent.when("tasks", "new_task", operator=">=", value=0.5)
120
+ async def handle_task(trigger: TriggerPayload):
121
+ print(f"Got task: {trigger.context_pheromones}")
122
+ await agent.emit("tasks", "completed", 1.0)
123
+
124
+ run_agent(agent)
125
+ ```
126
+
127
+ ## API Reference
128
+
129
+ ### SbpClient / AsyncSbpClient
130
+
131
+ | Method | Description |
132
+ |--------|-------------|
133
+ | `emit(trail, type, intensity, ...)` | Deposit a pheromone |
134
+ | `sniff(trails, types, ...)` | Read environment state |
135
+ | `register_scent(scent_id, condition, ...)` | Register a trigger |
136
+ | `deregister_scent(scent_id)` | Remove a trigger |
137
+ | `subscribe(scent_id, handler)` | Listen for triggers (async only) |
138
+ | `inspect(include)` | Get blackboard metadata |
139
+ | `evaporate(...)` | Force cleanup |
140
+
141
+ ### Condition Types
142
+
143
+ ```python
144
+ # Simple threshold
145
+ ThresholdCondition(
146
+ trail="market.signals",
147
+ signal_type="volatility",
148
+ aggregation="max", # sum, max, avg, count, any
149
+ operator=">=", # >=, >, <=, <, ==, !=
150
+ value=0.7,
151
+ )
152
+
153
+ # Composite (AND/OR/NOT)
154
+ CompositeCondition(
155
+ operator="and",
156
+ conditions=[condition1, condition2],
157
+ )
158
+
159
+ # Rate-based
160
+ RateCondition(
161
+ trail="events",
162
+ signal_type="click",
163
+ metric="emissions_per_second",
164
+ window_ms=10000,
165
+ operator=">=",
166
+ value=5.0,
167
+ )
168
+ ```
169
+
170
+ ### Decay Models
171
+
172
+ ```python
173
+ from sbp.types import exponential_decay, linear_decay, immortal
174
+
175
+ # Exponential (default) - half-life in milliseconds
176
+ decay = exponential_decay(half_life_ms=300000) # 5 minutes
177
+
178
+ # Linear - rate per millisecond
179
+ decay = linear_decay(rate_per_ms=0.0001)
180
+
181
+ # Immortal - never decays
182
+ decay = immortal()
183
+ ```
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,159 @@
1
+ # SBP Python Client
2
+
3
+ Python SDK for the Stigmergic Blackboard Protocol.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install sbp-client
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from sbp import SbpClient, ThresholdCondition
15
+
16
+ # Connect to the blackboard
17
+ with SbpClient("http://localhost:3000") as client:
18
+ # Emit a pheromone
19
+ client.emit(
20
+ trail="market.signals",
21
+ type="volatility",
22
+ intensity=0.8,
23
+ payload={"symbol": "BTC-USD", "vix": 45.2},
24
+ )
25
+
26
+ # Sniff the environment
27
+ result = client.sniff(trails=["market.signals"])
28
+ for p in result.pheromones:
29
+ print(f"{p.trail}/{p.type}: {p.current_intensity:.2f}")
30
+ ```
31
+
32
+ ## Local Mode (No Server Required)
33
+
34
+ You can run SBP entirely in-memory within a single process. This is useful for testing, simulations, or simple multi-agent scripts where you don't want to manage a separate server process.
35
+
36
+ ```python
37
+ from sbp import SbpClient, SbpAgent
38
+
39
+ # Client in local mode
40
+ with SbpClient(local=True) as client:
41
+ client.emit("local.test", "signal", 0.9)
42
+
43
+ # Agent in local mode
44
+ # Note: In a single process, all local=True instances share the same blackboard state.
45
+ agent = SbpAgent("my-agent", local=True)
46
+
47
+ @agent.when("local.test", "signal", value=0.5)
48
+ async def handle(trigger):
49
+ print("Received signal locally!")
50
+ ```
51
+
52
+ ## Async Usage
53
+
54
+ ```python
55
+ import asyncio
56
+ from sbp import AsyncSbpClient, ThresholdCondition
57
+
58
+ async def main():
59
+ async with AsyncSbpClient() as client:
60
+ # Emit
61
+ await client.emit("signals", "event", 0.7)
62
+
63
+ # Register a scent (trigger condition)
64
+ await client.register_scent(
65
+ "my-scent",
66
+ condition=ThresholdCondition(
67
+ trail="signals",
68
+ signal_type="event",
69
+ aggregation="max",
70
+ operator=">=",
71
+ value=0.5,
72
+ ),
73
+ )
74
+
75
+ # Subscribe to triggers via WebSocket
76
+ async def on_trigger(trigger):
77
+ print(f"Triggered! {trigger.scent_id}")
78
+
79
+ await client.subscribe("my-scent", on_trigger)
80
+
81
+ asyncio.run(main())
82
+ ```
83
+
84
+ ## Declarative Agent Framework
85
+
86
+ ```python
87
+ from sbp import SbpAgent, TriggerPayload, run_agent
88
+
89
+ agent = SbpAgent("my-agent", "http://localhost:3000")
90
+
91
+ @agent.when("tasks", "new_task", operator=">=", value=0.5)
92
+ async def handle_task(trigger: TriggerPayload):
93
+ print(f"Got task: {trigger.context_pheromones}")
94
+ await agent.emit("tasks", "completed", 1.0)
95
+
96
+ run_agent(agent)
97
+ ```
98
+
99
+ ## API Reference
100
+
101
+ ### SbpClient / AsyncSbpClient
102
+
103
+ | Method | Description |
104
+ |--------|-------------|
105
+ | `emit(trail, type, intensity, ...)` | Deposit a pheromone |
106
+ | `sniff(trails, types, ...)` | Read environment state |
107
+ | `register_scent(scent_id, condition, ...)` | Register a trigger |
108
+ | `deregister_scent(scent_id)` | Remove a trigger |
109
+ | `subscribe(scent_id, handler)` | Listen for triggers (async only) |
110
+ | `inspect(include)` | Get blackboard metadata |
111
+ | `evaporate(...)` | Force cleanup |
112
+
113
+ ### Condition Types
114
+
115
+ ```python
116
+ # Simple threshold
117
+ ThresholdCondition(
118
+ trail="market.signals",
119
+ signal_type="volatility",
120
+ aggregation="max", # sum, max, avg, count, any
121
+ operator=">=", # >=, >, <=, <, ==, !=
122
+ value=0.7,
123
+ )
124
+
125
+ # Composite (AND/OR/NOT)
126
+ CompositeCondition(
127
+ operator="and",
128
+ conditions=[condition1, condition2],
129
+ )
130
+
131
+ # Rate-based
132
+ RateCondition(
133
+ trail="events",
134
+ signal_type="click",
135
+ metric="emissions_per_second",
136
+ window_ms=10000,
137
+ operator=">=",
138
+ value=5.0,
139
+ )
140
+ ```
141
+
142
+ ### Decay Models
143
+
144
+ ```python
145
+ from sbp.types import exponential_decay, linear_decay, immortal
146
+
147
+ # Exponential (default) - half-life in milliseconds
148
+ decay = exponential_decay(half_life_ms=300000) # 5 minutes
149
+
150
+ # Linear - rate per millisecond
151
+ decay = linear_decay(rate_per_ms=0.0001)
152
+
153
+ # Immortal - never decays
154
+ decay = immortal()
155
+ ```
156
+
157
+ ## License
158
+
159
+ MIT
@@ -0,0 +1,202 @@
1
+ """
2
+ SBP Example: Market Volatility Crisis Detection
3
+
4
+ This example demonstrates multi-agent coordination using SBP.
5
+ Three agents work together without direct communication:
6
+
7
+ 1. Market Analyzer - Emits volatility signals based on market data
8
+ 2. Order Monitor - Emits signals about large orders
9
+ 3. Crisis Handler - Wakes up when volatility AND large orders exceed thresholds
10
+
11
+ Run:
12
+ # Terminal 1: Start the SBP server
13
+ cd packages/server && npm run dev
14
+
15
+ # Terminal 2: Run this example
16
+ cd packages/client-python && python -m examples.market_crisis
17
+ """
18
+
19
+ import asyncio
20
+ import random
21
+ from sbp import (
22
+ AsyncSbpClient,
23
+ ThresholdCondition,
24
+ CompositeCondition,
25
+ TriggerPayload,
26
+ exponential_decay,
27
+ )
28
+
29
+
30
+ async def market_analyzer(client: AsyncSbpClient) -> None:
31
+ """Simulates a market analyzer that emits volatility signals"""
32
+ print("[Market Analyzer] Starting...")
33
+
34
+ while True:
35
+ # Simulate market volatility (random for demo)
36
+ volatility = random.uniform(0.2, 0.95)
37
+
38
+ await client.emit(
39
+ trail="market.signals",
40
+ type="volatility",
41
+ intensity=volatility,
42
+ decay=exponential_decay(half_life_ms=30000), # 30 second half-life
43
+ payload={
44
+ "symbol": "BTC-USD",
45
+ "vix_equivalent": volatility * 100,
46
+ "timestamp": asyncio.get_event_loop().time(),
47
+ },
48
+ tags=["crypto", "realtime"],
49
+ )
50
+
51
+ print(f"[Market Analyzer] Emitted volatility: {volatility:.2f}")
52
+ await asyncio.sleep(2)
53
+
54
+
55
+ async def order_monitor(client: AsyncSbpClient) -> None:
56
+ """Simulates an order monitor that emits large order signals"""
57
+ print("[Order Monitor] Starting...")
58
+
59
+ while True:
60
+ # Simulate detecting large orders (random for demo)
61
+ if random.random() > 0.5:
62
+ size = random.randint(500000, 5000000)
63
+ side = random.choice(["buy", "sell"])
64
+
65
+ await client.emit(
66
+ trail="market.orders",
67
+ type="large_order",
68
+ intensity=min(1.0, size / 5000000), # Normalize by max size
69
+ decay=exponential_decay(half_life_ms=60000), # 1 minute half-life
70
+ payload={
71
+ "symbol": "BTC-USD",
72
+ "size_usd": size,
73
+ "side": side,
74
+ },
75
+ tags=["crypto", "whale"],
76
+ )
77
+
78
+ print(f"[Order Monitor] Detected {side} order: ${size:,}")
79
+
80
+ await asyncio.sleep(3)
81
+
82
+
83
+ async def crisis_handler(client: AsyncSbpClient) -> None:
84
+ """Agent that triggers when crisis conditions are met"""
85
+ print("[Crisis Handler] Registering scent...")
86
+
87
+ # Define the crisis condition:
88
+ # High volatility AND multiple large orders
89
+ condition = CompositeCondition(
90
+ operator="and",
91
+ conditions=[
92
+ ThresholdCondition(
93
+ trail="market.signals",
94
+ signal_type="volatility",
95
+ aggregation="max",
96
+ operator=">=",
97
+ value=0.7, # 70% volatility threshold
98
+ ),
99
+ ThresholdCondition(
100
+ trail="market.orders",
101
+ signal_type="large_order",
102
+ aggregation="count",
103
+ operator=">=",
104
+ value=2, # At least 2 large orders
105
+ ),
106
+ ],
107
+ )
108
+
109
+ # Register the scent
110
+ await client.register_scent(
111
+ scent_id="crisis-detector",
112
+ condition=condition,
113
+ cooldown_ms=10000, # 10 second cooldown between triggers
114
+ activation_payload={"severity": "high"},
115
+ context_trails=["market.signals", "market.orders"],
116
+ )
117
+
118
+ # Subscribe to triggers
119
+ async def on_crisis(trigger: TriggerPayload) -> None:
120
+ print("\n" + "=" * 60)
121
+ print("🚨 CRISIS DETECTED!")
122
+ print(f" Triggered at: {trigger.triggered_at}")
123
+ print(f" Context pheromones: {len(trigger.context_pheromones)}")
124
+
125
+ for p in trigger.context_pheromones:
126
+ print(f" - {p.trail}/{p.type}: {p.current_intensity:.2f}")
127
+ print(f" Payload: {p.payload}")
128
+
129
+ # Crisis handler could emit its own response
130
+ await client.emit(
131
+ trail="system.alerts",
132
+ type="crisis_response",
133
+ intensity=1.0,
134
+ payload={
135
+ "action": "reduce_exposure",
136
+ "triggered_by": trigger.scent_id,
137
+ },
138
+ )
139
+ print(" ✓ Emitted crisis response signal")
140
+ print("=" * 60 + "\n")
141
+
142
+ await client.subscribe("crisis-detector", on_crisis)
143
+ print("[Crisis Handler] Listening for crisis conditions...")
144
+
145
+ # Keep running
146
+ while True:
147
+ await asyncio.sleep(1)
148
+
149
+
150
+ async def environment_monitor(client: AsyncSbpClient) -> None:
151
+ """Periodically displays environment state"""
152
+ while True:
153
+ await asyncio.sleep(5)
154
+
155
+ result = await client.sniff(
156
+ trails=["market.signals", "market.orders", "system.alerts"],
157
+ min_intensity=0.1,
158
+ )
159
+
160
+ print("\n--- Environment State ---")
161
+ for key, agg in result.aggregates.items():
162
+ print(f" {key}: count={agg.count}, max={agg.max_intensity:.2f}")
163
+ print("-------------------------\n")
164
+
165
+
166
+ async def main() -> None:
167
+ """Run all agents concurrently"""
168
+ print("=" * 60)
169
+ print("SBP Market Crisis Detection Demo")
170
+ print("=" * 60)
171
+ print()
172
+
173
+ # Create clients for each agent
174
+ analyzer_client = AsyncSbpClient(agent_id="market-analyzer")
175
+ order_client = AsyncSbpClient(agent_id="order-monitor")
176
+ crisis_client = AsyncSbpClient(agent_id="crisis-handler")
177
+ monitor_client = AsyncSbpClient(agent_id="env-monitor")
178
+
179
+ await analyzer_client.connect()
180
+ await order_client.connect()
181
+ await crisis_client.connect()
182
+ await monitor_client.connect()
183
+
184
+ try:
185
+ # Run all agents concurrently
186
+ await asyncio.gather(
187
+ market_analyzer(analyzer_client),
188
+ order_monitor(order_client),
189
+ crisis_handler(crisis_client),
190
+ environment_monitor(monitor_client),
191
+ )
192
+ except KeyboardInterrupt:
193
+ print("\nShutting down...")
194
+ finally:
195
+ await analyzer_client.close()
196
+ await order_client.close()
197
+ await crisis_client.close()
198
+ await monitor_client.close()
199
+
200
+
201
+ if __name__ == "__main__":
202
+ asyncio.run(main())
@@ -0,0 +1,112 @@
1
+ """
2
+ SBP Example: Simple Agent using the Declarative Framework
3
+
4
+ This example shows how to build an agent using the @agent.when decorator.
5
+
6
+ Run:
7
+ # Terminal 1: Start the SBP server
8
+ cd packages/server && npm run dev
9
+
10
+ # Terminal 2: Run this agent
11
+ cd packages/client-python && python -m examples.simple_agent
12
+
13
+ # Terminal 3: Emit test signals
14
+ curl -X POST http://localhost:3000/emit \
15
+ -H "Content-Type: application/json" \
16
+ -d '{"trail": "tasks", "type": "new_task", "intensity": 0.8, "payload": {"name": "Process data"}}'
17
+ """
18
+
19
+ import asyncio
20
+ from sbp import SbpAgent, TriggerPayload, ThresholdCondition, run_agent
21
+
22
+
23
+ # Create the agent
24
+ agent = SbpAgent("task-worker", "http://localhost:3000")
25
+
26
+
27
+ # Simple threshold trigger using @when decorator
28
+ @agent.when("tasks", "new_task", operator=">=", value=0.5, cooldown_ms=5000)
29
+ async def handle_new_task(trigger: TriggerPayload) -> None:
30
+ """Handle new task signals"""
31
+ print(f"\n📋 New task received!")
32
+
33
+ for p in trigger.context_pheromones:
34
+ task_name = p.payload.get("name", "Unknown")
35
+ print(f" Task: {task_name}")
36
+ print(f" Intensity: {p.current_intensity:.2f}")
37
+
38
+ # Emit a "processing" signal
39
+ await agent.emit(
40
+ "tasks",
41
+ "processing",
42
+ intensity=0.7,
43
+ payload={"worker": agent.agent_id},
44
+ )
45
+ print(" ✓ Emitted processing signal")
46
+
47
+ # Simulate work
48
+ await asyncio.sleep(2)
49
+
50
+ # Emit completion signal
51
+ await agent.emit(
52
+ "tasks",
53
+ "completed",
54
+ intensity=1.0,
55
+ payload={
56
+ "worker": agent.agent_id,
57
+ "original_task": trigger.context_pheromones[0].payload if trigger.context_pheromones else {},
58
+ },
59
+ )
60
+ print(" ✓ Task completed!")
61
+
62
+
63
+ # More complex condition using @on_scent decorator
64
+ @agent.on_scent(
65
+ "high-load-detector",
66
+ condition=ThresholdCondition(
67
+ trail="tasks",
68
+ signal_type="new_task",
69
+ aggregation="count",
70
+ operator=">=",
71
+ value=5,
72
+ ),
73
+ cooldown_ms=30000, # Only trigger once per 30 seconds
74
+ context_trails=["tasks"],
75
+ )
76
+ async def handle_high_load(trigger: TriggerPayload) -> None:
77
+ """Triggered when task queue is getting backed up"""
78
+ pending_count = len([
79
+ p for p in trigger.context_pheromones
80
+ if p.type == "new_task"
81
+ ])
82
+
83
+ print(f"\n⚠️ High load detected! {pending_count} tasks pending")
84
+
85
+ # Emit alert
86
+ await agent.emit(
87
+ "system.alerts",
88
+ "high_load",
89
+ intensity=0.9,
90
+ payload={
91
+ "pending_tasks": pending_count,
92
+ "worker": agent.agent_id,
93
+ },
94
+ )
95
+
96
+
97
+ if __name__ == "__main__":
98
+ print("=" * 50)
99
+ print("SBP Task Worker Agent")
100
+ print("=" * 50)
101
+ print()
102
+ print("Listening for task signals...")
103
+ print("Send a task with:")
104
+ print()
105
+ print(' curl -X POST http://localhost:3000/emit \\')
106
+ print(' -H "Content-Type: application/json" \\')
107
+ print(" -d '{\"trail\": \"tasks\", \"type\": \"new_task\", \"intensity\": 0.8}'")
108
+ print()
109
+ print("Press Ctrl+C to stop")
110
+ print()
111
+
112
+ run_agent(agent)