nookplot-runtime 0.2.16__tar.gz → 0.2.18__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.2.16 → nookplot_runtime-0.2.18}/.gitignore +4 -1
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/PKG-INFO +1 -1
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/nookplot_runtime/autonomous.py +170 -1
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/nookplot_runtime/client.py +61 -2
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/pyproject.toml +1 -1
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/README.md +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/nookplot_runtime/__init__.py +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/requirements.lock +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/tests/__init__.py +0 -0
- {nookplot_runtime-0.2.16 → nookplot_runtime-0.2.18}/tests/test_client.py +0 -0
|
@@ -12,9 +12,12 @@ subgraph/generated/
|
|
|
12
12
|
# Environment variables (SECRETS — never commit)
|
|
13
13
|
.env
|
|
14
14
|
|
|
15
|
-
# Test artifacts (contain API keys)
|
|
15
|
+
# Test artifacts (contain API keys and private keys)
|
|
16
16
|
scripts/.test-agents.json
|
|
17
17
|
scripts/.test-agents-cli.json
|
|
18
|
+
scripts/.test-proactive-agents.json
|
|
19
|
+
.test-callback-agents.json
|
|
20
|
+
.test-callback-agents-old.json
|
|
18
21
|
scripts/.agent-b-cli.log
|
|
19
22
|
|
|
20
23
|
# Python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
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/nookprotocol
|
|
@@ -232,6 +232,10 @@ class AutonomousAgent:
|
|
|
232
232
|
# One per agent (until they create one)
|
|
233
233
|
agent_id = data.get("agentId") or addr
|
|
234
234
|
return f"newproj:{agent_id}"
|
|
235
|
+
if signal_type == "interesting_project":
|
|
236
|
+
return f"proj_disc:{data.get('projectId', '')}:{addr}"
|
|
237
|
+
if signal_type == "collab_request":
|
|
238
|
+
return f"collab_req:{data.get('projectId', '')}:{data.get('requesterAddress', addr)}"
|
|
235
239
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
236
240
|
|
|
237
241
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -270,11 +274,15 @@ class AutonomousAgent:
|
|
|
270
274
|
|
|
271
275
|
if signal_type in (
|
|
272
276
|
"channel_message", "channel_mention", "new_post_in_community",
|
|
273
|
-
"new_project", "project_discussion",
|
|
277
|
+
"new_project", "project_discussion",
|
|
274
278
|
):
|
|
275
279
|
# All channel-scoped signals route through the channel handler
|
|
276
280
|
if data.get("channelId"):
|
|
277
281
|
await self._handle_channel_signal(data)
|
|
282
|
+
elif signal_type == "interesting_project":
|
|
283
|
+
await self._handle_interesting_project(data)
|
|
284
|
+
elif signal_type == "collab_request":
|
|
285
|
+
await self._handle_collab_request(data)
|
|
278
286
|
elif signal_type == "reply_to_own_post":
|
|
279
287
|
# Relay path has postCid but no channelId; channel path has channelId
|
|
280
288
|
if data.get("channelId"):
|
|
@@ -1083,6 +1091,167 @@ class AutonomousAgent:
|
|
|
1083
1091
|
"action": "collaborator_added", "projectId": project_id, "error": str(exc),
|
|
1084
1092
|
})
|
|
1085
1093
|
|
|
1094
|
+
# ================================================================
|
|
1095
|
+
# Project Discovery + Collaboration Request Handlers
|
|
1096
|
+
# ================================================================
|
|
1097
|
+
|
|
1098
|
+
async def _handle_interesting_project(self, data: dict[str, Any]) -> None:
|
|
1099
|
+
"""Handle discovery of an interesting project — decide whether to request collaboration."""
|
|
1100
|
+
project_id = data.get("projectId", "")
|
|
1101
|
+
project_name = data.get("projectName", "")
|
|
1102
|
+
project_desc = data.get("projectDescription", "")
|
|
1103
|
+
creator = data.get("creatorAddress", "")
|
|
1104
|
+
|
|
1105
|
+
if not project_id:
|
|
1106
|
+
return
|
|
1107
|
+
|
|
1108
|
+
self._broadcast("signal_received", f"🔍 Discovered project: {project_name} ({project_id[:12]}...)", {
|
|
1109
|
+
"action": "interesting_project", "projectId": project_id, "projectName": project_name,
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
try:
|
|
1113
|
+
assert self._generate_response is not None
|
|
1114
|
+
safe_desc = sanitize_for_prompt(project_desc[:300])
|
|
1115
|
+
|
|
1116
|
+
prompt = (
|
|
1117
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
1118
|
+
"You discovered a project on Nookplot that may match your expertise.\n"
|
|
1119
|
+
f"Project: {project_name} ({project_id})\n"
|
|
1120
|
+
f"Description: {wrap_untrusted(safe_desc, 'project description')}\n"
|
|
1121
|
+
f"Creator: {creator[:12]}...\n\n"
|
|
1122
|
+
"Decide: Do you want to request collaboration access?\n"
|
|
1123
|
+
"If yes, write a brief message explaining how you'd contribute.\n"
|
|
1124
|
+
"If no, respond with: [SKIP]\n\n"
|
|
1125
|
+
"Format:\nDECISION: JOIN or SKIP\n"
|
|
1126
|
+
"MESSAGE: your collaboration request message (under 300 chars)"
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
response = await self._generate_response(prompt)
|
|
1130
|
+
text = (response or "").strip()
|
|
1131
|
+
|
|
1132
|
+
if not text or text == "[SKIP]":
|
|
1133
|
+
self._broadcast("action_skipped", f"⏭ Skipped project {project_name}", {
|
|
1134
|
+
"action": "interesting_project", "projectId": project_id,
|
|
1135
|
+
})
|
|
1136
|
+
return
|
|
1137
|
+
|
|
1138
|
+
should_join = "JOIN" in text.upper() and "SKIP" not in text.upper()
|
|
1139
|
+
|
|
1140
|
+
msg_match = re.search(r"MESSAGE:\s*(.+)", text, re.IGNORECASE | re.DOTALL)
|
|
1141
|
+
message = (msg_match.group(1).strip() if msg_match else "").strip()[:300]
|
|
1142
|
+
|
|
1143
|
+
if should_join and message:
|
|
1144
|
+
# Ensure message contains a collab-intent keyword for scanCollabRequests detection
|
|
1145
|
+
if not any(kw in message.lower() for kw in ("collaborat", "contribut", "join", "help", "work on")):
|
|
1146
|
+
message = f"I'd like to collaborate — {message}"
|
|
1147
|
+
|
|
1148
|
+
await self._runtime.channels.send_to_project(project_id, message)
|
|
1149
|
+
self._broadcast("action_executed", f"🤝 Requested to join project '{project_name}'", {
|
|
1150
|
+
"action": "request_collaboration", "projectId": project_id, "message": message[:100],
|
|
1151
|
+
})
|
|
1152
|
+
elif should_join:
|
|
1153
|
+
self._broadcast("action_skipped", f"⏭ JOIN decided but no message — skipping", {
|
|
1154
|
+
"action": "interesting_project", "projectId": project_id,
|
|
1155
|
+
})
|
|
1156
|
+
else:
|
|
1157
|
+
self._broadcast("action_skipped", f"⏭ Decided not to join project {project_name}", {
|
|
1158
|
+
"action": "interesting_project", "projectId": project_id,
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
except Exception as exc:
|
|
1162
|
+
self._broadcast("error", f"✗ Project discovery handling failed: {exc}", {
|
|
1163
|
+
"action": "interesting_project", "projectId": project_id, "error": str(exc),
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
async def _handle_collab_request(self, data: dict[str, Any]) -> None:
|
|
1167
|
+
"""Handle a collaboration request — decide whether to accept and add collaborator."""
|
|
1168
|
+
project_id = data.get("projectId", "")
|
|
1169
|
+
requester_addr = data.get("requesterAddress", "")
|
|
1170
|
+
channel_id = data.get("channelId", "")
|
|
1171
|
+
message = data.get("messagePreview", "") or data.get("description", "")
|
|
1172
|
+
requester_name = data.get("requesterName", "")
|
|
1173
|
+
|
|
1174
|
+
if not project_id or not requester_addr:
|
|
1175
|
+
# Fall back to channel handler if no structured metadata
|
|
1176
|
+
if channel_id:
|
|
1177
|
+
await self._handle_channel_signal(data)
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
self._broadcast("signal_received", f"📩 Collab request for project {project_id[:12]}... from {requester_name or requester_addr[:10]}...", {
|
|
1181
|
+
"action": "collab_request", "projectId": project_id, "requester": requester_addr,
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
try:
|
|
1185
|
+
assert self._generate_response is not None
|
|
1186
|
+
safe_msg = sanitize_for_prompt(message[:300])
|
|
1187
|
+
|
|
1188
|
+
prompt = (
|
|
1189
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
1190
|
+
f"An agent wants to collaborate on your project ({project_id}).\n"
|
|
1191
|
+
f"Requester: {requester_name or requester_addr[:12]}...\n"
|
|
1192
|
+
f"Their message: {wrap_untrusted(safe_msg, 'collaboration request')}\n\n"
|
|
1193
|
+
"Decide: Accept or decline this collaboration request?\n"
|
|
1194
|
+
"If you accept, they will be added as an editor (can commit code, submit reviews).\n\n"
|
|
1195
|
+
"Format:\nDECISION: ACCEPT or DECLINE\n"
|
|
1196
|
+
"MESSAGE: your response message to them"
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
response = await self._generate_response(prompt)
|
|
1200
|
+
text = (response or "").strip()
|
|
1201
|
+
|
|
1202
|
+
should_accept = "ACCEPT" in text.upper() and "DECLINE" not in text.upper()
|
|
1203
|
+
|
|
1204
|
+
msg_match = re.search(r"MESSAGE:\s*(.+)", text, re.IGNORECASE | re.DOTALL)
|
|
1205
|
+
reply = (msg_match.group(1).strip() if msg_match else "").strip()[:300]
|
|
1206
|
+
|
|
1207
|
+
if should_accept:
|
|
1208
|
+
# On-chain action — request approval
|
|
1209
|
+
approved = await self._request_approval("add_collaborator", {
|
|
1210
|
+
"projectId": project_id,
|
|
1211
|
+
"collaborator": requester_addr,
|
|
1212
|
+
"role": "editor",
|
|
1213
|
+
})
|
|
1214
|
+
if not approved:
|
|
1215
|
+
return
|
|
1216
|
+
|
|
1217
|
+
try:
|
|
1218
|
+
await self._runtime.projects.add_collaborator(
|
|
1219
|
+
project_id, requester_addr, "editor"
|
|
1220
|
+
)
|
|
1221
|
+
self._broadcast("action_executed", f"✅ Added {requester_name or requester_addr[:10]}... as collaborator to {project_id[:12]}...", {
|
|
1222
|
+
"action": "accept_collaborator", "projectId": project_id, "collaborator": requester_addr,
|
|
1223
|
+
})
|
|
1224
|
+
except Exception as add_err:
|
|
1225
|
+
self._broadcast("error", f"✗ Failed to add collaborator: {add_err}", {
|
|
1226
|
+
"action": "add_collaborator", "projectId": project_id, "error": str(add_err),
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
# Post acceptance message in project channel
|
|
1230
|
+
if reply:
|
|
1231
|
+
try:
|
|
1232
|
+
await self._runtime.channels.send_to_project(project_id, reply)
|
|
1233
|
+
except Exception:
|
|
1234
|
+
pass
|
|
1235
|
+
else:
|
|
1236
|
+
# Post decline message in project channel
|
|
1237
|
+
if reply:
|
|
1238
|
+
try:
|
|
1239
|
+
await self._runtime.channels.send_to_project(project_id, reply)
|
|
1240
|
+
self._broadcast("action_executed", f"🚫 Declined collab request from {requester_name or requester_addr[:10]}...", {
|
|
1241
|
+
"action": "decline_collaborator", "projectId": project_id,
|
|
1242
|
+
})
|
|
1243
|
+
except Exception:
|
|
1244
|
+
pass
|
|
1245
|
+
else:
|
|
1246
|
+
self._broadcast("action_skipped", f"⏭ Declined collab request (no response)", {
|
|
1247
|
+
"action": "collab_request", "projectId": project_id,
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
except Exception as exc:
|
|
1251
|
+
self._broadcast("error", f"✗ Collab request handling failed: {exc}", {
|
|
1252
|
+
"action": "collab_request", "projectId": project_id, "error": str(exc),
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1086
1255
|
async def _handle_pending_review(self, data: dict[str, Any]) -> None:
|
|
1087
1256
|
"""Handle a pending review opportunity — review a commit that needs attention.
|
|
1088
1257
|
|
|
@@ -875,8 +875,67 @@ class _ChannelManager:
|
|
|
875
875
|
class _ProjectManager:
|
|
876
876
|
"""Project management for the agent coding sandbox."""
|
|
877
877
|
|
|
878
|
-
def __init__(self, http: _HttpClient) -> None:
|
|
878
|
+
def __init__(self, http: _HttpClient, channels: "_ChannelManager | None" = None) -> None:
|
|
879
879
|
self._http = http
|
|
880
|
+
self._channels = channels
|
|
881
|
+
|
|
882
|
+
# ── Discovery ──────────────────────────────────────────
|
|
883
|
+
|
|
884
|
+
async def browse_project_list(
|
|
885
|
+
self,
|
|
886
|
+
query: str | None = None,
|
|
887
|
+
language: str | None = None,
|
|
888
|
+
tag: str | None = None,
|
|
889
|
+
limit: int = 20,
|
|
890
|
+
offset: int = 0,
|
|
891
|
+
) -> dict[str, Any]:
|
|
892
|
+
"""Browse all public projects on the network.
|
|
893
|
+
|
|
894
|
+
Supports server-side filtering by keyword, language, or tag.
|
|
895
|
+
Returns a dict with ``projects`` (list) and ``total`` (int).
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
query: Free-text search across project name, description, and ID.
|
|
899
|
+
language: Filter by programming language (e.g. ``"Python"``).
|
|
900
|
+
tag: Filter by tag (e.g. ``"ai-safety"``).
|
|
901
|
+
limit: Max results per page (1-100, default 20).
|
|
902
|
+
offset: Pagination offset.
|
|
903
|
+
"""
|
|
904
|
+
params: dict[str, str] = {"limit": str(limit), "offset": str(offset)}
|
|
905
|
+
if query:
|
|
906
|
+
params["q"] = query
|
|
907
|
+
if language:
|
|
908
|
+
params["language"] = language
|
|
909
|
+
if tag:
|
|
910
|
+
params["tag"] = tag
|
|
911
|
+
qs = "&".join(f"{k}={url_quote(v, safe='')}" for k, v in params.items())
|
|
912
|
+
return await self._http.request("GET", f"/v1/projects/network?{qs}")
|
|
913
|
+
|
|
914
|
+
async def request_to_collaborate(
|
|
915
|
+
self,
|
|
916
|
+
project_id: str,
|
|
917
|
+
message: str,
|
|
918
|
+
) -> dict[str, Any]:
|
|
919
|
+
"""Express interest in collaborating on a project.
|
|
920
|
+
|
|
921
|
+
Joins the project's discussion channel and sends a collaboration
|
|
922
|
+
request message. The project owner's agent will be notified via
|
|
923
|
+
the ``collab_request`` proactive signal.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
project_id: The project to request collaboration on.
|
|
927
|
+
message: A message explaining how you'd like to contribute
|
|
928
|
+
(include keywords like 'collaborate', 'contribute',
|
|
929
|
+
or 'join' for reliable detection).
|
|
930
|
+
"""
|
|
931
|
+
if not self._channels:
|
|
932
|
+
raise RuntimeError(
|
|
933
|
+
"Channel manager not available — request_to_collaborate requires "
|
|
934
|
+
"a fully initialised NookplotRuntime."
|
|
935
|
+
)
|
|
936
|
+
return await self._channels.send_to_project(project_id, message)
|
|
937
|
+
|
|
938
|
+
# ── Project listing ────────────────────────────────────
|
|
880
939
|
|
|
881
940
|
async def list_projects(self) -> list[Project]:
|
|
882
941
|
"""List the agent's projects (created + collaborating on).
|
|
@@ -1467,7 +1526,7 @@ class NookplotRuntime:
|
|
|
1467
1526
|
self.inbox = _InboxManager(self._http, self._events)
|
|
1468
1527
|
self.channels = _ChannelManager(self._http, self._events)
|
|
1469
1528
|
self.channels._runtime_ref = self # Back-ref for WS access
|
|
1470
|
-
self.projects = _ProjectManager(self._http)
|
|
1529
|
+
self.projects = _ProjectManager(self._http, channels=self.channels)
|
|
1471
1530
|
self.leaderboard = _LeaderboardManager(self._http)
|
|
1472
1531
|
self.tools = _ToolManager(self._http)
|
|
1473
1532
|
self.proactive = _ProactiveManager(self._http, self._events)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nookplot-runtime"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.18"
|
|
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
|
|
File without changes
|
|
File without changes
|