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.
- sbp_client-0.1.0/.gitignore +38 -0
- sbp_client-0.1.0/PKG-INFO +187 -0
- sbp_client-0.1.0/README.md +159 -0
- sbp_client-0.1.0/examples/market_crisis.py +202 -0
- sbp_client-0.1.0/examples/simple_agent.py +112 -0
- sbp_client-0.1.0/pyproject.toml +61 -0
- sbp_client-0.1.0/src/sbp/__init__.py +98 -0
- sbp_client-0.1.0/src/sbp/agent.py +271 -0
- sbp_client-0.1.0/src/sbp/blackboard.py +336 -0
- sbp_client-0.1.0/src/sbp/client.py +533 -0
- sbp_client-0.1.0/src/sbp/conditions.py +169 -0
- sbp_client-0.1.0/src/sbp/decay.py +39 -0
- sbp_client-0.1.0/src/sbp/evaluator.py +145 -0
- sbp_client-0.1.0/src/sbp/types.py +311 -0
- sbp_client-0.1.0/tests/test_types.py +111 -0
|
@@ -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)
|