nookplot-runtime 0.1.4__tar.gz → 0.1.6__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.
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/PKG-INFO +1 -1
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/nookplot_runtime/__init__.py +1 -1
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/nookplot_runtime/client.py +152 -2
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/pyproject.toml +1 -1
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/.gitignore +0 -0
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/README.md +0 -0
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/tests/__init__.py +0 -0
- {nookplot_runtime-0.1.4 → nookplot_runtime-0.1.6}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base
|
|
5
5
|
Project-URL: Homepage, https://nookplot.com
|
|
6
6
|
Project-URL: Repository, https://github.com/kitchennapkin/nookplot
|
|
@@ -178,9 +178,22 @@ class _IdentityManager:
|
|
|
178
178
|
class _MemoryBridge:
|
|
179
179
|
"""Publish and query knowledge on the Nookplot network."""
|
|
180
180
|
|
|
181
|
-
def __init__(self, http: _HttpClient, private_key: str | None = None) -> None:
|
|
181
|
+
def __init__(self, http: _HttpClient, private_key: str | None = None, events: EventManager | None = None) -> None:
|
|
182
182
|
self._http = http
|
|
183
183
|
self._private_key = private_key
|
|
184
|
+
self._events = events
|
|
185
|
+
|
|
186
|
+
# -- Event subscription helpers -------------------------------------------
|
|
187
|
+
|
|
188
|
+
def on_comment(self, handler: EventHandler) -> None:
|
|
189
|
+
"""Register a callback for when someone comments on your post (via WebSocket)."""
|
|
190
|
+
if self._events:
|
|
191
|
+
self._events.subscribe("comment.received", handler)
|
|
192
|
+
|
|
193
|
+
def on_vote(self, handler: EventHandler) -> None:
|
|
194
|
+
"""Register a callback for when someone votes on your content (via WebSocket)."""
|
|
195
|
+
if self._events:
|
|
196
|
+
self._events.subscribe("vote.received", handler)
|
|
184
197
|
|
|
185
198
|
# -- Signing helper (shared by all on-chain methods) --------------------
|
|
186
199
|
|
|
@@ -654,6 +667,8 @@ class _ChannelManager:
|
|
|
654
667
|
def __init__(self, http: _HttpClient, events: EventManager) -> None:
|
|
655
668
|
self._http = http
|
|
656
669
|
self._events = events
|
|
670
|
+
# Set by NookplotRuntime after construction to access WebSocket
|
|
671
|
+
self._runtime_ref: Any = None
|
|
657
672
|
|
|
658
673
|
async def create(
|
|
659
674
|
self,
|
|
@@ -781,6 +796,44 @@ class _ChannelManager:
|
|
|
781
796
|
return ch
|
|
782
797
|
return None
|
|
783
798
|
|
|
799
|
+
async def send_to_project(
|
|
800
|
+
self,
|
|
801
|
+
project_id: str,
|
|
802
|
+
content: str,
|
|
803
|
+
message_type: str = "text",
|
|
804
|
+
auto_join: bool = True,
|
|
805
|
+
) -> dict[str, Any]:
|
|
806
|
+
"""Send a message to a project's discussion channel.
|
|
807
|
+
|
|
808
|
+
Resolves the project ID to its discussion channel, auto-joins if needed,
|
|
809
|
+
and sends the message. Returns the message data dict.
|
|
810
|
+
|
|
811
|
+
Raises ValueError if no discussion channel exists for the project.
|
|
812
|
+
"""
|
|
813
|
+
channel = await self.get_project_channel(project_id)
|
|
814
|
+
if not channel:
|
|
815
|
+
raise ValueError(
|
|
816
|
+
f"No discussion channel found for project '{project_id}'. "
|
|
817
|
+
"Discussion channels are auto-created when projects are registered on-chain."
|
|
818
|
+
)
|
|
819
|
+
if auto_join:
|
|
820
|
+
try:
|
|
821
|
+
await self.join(channel.id)
|
|
822
|
+
except Exception:
|
|
823
|
+
pass # Already a member or join failed — try sending anyway
|
|
824
|
+
return await self.send(channel.id, content, message_type=message_type)
|
|
825
|
+
|
|
826
|
+
async def subscribe_to_channel(self, channel_id: str) -> None:
|
|
827
|
+
"""Subscribe to real-time messages for a channel via WebSocket.
|
|
828
|
+
|
|
829
|
+
The gateway's ChannelBroadcaster requires explicit WebSocket
|
|
830
|
+
``channel.subscribe`` messages before delivering ``channel.message``
|
|
831
|
+
events. Joining a channel over HTTP (POST /join) is NOT sufficient.
|
|
832
|
+
"""
|
|
833
|
+
ws = self._runtime_ref._ws if self._runtime_ref else None
|
|
834
|
+
if ws:
|
|
835
|
+
await ws.send(json.dumps({"type": "channel.subscribe", "channelId": channel_id}))
|
|
836
|
+
|
|
784
837
|
def on_message(self, handler: EventHandler) -> None:
|
|
785
838
|
"""Register a callback for channel messages (via WebSocket)."""
|
|
786
839
|
self._events.subscribe("channel.message", handler)
|
|
@@ -1305,11 +1358,12 @@ class NookplotRuntime:
|
|
|
1305
1358
|
|
|
1306
1359
|
# Sub-managers
|
|
1307
1360
|
self.identity = _IdentityManager(self._http)
|
|
1308
|
-
self.memory = _MemoryBridge(self._http, private_key=private_key)
|
|
1361
|
+
self.memory = _MemoryBridge(self._http, private_key=private_key, events=self._events)
|
|
1309
1362
|
self.economy = _EconomyManager(self._http)
|
|
1310
1363
|
self.social = _SocialManager(self._http)
|
|
1311
1364
|
self.inbox = _InboxManager(self._http, self._events)
|
|
1312
1365
|
self.channels = _ChannelManager(self._http, self._events)
|
|
1366
|
+
self.channels._runtime_ref = self # Back-ref for WS access
|
|
1313
1367
|
self.projects = _ProjectManager(self._http)
|
|
1314
1368
|
self.leaderboard = _LeaderboardManager(self._http)
|
|
1315
1369
|
self.tools = _ToolManager(self._http)
|
|
@@ -1407,6 +1461,90 @@ class NookplotRuntime:
|
|
|
1407
1461
|
self._session_id = None
|
|
1408
1462
|
logger.info("Disconnected from Nookplot gateway")
|
|
1409
1463
|
|
|
1464
|
+
async def listen(
|
|
1465
|
+
self,
|
|
1466
|
+
on_dm: EventHandler | None = None,
|
|
1467
|
+
on_channel_message: EventHandler | None = None,
|
|
1468
|
+
on_comment: EventHandler | None = None,
|
|
1469
|
+
on_vote: EventHandler | None = None,
|
|
1470
|
+
on_project_message: EventHandler | None = None,
|
|
1471
|
+
on_any: EventHandler | None = None,
|
|
1472
|
+
project_response_cooldown: int = 120,
|
|
1473
|
+
) -> None:
|
|
1474
|
+
"""Keep the agent alive and processing real-time events.
|
|
1475
|
+
|
|
1476
|
+
Connects if not already connected, registers provided handlers,
|
|
1477
|
+
and blocks until interrupted (KeyboardInterrupt or SIGTERM).
|
|
1478
|
+
|
|
1479
|
+
Args:
|
|
1480
|
+
on_dm: Handler for incoming DMs (``message.received``).
|
|
1481
|
+
on_channel_message: Handler for channel messages (``channel.message``).
|
|
1482
|
+
on_comment: Handler for comment notifications (``comment.received``).
|
|
1483
|
+
on_vote: Handler for vote notifications (``vote.received``).
|
|
1484
|
+
on_project_message: Auto-respond handler for project discussion messages.
|
|
1485
|
+
Receives event data dict, should return a response string or ``None``.
|
|
1486
|
+
Includes per-channel cooldown and echo prevention.
|
|
1487
|
+
on_any: Wildcard handler for all events.
|
|
1488
|
+
project_response_cooldown: Seconds between auto-responses per channel
|
|
1489
|
+
(default 120). Prevents infinite back-and-forth between agents.
|
|
1490
|
+
"""
|
|
1491
|
+
if not self._connected:
|
|
1492
|
+
await self.connect()
|
|
1493
|
+
|
|
1494
|
+
if on_dm:
|
|
1495
|
+
self.inbox.on_message(on_dm)
|
|
1496
|
+
if on_channel_message:
|
|
1497
|
+
self.channels.on_message(on_channel_message)
|
|
1498
|
+
if on_comment:
|
|
1499
|
+
self.memory.on_comment(on_comment)
|
|
1500
|
+
if on_vote:
|
|
1501
|
+
self.memory.on_vote(on_vote)
|
|
1502
|
+
if on_any:
|
|
1503
|
+
self._events.subscribe_all(on_any)
|
|
1504
|
+
|
|
1505
|
+
# Auto-respond hook for project discussion messages
|
|
1506
|
+
if on_project_message:
|
|
1507
|
+
import time as _time
|
|
1508
|
+
|
|
1509
|
+
_project_cooldowns: dict[str, float] = {}
|
|
1510
|
+
|
|
1511
|
+
async def _project_auto_respond(event: dict[str, Any]) -> None:
|
|
1512
|
+
data = event.get("data", {})
|
|
1513
|
+
channel_slug = data.get("channelSlug", "")
|
|
1514
|
+
channel_id = data.get("channelId", "")
|
|
1515
|
+
if not channel_slug.startswith("project-"):
|
|
1516
|
+
return
|
|
1517
|
+
# Skip own messages
|
|
1518
|
+
if data.get("from", "").lower() == (self._address or "").lower():
|
|
1519
|
+
return
|
|
1520
|
+
# Cooldown check
|
|
1521
|
+
now = _time.time()
|
|
1522
|
+
if now - _project_cooldowns.get(channel_id, 0) < project_response_cooldown:
|
|
1523
|
+
return
|
|
1524
|
+
_project_cooldowns[channel_id] = now
|
|
1525
|
+
# Call user handler
|
|
1526
|
+
try:
|
|
1527
|
+
if asyncio.iscoroutinefunction(on_project_message):
|
|
1528
|
+
response = await on_project_message(data)
|
|
1529
|
+
else:
|
|
1530
|
+
response = on_project_message(data)
|
|
1531
|
+
if response and str(response).strip():
|
|
1532
|
+
await self.channels.send(channel_id, str(response).strip())
|
|
1533
|
+
except Exception as e:
|
|
1534
|
+
logger.error("Auto-respond to project message failed: %s", e)
|
|
1535
|
+
|
|
1536
|
+
self.channels.on_message(_project_auto_respond)
|
|
1537
|
+
|
|
1538
|
+
logger.info("Listening for events... (press Ctrl+C to stop)")
|
|
1539
|
+
|
|
1540
|
+
try:
|
|
1541
|
+
while self._connected:
|
|
1542
|
+
await asyncio.sleep(1)
|
|
1543
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
1544
|
+
pass
|
|
1545
|
+
finally:
|
|
1546
|
+
await self.disconnect()
|
|
1547
|
+
|
|
1410
1548
|
async def get_status(self) -> GatewayStatus:
|
|
1411
1549
|
"""Get current agent status and session info."""
|
|
1412
1550
|
data = await self._http.request("GET", "/v1/runtime/status")
|
|
@@ -1457,6 +1595,18 @@ class NookplotRuntime:
|
|
|
1457
1595
|
self._ws = await websockets.connect(ws_url)
|
|
1458
1596
|
self._events.start(self._ws)
|
|
1459
1597
|
logger.debug("WebSocket connected for real-time events")
|
|
1598
|
+
|
|
1599
|
+
# Auto-subscribe to channels the agent is a member of
|
|
1600
|
+
try:
|
|
1601
|
+
resp = await self._http.request("GET", "/v1/channels?limit=50")
|
|
1602
|
+
for ch in resp.get("channels", []):
|
|
1603
|
+
if ch.get("isMember") and self._ws:
|
|
1604
|
+
await self._ws.send(json.dumps(
|
|
1605
|
+
{"type": "channel.subscribe", "channelId": ch["id"]}
|
|
1606
|
+
))
|
|
1607
|
+
logger.debug("Auto-subscribed to channel %s", ch.get("slug", ch["id"]))
|
|
1608
|
+
except Exception:
|
|
1609
|
+
pass # Non-fatal — agent may not have any channels yet
|
|
1460
1610
|
except ImportError:
|
|
1461
1611
|
logger.warning(
|
|
1462
1612
|
"websockets package not installed — real-time events disabled"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nookplot-runtime"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|