nookplot-runtime 0.5.13__tar.gz → 0.5.15__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.5.13 → nookplot_runtime-0.5.15}/.gitignore +1 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/PKG-INFO +1 -1
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/nookplot_runtime/autonomous.py +483 -5
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/pyproject.toml +1 -1
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/README.md +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/nookplot_runtime/__init__.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/nookplot_runtime/client.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/requirements.lock +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/tests/__init__.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.15}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.15
|
|
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
|
|
@@ -244,6 +244,45 @@ class AutonomousAgent:
|
|
|
244
244
|
return f"team_inv:{data.get('invitationId', '')}"
|
|
245
245
|
if signal_type in ("team_invitation_accepted", "team_invitation_declined"):
|
|
246
246
|
return f"team_resp:{data.get('invitationId', '')}"
|
|
247
|
+
# Bounty application/submission signals
|
|
248
|
+
if signal_type == "bounty_application_submitted":
|
|
249
|
+
return f"bounty_app:{data.get('applicationId', '')}"
|
|
250
|
+
if signal_type == "bounty_application_approved":
|
|
251
|
+
return f"bounty_app_approve:{data.get('applicationId', '')}"
|
|
252
|
+
if signal_type == "bounty_application_rejected":
|
|
253
|
+
return f"bounty_app_reject:{data.get('applicationId', '')}"
|
|
254
|
+
if signal_type == "bounty_work_submitted":
|
|
255
|
+
return f"bounty_work:{data.get('submissionId', '')}"
|
|
256
|
+
if signal_type == "bounty_submission_selected":
|
|
257
|
+
return f"bounty_sel:{data.get('submissionId', '')}"
|
|
258
|
+
if signal_type == "bounty_submission_not_selected":
|
|
259
|
+
return f"bounty_notsel:{data.get('bountyId', '')}:{addr}"
|
|
260
|
+
# On-chain bounty lifecycle signals
|
|
261
|
+
if signal_type == "bounty_claimed":
|
|
262
|
+
return f"bounty_claimed:{data.get('bountyId', '')}"
|
|
263
|
+
if signal_type == "bounty_work_approved":
|
|
264
|
+
return f"bounty_work_approved:{data.get('bountyId', '')}"
|
|
265
|
+
if signal_type == "bounty_disputed":
|
|
266
|
+
return f"bounty_disputed:{data.get('bountyId', '')}:{addr}"
|
|
267
|
+
if signal_type == "bounty_cancelled":
|
|
268
|
+
return f"bounty_cancelled:{data.get('bountyId', '')}"
|
|
269
|
+
if signal_type == "bounty_claimer_approved":
|
|
270
|
+
return f"bounty_claimer_approved:{data.get('bountyId', '')}:{addr}"
|
|
271
|
+
# Marketplace signals
|
|
272
|
+
if signal_type == "agreement_created":
|
|
273
|
+
return f"agreement_created:{data.get('agreementId', '')}"
|
|
274
|
+
if signal_type == "work_delivered":
|
|
275
|
+
return f"work_delivered:{data.get('agreementId', '')}"
|
|
276
|
+
if signal_type == "agreement_settled":
|
|
277
|
+
return f"agreement_settled:{data.get('agreementId', '')}"
|
|
278
|
+
if signal_type == "agreement_disputed":
|
|
279
|
+
return f"agreement_disputed:{data.get('agreementId', '')}"
|
|
280
|
+
if signal_type == "agreement_cancelled":
|
|
281
|
+
return f"agreement_cancelled:{data.get('agreementId', '')}"
|
|
282
|
+
if signal_type == "revision_requested":
|
|
283
|
+
return f"revision_requested:{data.get('agreementId', '')}"
|
|
284
|
+
if signal_type == "review_received":
|
|
285
|
+
return f"review_received:{data.get('agreementId', '')}"
|
|
247
286
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
248
287
|
|
|
249
288
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -342,6 +381,61 @@ class AutonomousAgent:
|
|
|
342
381
|
self._broadcast("action_skipped", f"📋 Team invitation {signal_type}: {data.get('inviteeAddress', '?')}", {
|
|
343
382
|
"signalType": signal_type, "invitationId": data.get("invitationId"),
|
|
344
383
|
})
|
|
384
|
+
# ── Bounty application/submission signals ──
|
|
385
|
+
elif signal_type == "bounty_application_submitted":
|
|
386
|
+
await self._handle_bounty_application_submitted(data)
|
|
387
|
+
elif signal_type == "bounty_application_approved":
|
|
388
|
+
await self._handle_bounty_application_approved(data)
|
|
389
|
+
elif signal_type == "bounty_application_rejected":
|
|
390
|
+
self._broadcast("action_skipped", f"Bounty application rejected for bounty #{data.get('bountyId', '?')}", {
|
|
391
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
392
|
+
})
|
|
393
|
+
elif signal_type == "bounty_work_submitted":
|
|
394
|
+
await self._handle_bounty_work_submitted(data)
|
|
395
|
+
elif signal_type == "bounty_submission_selected":
|
|
396
|
+
await self._handle_bounty_submission_selected(data)
|
|
397
|
+
elif signal_type == "bounty_submission_not_selected":
|
|
398
|
+
self._broadcast("action_skipped", f"Bounty submission not selected for bounty #{data.get('bountyId', '?')}", {
|
|
399
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
400
|
+
})
|
|
401
|
+
# ── On-chain bounty lifecycle signals ──
|
|
402
|
+
elif signal_type == "bounty_claimed":
|
|
403
|
+
self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} claimed by {data.get('claimerAddress', '?')}", {
|
|
404
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
405
|
+
})
|
|
406
|
+
elif signal_type == "bounty_work_approved":
|
|
407
|
+
self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} work approved — reward released", {
|
|
408
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
409
|
+
})
|
|
410
|
+
elif signal_type == "bounty_disputed":
|
|
411
|
+
self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} work disputed", {
|
|
412
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
413
|
+
})
|
|
414
|
+
elif signal_type == "bounty_cancelled":
|
|
415
|
+
self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} cancelled by creator", {
|
|
416
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
417
|
+
})
|
|
418
|
+
elif signal_type == "bounty_claimer_approved":
|
|
419
|
+
await self._handle_bounty_claimer_approved(data)
|
|
420
|
+
# ── Marketplace signals ──
|
|
421
|
+
elif signal_type == "agreement_created":
|
|
422
|
+
await self._handle_agreement_created(data)
|
|
423
|
+
elif signal_type == "work_delivered":
|
|
424
|
+
await self._handle_work_delivered(data)
|
|
425
|
+
elif signal_type == "agreement_settled":
|
|
426
|
+
await self._handle_agreement_settled(data)
|
|
427
|
+
elif signal_type == "agreement_disputed":
|
|
428
|
+
await self._handle_agreement_disputed(data)
|
|
429
|
+
elif signal_type == "agreement_cancelled":
|
|
430
|
+
self._broadcast("action_skipped", f"Agreement #{data.get('agreementId', '?')} cancelled", {
|
|
431
|
+
"signalType": signal_type, "agreementId": data.get("agreementId"),
|
|
432
|
+
})
|
|
433
|
+
elif signal_type == "revision_requested":
|
|
434
|
+
await self._handle_revision_requested(data)
|
|
435
|
+
elif signal_type == "review_received":
|
|
436
|
+
self._broadcast("action_skipped", f"Received {data.get('rating', '?')}-star review on Agreement #{data.get('agreementId', '?')}", {
|
|
437
|
+
"signalType": signal_type, "agreementId": data.get("agreementId"), "rating": data.get("rating"),
|
|
438
|
+
})
|
|
345
439
|
else:
|
|
346
440
|
self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
|
|
347
441
|
"signalType": signal_type,
|
|
@@ -831,6 +925,323 @@ class AutonomousAgent:
|
|
|
831
925
|
"action": "team_invitation", "invitationId": invitation_id, "error": str(exc),
|
|
832
926
|
})
|
|
833
927
|
|
|
928
|
+
# ── Marketplace signal handlers ──
|
|
929
|
+
|
|
930
|
+
async def _parse_and_execute_action(self, text: str) -> None:
|
|
931
|
+
"""Parse an LLM response in 'ACTION: <type>\\nPARAMS: <json>' format and dispatch."""
|
|
932
|
+
import re, json as _json
|
|
933
|
+
action_match = re.search(r"ACTION:\s*(\S+)", text, re.IGNORECASE)
|
|
934
|
+
if not action_match:
|
|
935
|
+
if self._verbose:
|
|
936
|
+
logger.debug("[autonomous] No action parsed from response")
|
|
937
|
+
return
|
|
938
|
+
action_type = action_match.group(1).lower()
|
|
939
|
+
|
|
940
|
+
payload: dict[str, Any] = {}
|
|
941
|
+
params_match = re.search(r"PARAMS:\s*(\{[\s\S]*\})", text, re.IGNORECASE)
|
|
942
|
+
if params_match:
|
|
943
|
+
try:
|
|
944
|
+
payload = _json.loads(params_match.group(1))
|
|
945
|
+
except _json.JSONDecodeError:
|
|
946
|
+
if self._verbose:
|
|
947
|
+
logger.debug("[autonomous] Failed to parse PARAMS JSON")
|
|
948
|
+
|
|
949
|
+
await self._handle_action_request({
|
|
950
|
+
"actionType": action_type,
|
|
951
|
+
"payload": payload,
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
async def _handle_agreement_created(self, data: dict[str, Any]) -> None:
|
|
955
|
+
"""Provider received a new agreement — decide whether to start work."""
|
|
956
|
+
agreement_id = data.get("agreementId", "")
|
|
957
|
+
buyer_address = data.get("buyerAddress", "")
|
|
958
|
+
escrow_amount = data.get("escrowAmount", "0")
|
|
959
|
+
description = data.get("description", "")
|
|
960
|
+
|
|
961
|
+
try:
|
|
962
|
+
prompt = (
|
|
963
|
+
"You've been hired for a service agreement on Nookplot.\n"
|
|
964
|
+
f"Agreement #{agreement_id}\n"
|
|
965
|
+
f"Buyer: {str(buyer_address)[:12]}...\n"
|
|
966
|
+
f"Escrow: {escrow_amount} (locked until you deliver)\n"
|
|
967
|
+
+ (f"Terms: {str(description)[:500]}\n" if description else "")
|
|
968
|
+
+ "\nReview the terms and decide your next step.\n\n"
|
|
969
|
+
"Available actions:\n"
|
|
970
|
+
"- deliver_work: Submit your completed work (params: agreementId, deliveryCid)\n"
|
|
971
|
+
"- send_agreement_message: Send a message to the buyer (params: agreementId, messageType='general', content)\n"
|
|
972
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>\n"
|
|
973
|
+
"Or respond with ACKNOWLEDGE if you need more time."
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
assert self._generate_response is not None
|
|
977
|
+
response = await self._generate_response(prompt)
|
|
978
|
+
text = (response or "").strip()
|
|
979
|
+
|
|
980
|
+
if "ACKNOWLEDGE" in text.upper() or not text:
|
|
981
|
+
self._broadcast("action_executed", f"Acknowledged Agreement #{agreement_id}", {
|
|
982
|
+
"action": "agreement_acknowledged", "agreementId": agreement_id,
|
|
983
|
+
})
|
|
984
|
+
return
|
|
985
|
+
|
|
986
|
+
await self._parse_and_execute_action(text)
|
|
987
|
+
except Exception as exc:
|
|
988
|
+
self._broadcast("error", f"Agreement created handling failed: {exc}", {
|
|
989
|
+
"action": "agreement_created", "agreementId": agreement_id, "error": str(exc),
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
async def _handle_work_delivered(self, data: dict[str, Any]) -> None:
|
|
993
|
+
"""Buyer receives delivery — decide whether to settle, dispute, or request revision."""
|
|
994
|
+
agreement_id = data.get("agreementId", "")
|
|
995
|
+
escrow_amount = data.get("escrowAmount", "0")
|
|
996
|
+
|
|
997
|
+
try:
|
|
998
|
+
prompt = (
|
|
999
|
+
"Work has been delivered for a service agreement on Nookplot.\n"
|
|
1000
|
+
f"Agreement #{agreement_id}\n"
|
|
1001
|
+
f"Escrow: {escrow_amount}\n"
|
|
1002
|
+
"\nYou are the buyer. Review the submission and decide:\n\n"
|
|
1003
|
+
"Available actions:\n"
|
|
1004
|
+
"- settle_agreement: Approve work and release escrow (params: agreementId)\n"
|
|
1005
|
+
"- dispute_agreement: Dispute the delivery (params: agreementId)\n"
|
|
1006
|
+
"- send_agreement_message: Request revision (params: agreementId, messageType='revision_request', content)\n"
|
|
1007
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
assert self._generate_response is not None
|
|
1011
|
+
response = await self._generate_response(prompt)
|
|
1012
|
+
text = (response or "").strip()
|
|
1013
|
+
|
|
1014
|
+
if text:
|
|
1015
|
+
await self._parse_and_execute_action(text)
|
|
1016
|
+
except Exception as exc:
|
|
1017
|
+
self._broadcast("error", f"Work delivered handling failed: {exc}", {
|
|
1018
|
+
"action": "work_delivered", "agreementId": agreement_id, "error": str(exc),
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
async def _handle_agreement_settled(self, data: dict[str, Any]) -> None:
|
|
1022
|
+
"""Provider receives settlement — optionally submit a review."""
|
|
1023
|
+
agreement_id = data.get("agreementId", "")
|
|
1024
|
+
escrow_amount = data.get("escrowAmount", "0")
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
prompt = (
|
|
1028
|
+
"A service agreement has been settled on Nookplot — escrow released to you.\n"
|
|
1029
|
+
f"Agreement #{agreement_id}\n"
|
|
1030
|
+
f"Amount received: {escrow_amount}\n\n"
|
|
1031
|
+
"Would you like to submit a review for the buyer?\n\n"
|
|
1032
|
+
"Available actions:\n"
|
|
1033
|
+
'- submit_review: Leave a review (params: agreementId, rating (1-5), comment)\n'
|
|
1034
|
+
"\nFormat:\nACTION: submit_review\n"
|
|
1035
|
+
'PARAMS: {"agreementId": ..., "rating": ..., "comment": "..."}\n'
|
|
1036
|
+
"Or respond with SKIP to not leave a review."
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
assert self._generate_response is not None
|
|
1040
|
+
response = await self._generate_response(prompt)
|
|
1041
|
+
text = (response or "").strip()
|
|
1042
|
+
|
|
1043
|
+
if "SKIP" in text.upper() or not text:
|
|
1044
|
+
self._broadcast("action_executed", f"Agreement #{agreement_id} settled (no review)", {
|
|
1045
|
+
"action": "agreement_settled_noted", "agreementId": agreement_id,
|
|
1046
|
+
})
|
|
1047
|
+
return
|
|
1048
|
+
|
|
1049
|
+
await self._parse_and_execute_action(text)
|
|
1050
|
+
except Exception as exc:
|
|
1051
|
+
self._broadcast("error", f"Agreement settled handling failed: {exc}", {
|
|
1052
|
+
"action": "agreement_settled", "agreementId": agreement_id, "error": str(exc),
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
async def _handle_agreement_disputed(self, data: dict[str, Any]) -> None:
|
|
1056
|
+
"""Agent receives dispute notification — submit evidence."""
|
|
1057
|
+
agreement_id = data.get("agreementId", "")
|
|
1058
|
+
|
|
1059
|
+
try:
|
|
1060
|
+
prompt = (
|
|
1061
|
+
"A service agreement has been disputed on Nookplot.\n"
|
|
1062
|
+
f"Agreement #{agreement_id}\n\n"
|
|
1063
|
+
"You should provide evidence to support your position.\n\n"
|
|
1064
|
+
"Available actions:\n"
|
|
1065
|
+
"- send_agreement_message: Submit evidence (params: agreementId, messageType='dispute_evidence', content)\n"
|
|
1066
|
+
"\nFormat:\nACTION: send_agreement_message\n"
|
|
1067
|
+
'PARAMS: {"agreementId": ..., "messageType": "dispute_evidence", "content": "..."}'
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
assert self._generate_response is not None
|
|
1071
|
+
response = await self._generate_response(prompt)
|
|
1072
|
+
text = (response or "").strip()
|
|
1073
|
+
|
|
1074
|
+
if text:
|
|
1075
|
+
await self._parse_and_execute_action(text)
|
|
1076
|
+
except Exception as exc:
|
|
1077
|
+
self._broadcast("error", f"Agreement disputed handling failed: {exc}", {
|
|
1078
|
+
"action": "agreement_disputed", "agreementId": agreement_id, "error": str(exc),
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
async def _handle_revision_requested(self, data: dict[str, Any]) -> None:
|
|
1082
|
+
"""Provider receives revision request — revise and re-deliver."""
|
|
1083
|
+
agreement_id = data.get("agreementId", "")
|
|
1084
|
+
message = data.get("message", "")
|
|
1085
|
+
|
|
1086
|
+
try:
|
|
1087
|
+
prompt = (
|
|
1088
|
+
"The buyer has requested a revision on your delivered work.\n"
|
|
1089
|
+
f"Agreement #{agreement_id}\n"
|
|
1090
|
+
+ (f"Revision request: {str(message)[:500]}\n" if message else "")
|
|
1091
|
+
+ "\nRevise your work and re-deliver, or send a response.\n\n"
|
|
1092
|
+
"Available actions:\n"
|
|
1093
|
+
"- deliver_work: Submit revised work (params: agreementId, deliveryCid)\n"
|
|
1094
|
+
"- send_agreement_message: Respond to the buyer (params: agreementId, messageType='revision_response', content)\n"
|
|
1095
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
assert self._generate_response is not None
|
|
1099
|
+
response = await self._generate_response(prompt)
|
|
1100
|
+
text = (response or "").strip()
|
|
1101
|
+
|
|
1102
|
+
if text:
|
|
1103
|
+
await self._parse_and_execute_action(text)
|
|
1104
|
+
except Exception as exc:
|
|
1105
|
+
self._broadcast("error", f"Revision requested handling failed: {exc}", {
|
|
1106
|
+
"action": "revision_requested", "agreementId": agreement_id, "error": str(exc),
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
# ── Bounty application/submission signal handlers ──
|
|
1110
|
+
|
|
1111
|
+
async def _handle_bounty_application_submitted(self, data: dict[str, Any]) -> None:
|
|
1112
|
+
"""Bounty creator receives an application — approve or reject."""
|
|
1113
|
+
bounty_id = data.get("bountyId", "")
|
|
1114
|
+
application_id = data.get("applicationId", "")
|
|
1115
|
+
applicant = data.get("senderAddress", "?")
|
|
1116
|
+
preview = data.get("messagePreview", "")
|
|
1117
|
+
|
|
1118
|
+
try:
|
|
1119
|
+
prompt = (
|
|
1120
|
+
"An agent has applied to work on your bounty.\n"
|
|
1121
|
+
f"Bounty #{bounty_id}\n"
|
|
1122
|
+
f"Applicant: {str(applicant)[:12]}...\n"
|
|
1123
|
+
+ (f"Application message: {str(preview)[:200]}\n" if preview else "")
|
|
1124
|
+
+ f"Application ID: {application_id}\n\n"
|
|
1125
|
+
"Review the application and decide whether to approve or reject.\n\n"
|
|
1126
|
+
"Available actions:\n"
|
|
1127
|
+
"- approve_bounty_application: Approve the applicant (params: bountyId, applicationId)\n"
|
|
1128
|
+
"- reject_bounty_application: Reject the applicant (params: bountyId, applicationId)\n"
|
|
1129
|
+
"- ignore: Take no action\n"
|
|
1130
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1131
|
+
)
|
|
1132
|
+
assert self._generate_response is not None
|
|
1133
|
+
response = await self._generate_response(prompt)
|
|
1134
|
+
text = (response or "").strip()
|
|
1135
|
+
if text:
|
|
1136
|
+
await self._parse_and_execute_action(text)
|
|
1137
|
+
except Exception as exc:
|
|
1138
|
+
self._broadcast("error", f"Bounty application handling failed: {exc}", {
|
|
1139
|
+
"action": "bounty_application_submitted", "bountyId": bounty_id, "error": str(exc),
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
async def _handle_bounty_application_approved(self, data: dict[str, Any]) -> None:
|
|
1143
|
+
"""Applicant receives approval — submit work."""
|
|
1144
|
+
bounty_id = data.get("bountyId", "")
|
|
1145
|
+
|
|
1146
|
+
try:
|
|
1147
|
+
prompt = (
|
|
1148
|
+
"Your bounty application has been approved!\n"
|
|
1149
|
+
f"Bounty #{bounty_id}\n\n"
|
|
1150
|
+
"You can now submit your work for this bounty.\n\n"
|
|
1151
|
+
"Available actions:\n"
|
|
1152
|
+
"- submit_bounty_work: Submit your work (params: bountyId, content)\n"
|
|
1153
|
+
"- ignore: Take no action yet\n"
|
|
1154
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1155
|
+
)
|
|
1156
|
+
assert self._generate_response is not None
|
|
1157
|
+
response = await self._generate_response(prompt)
|
|
1158
|
+
text = (response or "").strip()
|
|
1159
|
+
if text:
|
|
1160
|
+
await self._parse_and_execute_action(text)
|
|
1161
|
+
except Exception as exc:
|
|
1162
|
+
self._broadcast("error", f"Bounty application approved handling failed: {exc}", {
|
|
1163
|
+
"action": "bounty_application_approved", "bountyId": bounty_id, "error": str(exc),
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
async def _handle_bounty_work_submitted(self, data: dict[str, Any]) -> None:
|
|
1167
|
+
"""Bounty creator receives work submission — review and select."""
|
|
1168
|
+
bounty_id = data.get("bountyId", "")
|
|
1169
|
+
submission_id = data.get("submissionId", "")
|
|
1170
|
+
submitter = data.get("senderAddress", "?")
|
|
1171
|
+
preview = data.get("messagePreview", "")
|
|
1172
|
+
|
|
1173
|
+
try:
|
|
1174
|
+
prompt = (
|
|
1175
|
+
"An applicant has submitted work for your bounty.\n"
|
|
1176
|
+
f"Bounty #{bounty_id}\n"
|
|
1177
|
+
f"Submitter: {str(submitter)[:12]}...\n"
|
|
1178
|
+
f"Submission ID: {submission_id}\n"
|
|
1179
|
+
+ (f"Work preview: {str(preview)[:200]}\n" if preview else "")
|
|
1180
|
+
+ "\nReview the submission and decide whether to select it as the winner.\n\n"
|
|
1181
|
+
"Available actions:\n"
|
|
1182
|
+
"- select_bounty_submission: Select this submission as the winner (params: bountyId, submissionId)\n"
|
|
1183
|
+
"- ignore: Take no action yet (wait for more submissions)\n"
|
|
1184
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1185
|
+
)
|
|
1186
|
+
assert self._generate_response is not None
|
|
1187
|
+
response = await self._generate_response(prompt)
|
|
1188
|
+
text = (response or "").strip()
|
|
1189
|
+
if text:
|
|
1190
|
+
await self._parse_and_execute_action(text)
|
|
1191
|
+
except Exception as exc:
|
|
1192
|
+
self._broadcast("error", f"Bounty work submitted handling failed: {exc}", {
|
|
1193
|
+
"action": "bounty_work_submitted", "bountyId": bounty_id, "error": str(exc),
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
async def _handle_bounty_submission_selected(self, data: dict[str, Any]) -> None:
|
|
1197
|
+
"""Winner receives selection — claim bounty on-chain."""
|
|
1198
|
+
bounty_id = data.get("bountyId", "")
|
|
1199
|
+
|
|
1200
|
+
try:
|
|
1201
|
+
prompt = (
|
|
1202
|
+
"Your submission was selected as the winner!\n"
|
|
1203
|
+
f"Bounty #{bounty_id}\n\n"
|
|
1204
|
+
"You can now claim the bounty reward on-chain.\n\n"
|
|
1205
|
+
"Available actions:\n"
|
|
1206
|
+
"- claim_bounty: Claim the bounty reward on-chain (params: bountyId)\n"
|
|
1207
|
+
"- ignore: Take no action yet\n"
|
|
1208
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1209
|
+
)
|
|
1210
|
+
assert self._generate_response is not None
|
|
1211
|
+
response = await self._generate_response(prompt)
|
|
1212
|
+
text = (response or "").strip()
|
|
1213
|
+
if text:
|
|
1214
|
+
await self._parse_and_execute_action(text)
|
|
1215
|
+
except Exception as exc:
|
|
1216
|
+
self._broadcast("error", f"Bounty submission selected handling failed: {exc}", {
|
|
1217
|
+
"action": "bounty_submission_selected", "bountyId": bounty_id, "error": str(exc),
|
|
1218
|
+
})
|
|
1219
|
+
|
|
1220
|
+
async def _handle_bounty_claimer_approved(self, data: dict[str, Any]) -> None:
|
|
1221
|
+
"""Approved claimer receives notification — claim the bounty."""
|
|
1222
|
+
bounty_id = data.get("bountyId", "")
|
|
1223
|
+
reward_amount = data.get("rewardAmount", "0")
|
|
1224
|
+
|
|
1225
|
+
try:
|
|
1226
|
+
prompt = (
|
|
1227
|
+
"You've been approved to claim a bounty on-chain!\n"
|
|
1228
|
+
f"Bounty #{bounty_id}, Reward: {reward_amount}\n\n"
|
|
1229
|
+
"You can now claim the bounty reward.\n\n"
|
|
1230
|
+
"Available actions:\n"
|
|
1231
|
+
"- claim_bounty: Claim the bounty reward on-chain (params: bountyId)\n"
|
|
1232
|
+
"- ignore: Take no action yet\n"
|
|
1233
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1234
|
+
)
|
|
1235
|
+
assert self._generate_response is not None
|
|
1236
|
+
response = await self._generate_response(prompt)
|
|
1237
|
+
text = (response or "").strip()
|
|
1238
|
+
if text:
|
|
1239
|
+
await self._parse_and_execute_action(text)
|
|
1240
|
+
except Exception as exc:
|
|
1241
|
+
self._broadcast("error", f"Bounty claimer approved handling failed: {exc}", {
|
|
1242
|
+
"action": "bounty_claimer_approved", "bountyId": bounty_id, "error": str(exc),
|
|
1243
|
+
})
|
|
1244
|
+
|
|
834
1245
|
async def _handle_community_gap(self, data: dict[str, Any]) -> None:
|
|
835
1246
|
"""Handle a community gap signal — propose creating a new community."""
|
|
836
1247
|
topic = data.get("messagePreview", "")
|
|
@@ -1526,7 +1937,7 @@ class AutonomousAgent:
|
|
|
1526
1937
|
_ON_CHAIN_ACTIONS = {
|
|
1527
1938
|
"vote", "follow_agent", "attest_agent", "create_community",
|
|
1528
1939
|
"create_project", "propose_clique", "propose_guild", "claim_bounty",
|
|
1529
|
-
"create_bounty", "deploy_preview", "create_bundle",
|
|
1940
|
+
"create_bounty", "approve_bounty_claimer", "deploy_preview", "create_bundle",
|
|
1530
1941
|
"link_project_to_guild", "link_project_to_clique",
|
|
1531
1942
|
"list_service", "create_agreement", "deliver_work",
|
|
1532
1943
|
"settle_agreement", "dispute_agreement", "cancel_agreement",
|
|
@@ -1703,19 +2114,86 @@ class AutonomousAgent:
|
|
|
1703
2114
|
commit_result = await self._runtime.projects.commit_files(pid, files, msg)
|
|
1704
2115
|
result = commit_result if isinstance(commit_result, dict) else {"committed": True}
|
|
1705
2116
|
|
|
1706
|
-
elif action_type == "
|
|
2117
|
+
elif action_type == "apply_bounty":
|
|
2118
|
+
bounty_id = payload.get("bountyId")
|
|
2119
|
+
if not bounty_id:
|
|
2120
|
+
raise ValueError("apply_bounty requires bountyId")
|
|
2121
|
+
apply_msg = suggested_content or payload.get("message", "")
|
|
2122
|
+
apply_result = await self._runtime._http.request(
|
|
2123
|
+
"POST", f"/v1/bounties/{bounty_id}/apply", {"message": apply_msg}
|
|
2124
|
+
)
|
|
2125
|
+
result = apply_result if isinstance(apply_result, dict) else {"applied": True}
|
|
2126
|
+
|
|
2127
|
+
elif action_type == "submit_bounty_work":
|
|
2128
|
+
bounty_id = payload.get("bountyId")
|
|
2129
|
+
if not bounty_id:
|
|
2130
|
+
raise ValueError("submit_bounty_work requires bountyId")
|
|
2131
|
+
work_content = suggested_content or payload.get("content", "")
|
|
2132
|
+
if not work_content:
|
|
2133
|
+
raise ValueError("submit_bounty_work requires content")
|
|
2134
|
+
cids = payload.get("deliverableCids", [])
|
|
2135
|
+
sub_result = await self._runtime._http.request(
|
|
2136
|
+
"POST", f"/v1/bounties/{bounty_id}/submissions",
|
|
2137
|
+
{"content": work_content, "deliverableCids": cids}
|
|
2138
|
+
)
|
|
2139
|
+
result = sub_result if isinstance(sub_result, dict) else {"submitted": True}
|
|
2140
|
+
|
|
2141
|
+
elif action_type in ("claim", "claim_bounty"):
|
|
1707
2142
|
bounty_id = payload.get("bountyId")
|
|
1708
|
-
submission = suggested_content or payload.get("submission", "")
|
|
1709
2143
|
if not bounty_id:
|
|
1710
2144
|
raise ValueError("claim_bounty requires bountyId")
|
|
1711
|
-
#
|
|
2145
|
+
# Only works if agent is the selected winner (application flow gate)
|
|
1712
2146
|
prep = await self._runtime._http.request(
|
|
1713
|
-
"POST", f"/v1/prepare/bounty/{bounty_id}/claim", {
|
|
2147
|
+
"POST", f"/v1/prepare/bounty/{bounty_id}/claim", {}
|
|
1714
2148
|
)
|
|
1715
2149
|
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
1716
2150
|
tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
|
|
1717
2151
|
result = relay if isinstance(relay, dict) else {"claimed": True}
|
|
1718
2152
|
|
|
2153
|
+
elif action_type == "approve_bounty_claimer":
|
|
2154
|
+
bounty_id = payload.get("bountyId")
|
|
2155
|
+
claimer = payload.get("claimer")
|
|
2156
|
+
if not bounty_id:
|
|
2157
|
+
raise ValueError("approve_bounty_claimer requires bountyId")
|
|
2158
|
+
if not claimer:
|
|
2159
|
+
raise ValueError("approve_bounty_claimer requires claimer")
|
|
2160
|
+
prep = await self._runtime._http.request(
|
|
2161
|
+
"POST", f"/v1/prepare/bounty/{bounty_id}/approve-claimer", {"claimer": claimer}
|
|
2162
|
+
)
|
|
2163
|
+
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
2164
|
+
tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
|
|
2165
|
+
result = relay if isinstance(relay, dict) else {"approved": True}
|
|
2166
|
+
|
|
2167
|
+
elif action_type == "approve_bounty_application":
|
|
2168
|
+
bounty_id = payload.get("bountyId")
|
|
2169
|
+
application_id = payload.get("applicationId")
|
|
2170
|
+
if not bounty_id or not application_id:
|
|
2171
|
+
raise ValueError("approve_bounty_application requires bountyId and applicationId")
|
|
2172
|
+
result_raw = await self._runtime._http.request(
|
|
2173
|
+
"POST", f"/v1/bounties/{bounty_id}/applications/{application_id}/approve", {}
|
|
2174
|
+
)
|
|
2175
|
+
result = result_raw if isinstance(result_raw, dict) else {"approved": True}
|
|
2176
|
+
|
|
2177
|
+
elif action_type == "reject_bounty_application":
|
|
2178
|
+
bounty_id = payload.get("bountyId")
|
|
2179
|
+
application_id = payload.get("applicationId")
|
|
2180
|
+
if not bounty_id or not application_id:
|
|
2181
|
+
raise ValueError("reject_bounty_application requires bountyId and applicationId")
|
|
2182
|
+
result_raw = await self._runtime._http.request(
|
|
2183
|
+
"POST", f"/v1/bounties/{bounty_id}/applications/{application_id}/reject", {}
|
|
2184
|
+
)
|
|
2185
|
+
result = result_raw if isinstance(result_raw, dict) else {"rejected": True}
|
|
2186
|
+
|
|
2187
|
+
elif action_type == "select_bounty_submission":
|
|
2188
|
+
bounty_id = payload.get("bountyId")
|
|
2189
|
+
submission_id = payload.get("submissionId")
|
|
2190
|
+
if not bounty_id or not submission_id:
|
|
2191
|
+
raise ValueError("select_bounty_submission requires bountyId and submissionId")
|
|
2192
|
+
result_raw = await self._runtime._http.request(
|
|
2193
|
+
"POST", f"/v1/bounties/{bounty_id}/submissions/{submission_id}/select", {}
|
|
2194
|
+
)
|
|
2195
|
+
result = result_raw if isinstance(result_raw, dict) else {"selected": True}
|
|
2196
|
+
|
|
1719
2197
|
elif action_type == "create_bounty":
|
|
1720
2198
|
title = suggested_content or payload.get("title")
|
|
1721
2199
|
desc = payload.get("description", "")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nookplot-runtime"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.15"
|
|
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
|
|
File without changes
|