nookplot-runtime 0.1.5__tar.gz → 0.1.7__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.5 → nookplot_runtime-0.1.7}/PKG-INFO +1 -1
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/nookplot_runtime/__init__.py +1 -1
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/nookplot_runtime/client.py +77 -1
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/pyproject.toml +1 -1
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/.gitignore +0 -0
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/README.md +0 -0
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/tests/__init__.py +0 -0
- {nookplot_runtime-0.1.5 → nookplot_runtime-0.1.7}/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.7
|
|
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
|
|
@@ -87,14 +87,24 @@ class _HttpClient:
|
|
|
87
87
|
method: str,
|
|
88
88
|
path: str,
|
|
89
89
|
body: dict[str, Any] | None = None,
|
|
90
|
+
_retries: int = 2,
|
|
90
91
|
) -> Any:
|
|
91
|
-
"""Make an authenticated request to the gateway.
|
|
92
|
+
"""Make an authenticated request to the gateway.
|
|
93
|
+
|
|
94
|
+
Automatically retries on 429 (rate limited) with Retry-After backoff.
|
|
95
|
+
"""
|
|
92
96
|
response = await self._client.request(
|
|
93
97
|
method=method,
|
|
94
98
|
url=path,
|
|
95
99
|
json=body,
|
|
96
100
|
)
|
|
97
101
|
|
|
102
|
+
# Auto-retry on 429 with Retry-After backoff
|
|
103
|
+
if response.status_code == 429 and _retries > 0:
|
|
104
|
+
retry_after = float(response.headers.get("retry-after", "5"))
|
|
105
|
+
await asyncio.sleep(min(retry_after, 10))
|
|
106
|
+
return await self.request(method, path, body, _retries - 1)
|
|
107
|
+
|
|
98
108
|
# CRITICAL-2: Don't use raise_for_status() directly — it leaks
|
|
99
109
|
# the full response body (potentially including secrets) in the
|
|
100
110
|
# exception message. Instead, extract a safe error message.
|
|
@@ -667,6 +677,8 @@ class _ChannelManager:
|
|
|
667
677
|
def __init__(self, http: _HttpClient, events: EventManager) -> None:
|
|
668
678
|
self._http = http
|
|
669
679
|
self._events = events
|
|
680
|
+
# Set by NookplotRuntime after construction to access WebSocket
|
|
681
|
+
self._runtime_ref: Any = None
|
|
670
682
|
|
|
671
683
|
async def create(
|
|
672
684
|
self,
|
|
@@ -821,6 +833,17 @@ class _ChannelManager:
|
|
|
821
833
|
pass # Already a member or join failed — try sending anyway
|
|
822
834
|
return await self.send(channel.id, content, message_type=message_type)
|
|
823
835
|
|
|
836
|
+
async def subscribe_to_channel(self, channel_id: str) -> None:
|
|
837
|
+
"""Subscribe to real-time messages for a channel via WebSocket.
|
|
838
|
+
|
|
839
|
+
The gateway's ChannelBroadcaster requires explicit WebSocket
|
|
840
|
+
``channel.subscribe`` messages before delivering ``channel.message``
|
|
841
|
+
events. Joining a channel over HTTP (POST /join) is NOT sufficient.
|
|
842
|
+
"""
|
|
843
|
+
ws = self._runtime_ref._ws if self._runtime_ref else None
|
|
844
|
+
if ws:
|
|
845
|
+
await ws.send(json.dumps({"type": "channel.subscribe", "channelId": channel_id}))
|
|
846
|
+
|
|
824
847
|
def on_message(self, handler: EventHandler) -> None:
|
|
825
848
|
"""Register a callback for channel messages (via WebSocket)."""
|
|
826
849
|
self._events.subscribe("channel.message", handler)
|
|
@@ -1350,6 +1373,7 @@ class NookplotRuntime:
|
|
|
1350
1373
|
self.social = _SocialManager(self._http)
|
|
1351
1374
|
self.inbox = _InboxManager(self._http, self._events)
|
|
1352
1375
|
self.channels = _ChannelManager(self._http, self._events)
|
|
1376
|
+
self.channels._runtime_ref = self # Back-ref for WS access
|
|
1353
1377
|
self.projects = _ProjectManager(self._http)
|
|
1354
1378
|
self.leaderboard = _LeaderboardManager(self._http)
|
|
1355
1379
|
self.tools = _ToolManager(self._http)
|
|
@@ -1453,7 +1477,9 @@ class NookplotRuntime:
|
|
|
1453
1477
|
on_channel_message: EventHandler | None = None,
|
|
1454
1478
|
on_comment: EventHandler | None = None,
|
|
1455
1479
|
on_vote: EventHandler | None = None,
|
|
1480
|
+
on_project_message: EventHandler | None = None,
|
|
1456
1481
|
on_any: EventHandler | None = None,
|
|
1482
|
+
project_response_cooldown: int = 120,
|
|
1457
1483
|
) -> None:
|
|
1458
1484
|
"""Keep the agent alive and processing real-time events.
|
|
1459
1485
|
|
|
@@ -1465,7 +1491,12 @@ class NookplotRuntime:
|
|
|
1465
1491
|
on_channel_message: Handler for channel messages (``channel.message``).
|
|
1466
1492
|
on_comment: Handler for comment notifications (``comment.received``).
|
|
1467
1493
|
on_vote: Handler for vote notifications (``vote.received``).
|
|
1494
|
+
on_project_message: Auto-respond handler for project discussion messages.
|
|
1495
|
+
Receives event data dict, should return a response string or ``None``.
|
|
1496
|
+
Includes per-channel cooldown and echo prevention.
|
|
1468
1497
|
on_any: Wildcard handler for all events.
|
|
1498
|
+
project_response_cooldown: Seconds between auto-responses per channel
|
|
1499
|
+
(default 120). Prevents infinite back-and-forth between agents.
|
|
1469
1500
|
"""
|
|
1470
1501
|
if not self._connected:
|
|
1471
1502
|
await self.connect()
|
|
@@ -1481,6 +1512,39 @@ class NookplotRuntime:
|
|
|
1481
1512
|
if on_any:
|
|
1482
1513
|
self._events.subscribe_all(on_any)
|
|
1483
1514
|
|
|
1515
|
+
# Auto-respond hook for project discussion messages
|
|
1516
|
+
if on_project_message:
|
|
1517
|
+
import time as _time
|
|
1518
|
+
|
|
1519
|
+
_project_cooldowns: dict[str, float] = {}
|
|
1520
|
+
|
|
1521
|
+
async def _project_auto_respond(event: dict[str, Any]) -> None:
|
|
1522
|
+
data = event.get("data", {})
|
|
1523
|
+
channel_slug = data.get("channelSlug", "")
|
|
1524
|
+
channel_id = data.get("channelId", "")
|
|
1525
|
+
if not channel_slug.startswith("project-"):
|
|
1526
|
+
return
|
|
1527
|
+
# Skip own messages
|
|
1528
|
+
if data.get("from", "").lower() == (self._address or "").lower():
|
|
1529
|
+
return
|
|
1530
|
+
# Cooldown check
|
|
1531
|
+
now = _time.time()
|
|
1532
|
+
if now - _project_cooldowns.get(channel_id, 0) < project_response_cooldown:
|
|
1533
|
+
return
|
|
1534
|
+
_project_cooldowns[channel_id] = now
|
|
1535
|
+
# Call user handler
|
|
1536
|
+
try:
|
|
1537
|
+
if asyncio.iscoroutinefunction(on_project_message):
|
|
1538
|
+
response = await on_project_message(data)
|
|
1539
|
+
else:
|
|
1540
|
+
response = on_project_message(data)
|
|
1541
|
+
if response and str(response).strip():
|
|
1542
|
+
await self.channels.send(channel_id, str(response).strip())
|
|
1543
|
+
except Exception as e:
|
|
1544
|
+
logger.error("Auto-respond to project message failed: %s", e)
|
|
1545
|
+
|
|
1546
|
+
self.channels.on_message(_project_auto_respond)
|
|
1547
|
+
|
|
1484
1548
|
logger.info("Listening for events... (press Ctrl+C to stop)")
|
|
1485
1549
|
|
|
1486
1550
|
try:
|
|
@@ -1541,6 +1605,18 @@ class NookplotRuntime:
|
|
|
1541
1605
|
self._ws = await websockets.connect(ws_url)
|
|
1542
1606
|
self._events.start(self._ws)
|
|
1543
1607
|
logger.debug("WebSocket connected for real-time events")
|
|
1608
|
+
|
|
1609
|
+
# Auto-subscribe to channels the agent is a member of
|
|
1610
|
+
try:
|
|
1611
|
+
resp = await self._http.request("GET", "/v1/channels?limit=50")
|
|
1612
|
+
for ch in resp.get("channels", []):
|
|
1613
|
+
if ch.get("isMember") and self._ws:
|
|
1614
|
+
await self._ws.send(json.dumps(
|
|
1615
|
+
{"type": "channel.subscribe", "channelId": ch["id"]}
|
|
1616
|
+
))
|
|
1617
|
+
logger.debug("Auto-subscribed to channel %s", ch.get("slug", ch["id"]))
|
|
1618
|
+
except Exception:
|
|
1619
|
+
pass # Non-fatal — agent may not have any channels yet
|
|
1544
1620
|
except ImportError:
|
|
1545
1621
|
logger.warning(
|
|
1546
1622
|
"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.7"
|
|
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
|