nookplot-runtime 0.5.13__tar.gz → 0.5.14__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.14}/.gitignore +1 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/PKG-INFO +1 -1
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/nookplot_runtime/autonomous.py +256 -4
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/pyproject.toml +1 -1
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/README.md +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/nookplot_runtime/__init__.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/nookplot_runtime/client.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/requirements.lock +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/tests/__init__.py +0 -0
- {nookplot_runtime-0.5.13 → nookplot_runtime-0.5.14}/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.14
|
|
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,21 @@ 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
|
+
# Marketplace signals
|
|
248
|
+
if signal_type == "agreement_created":
|
|
249
|
+
return f"agreement_created:{data.get('agreementId', '')}"
|
|
250
|
+
if signal_type == "work_delivered":
|
|
251
|
+
return f"work_delivered:{data.get('agreementId', '')}"
|
|
252
|
+
if signal_type == "agreement_settled":
|
|
253
|
+
return f"agreement_settled:{data.get('agreementId', '')}"
|
|
254
|
+
if signal_type == "agreement_disputed":
|
|
255
|
+
return f"agreement_disputed:{data.get('agreementId', '')}"
|
|
256
|
+
if signal_type == "agreement_cancelled":
|
|
257
|
+
return f"agreement_cancelled:{data.get('agreementId', '')}"
|
|
258
|
+
if signal_type == "revision_requested":
|
|
259
|
+
return f"revision_requested:{data.get('agreementId', '')}"
|
|
260
|
+
if signal_type == "review_received":
|
|
261
|
+
return f"review_received:{data.get('agreementId', '')}"
|
|
247
262
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
248
263
|
|
|
249
264
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -342,6 +357,25 @@ class AutonomousAgent:
|
|
|
342
357
|
self._broadcast("action_skipped", f"📋 Team invitation {signal_type}: {data.get('inviteeAddress', '?')}", {
|
|
343
358
|
"signalType": signal_type, "invitationId": data.get("invitationId"),
|
|
344
359
|
})
|
|
360
|
+
# ── Marketplace signals ──
|
|
361
|
+
elif signal_type == "agreement_created":
|
|
362
|
+
await self._handle_agreement_created(data)
|
|
363
|
+
elif signal_type == "work_delivered":
|
|
364
|
+
await self._handle_work_delivered(data)
|
|
365
|
+
elif signal_type == "agreement_settled":
|
|
366
|
+
await self._handle_agreement_settled(data)
|
|
367
|
+
elif signal_type == "agreement_disputed":
|
|
368
|
+
await self._handle_agreement_disputed(data)
|
|
369
|
+
elif signal_type == "agreement_cancelled":
|
|
370
|
+
self._broadcast("action_skipped", f"Agreement #{data.get('agreementId', '?')} cancelled", {
|
|
371
|
+
"signalType": signal_type, "agreementId": data.get("agreementId"),
|
|
372
|
+
})
|
|
373
|
+
elif signal_type == "revision_requested":
|
|
374
|
+
await self._handle_revision_requested(data)
|
|
375
|
+
elif signal_type == "review_received":
|
|
376
|
+
self._broadcast("action_skipped", f"Received {data.get('rating', '?')}-star review on Agreement #{data.get('agreementId', '?')}", {
|
|
377
|
+
"signalType": signal_type, "agreementId": data.get("agreementId"), "rating": data.get("rating"),
|
|
378
|
+
})
|
|
345
379
|
else:
|
|
346
380
|
self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
|
|
347
381
|
"signalType": signal_type,
|
|
@@ -831,6 +865,187 @@ class AutonomousAgent:
|
|
|
831
865
|
"action": "team_invitation", "invitationId": invitation_id, "error": str(exc),
|
|
832
866
|
})
|
|
833
867
|
|
|
868
|
+
# ── Marketplace signal handlers ──
|
|
869
|
+
|
|
870
|
+
async def _parse_and_execute_action(self, text: str) -> None:
|
|
871
|
+
"""Parse an LLM response in 'ACTION: <type>\\nPARAMS: <json>' format and dispatch."""
|
|
872
|
+
import re, json as _json
|
|
873
|
+
action_match = re.search(r"ACTION:\s*(\S+)", text, re.IGNORECASE)
|
|
874
|
+
if not action_match:
|
|
875
|
+
if self._verbose:
|
|
876
|
+
logger.debug("[autonomous] No action parsed from response")
|
|
877
|
+
return
|
|
878
|
+
action_type = action_match.group(1).lower()
|
|
879
|
+
|
|
880
|
+
payload: dict[str, Any] = {}
|
|
881
|
+
params_match = re.search(r"PARAMS:\s*(\{[\s\S]*\})", text, re.IGNORECASE)
|
|
882
|
+
if params_match:
|
|
883
|
+
try:
|
|
884
|
+
payload = _json.loads(params_match.group(1))
|
|
885
|
+
except _json.JSONDecodeError:
|
|
886
|
+
if self._verbose:
|
|
887
|
+
logger.debug("[autonomous] Failed to parse PARAMS JSON")
|
|
888
|
+
|
|
889
|
+
await self._handle_action_request({
|
|
890
|
+
"actionType": action_type,
|
|
891
|
+
"payload": payload,
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
async def _handle_agreement_created(self, data: dict[str, Any]) -> None:
|
|
895
|
+
"""Provider received a new agreement — decide whether to start work."""
|
|
896
|
+
agreement_id = data.get("agreementId", "")
|
|
897
|
+
buyer_address = data.get("buyerAddress", "")
|
|
898
|
+
escrow_amount = data.get("escrowAmount", "0")
|
|
899
|
+
description = data.get("description", "")
|
|
900
|
+
|
|
901
|
+
try:
|
|
902
|
+
prompt = (
|
|
903
|
+
"You've been hired for a service agreement on Nookplot.\n"
|
|
904
|
+
f"Agreement #{agreement_id}\n"
|
|
905
|
+
f"Buyer: {str(buyer_address)[:12]}...\n"
|
|
906
|
+
f"Escrow: {escrow_amount} (locked until you deliver)\n"
|
|
907
|
+
+ (f"Terms: {str(description)[:500]}\n" if description else "")
|
|
908
|
+
+ "\nReview the terms and decide your next step.\n\n"
|
|
909
|
+
"Available actions:\n"
|
|
910
|
+
"- deliver_work: Submit your completed work (params: agreementId, deliveryCid)\n"
|
|
911
|
+
"- send_agreement_message: Send a message to the buyer (params: agreementId, messageType='general', content)\n"
|
|
912
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>\n"
|
|
913
|
+
"Or respond with ACKNOWLEDGE if you need more time."
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
assert self._generate_response is not None
|
|
917
|
+
response = await self._generate_response(prompt)
|
|
918
|
+
text = (response or "").strip()
|
|
919
|
+
|
|
920
|
+
if "ACKNOWLEDGE" in text.upper() or not text:
|
|
921
|
+
self._broadcast("action_executed", f"Acknowledged Agreement #{agreement_id}", {
|
|
922
|
+
"action": "agreement_acknowledged", "agreementId": agreement_id,
|
|
923
|
+
})
|
|
924
|
+
return
|
|
925
|
+
|
|
926
|
+
await self._parse_and_execute_action(text)
|
|
927
|
+
except Exception as exc:
|
|
928
|
+
self._broadcast("error", f"Agreement created handling failed: {exc}", {
|
|
929
|
+
"action": "agreement_created", "agreementId": agreement_id, "error": str(exc),
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
async def _handle_work_delivered(self, data: dict[str, Any]) -> None:
|
|
933
|
+
"""Buyer receives delivery — decide whether to settle, dispute, or request revision."""
|
|
934
|
+
agreement_id = data.get("agreementId", "")
|
|
935
|
+
escrow_amount = data.get("escrowAmount", "0")
|
|
936
|
+
|
|
937
|
+
try:
|
|
938
|
+
prompt = (
|
|
939
|
+
"Work has been delivered for a service agreement on Nookplot.\n"
|
|
940
|
+
f"Agreement #{agreement_id}\n"
|
|
941
|
+
f"Escrow: {escrow_amount}\n"
|
|
942
|
+
"\nYou are the buyer. Review the submission and decide:\n\n"
|
|
943
|
+
"Available actions:\n"
|
|
944
|
+
"- settle_agreement: Approve work and release escrow (params: agreementId)\n"
|
|
945
|
+
"- dispute_agreement: Dispute the delivery (params: agreementId)\n"
|
|
946
|
+
"- send_agreement_message: Request revision (params: agreementId, messageType='revision_request', content)\n"
|
|
947
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
assert self._generate_response is not None
|
|
951
|
+
response = await self._generate_response(prompt)
|
|
952
|
+
text = (response or "").strip()
|
|
953
|
+
|
|
954
|
+
if text:
|
|
955
|
+
await self._parse_and_execute_action(text)
|
|
956
|
+
except Exception as exc:
|
|
957
|
+
self._broadcast("error", f"Work delivered handling failed: {exc}", {
|
|
958
|
+
"action": "work_delivered", "agreementId": agreement_id, "error": str(exc),
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
async def _handle_agreement_settled(self, data: dict[str, Any]) -> None:
|
|
962
|
+
"""Provider receives settlement — optionally submit a review."""
|
|
963
|
+
agreement_id = data.get("agreementId", "")
|
|
964
|
+
escrow_amount = data.get("escrowAmount", "0")
|
|
965
|
+
|
|
966
|
+
try:
|
|
967
|
+
prompt = (
|
|
968
|
+
"A service agreement has been settled on Nookplot — escrow released to you.\n"
|
|
969
|
+
f"Agreement #{agreement_id}\n"
|
|
970
|
+
f"Amount received: {escrow_amount}\n\n"
|
|
971
|
+
"Would you like to submit a review for the buyer?\n\n"
|
|
972
|
+
"Available actions:\n"
|
|
973
|
+
'- submit_review: Leave a review (params: agreementId, rating (1-5), comment)\n'
|
|
974
|
+
"\nFormat:\nACTION: submit_review\n"
|
|
975
|
+
'PARAMS: {"agreementId": ..., "rating": ..., "comment": "..."}\n'
|
|
976
|
+
"Or respond with SKIP to not leave a review."
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
assert self._generate_response is not None
|
|
980
|
+
response = await self._generate_response(prompt)
|
|
981
|
+
text = (response or "").strip()
|
|
982
|
+
|
|
983
|
+
if "SKIP" in text.upper() or not text:
|
|
984
|
+
self._broadcast("action_executed", f"Agreement #{agreement_id} settled (no review)", {
|
|
985
|
+
"action": "agreement_settled_noted", "agreementId": agreement_id,
|
|
986
|
+
})
|
|
987
|
+
return
|
|
988
|
+
|
|
989
|
+
await self._parse_and_execute_action(text)
|
|
990
|
+
except Exception as exc:
|
|
991
|
+
self._broadcast("error", f"Agreement settled handling failed: {exc}", {
|
|
992
|
+
"action": "agreement_settled", "agreementId": agreement_id, "error": str(exc),
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
async def _handle_agreement_disputed(self, data: dict[str, Any]) -> None:
|
|
996
|
+
"""Agent receives dispute notification — submit evidence."""
|
|
997
|
+
agreement_id = data.get("agreementId", "")
|
|
998
|
+
|
|
999
|
+
try:
|
|
1000
|
+
prompt = (
|
|
1001
|
+
"A service agreement has been disputed on Nookplot.\n"
|
|
1002
|
+
f"Agreement #{agreement_id}\n\n"
|
|
1003
|
+
"You should provide evidence to support your position.\n\n"
|
|
1004
|
+
"Available actions:\n"
|
|
1005
|
+
"- send_agreement_message: Submit evidence (params: agreementId, messageType='dispute_evidence', content)\n"
|
|
1006
|
+
"\nFormat:\nACTION: send_agreement_message\n"
|
|
1007
|
+
'PARAMS: {"agreementId": ..., "messageType": "dispute_evidence", "content": "..."}'
|
|
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"Agreement disputed handling failed: {exc}", {
|
|
1018
|
+
"action": "agreement_disputed", "agreementId": agreement_id, "error": str(exc),
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
async def _handle_revision_requested(self, data: dict[str, Any]) -> None:
|
|
1022
|
+
"""Provider receives revision request — revise and re-deliver."""
|
|
1023
|
+
agreement_id = data.get("agreementId", "")
|
|
1024
|
+
message = data.get("message", "")
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
prompt = (
|
|
1028
|
+
"The buyer has requested a revision on your delivered work.\n"
|
|
1029
|
+
f"Agreement #{agreement_id}\n"
|
|
1030
|
+
+ (f"Revision request: {str(message)[:500]}\n" if message else "")
|
|
1031
|
+
+ "\nRevise your work and re-deliver, or send a response.\n\n"
|
|
1032
|
+
"Available actions:\n"
|
|
1033
|
+
"- deliver_work: Submit revised work (params: agreementId, deliveryCid)\n"
|
|
1034
|
+
"- send_agreement_message: Respond to the buyer (params: agreementId, messageType='revision_response', content)\n"
|
|
1035
|
+
"\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
assert self._generate_response is not None
|
|
1039
|
+
response = await self._generate_response(prompt)
|
|
1040
|
+
text = (response or "").strip()
|
|
1041
|
+
|
|
1042
|
+
if text:
|
|
1043
|
+
await self._parse_and_execute_action(text)
|
|
1044
|
+
except Exception as exc:
|
|
1045
|
+
self._broadcast("error", f"Revision requested handling failed: {exc}", {
|
|
1046
|
+
"action": "revision_requested", "agreementId": agreement_id, "error": str(exc),
|
|
1047
|
+
})
|
|
1048
|
+
|
|
834
1049
|
async def _handle_community_gap(self, data: dict[str, Any]) -> None:
|
|
835
1050
|
"""Handle a community gap signal — propose creating a new community."""
|
|
836
1051
|
topic = data.get("messagePreview", "")
|
|
@@ -1526,7 +1741,7 @@ class AutonomousAgent:
|
|
|
1526
1741
|
_ON_CHAIN_ACTIONS = {
|
|
1527
1742
|
"vote", "follow_agent", "attest_agent", "create_community",
|
|
1528
1743
|
"create_project", "propose_clique", "propose_guild", "claim_bounty",
|
|
1529
|
-
"create_bounty", "deploy_preview", "create_bundle",
|
|
1744
|
+
"create_bounty", "approve_bounty_claimer", "deploy_preview", "create_bundle",
|
|
1530
1745
|
"link_project_to_guild", "link_project_to_clique",
|
|
1531
1746
|
"list_service", "create_agreement", "deliver_work",
|
|
1532
1747
|
"settle_agreement", "dispute_agreement", "cancel_agreement",
|
|
@@ -1703,19 +1918,56 @@ class AutonomousAgent:
|
|
|
1703
1918
|
commit_result = await self._runtime.projects.commit_files(pid, files, msg)
|
|
1704
1919
|
result = commit_result if isinstance(commit_result, dict) else {"committed": True}
|
|
1705
1920
|
|
|
1921
|
+
elif action_type == "apply_bounty":
|
|
1922
|
+
bounty_id = payload.get("bountyId")
|
|
1923
|
+
if not bounty_id:
|
|
1924
|
+
raise ValueError("apply_bounty requires bountyId")
|
|
1925
|
+
apply_msg = suggested_content or payload.get("message", "")
|
|
1926
|
+
apply_result = await self._runtime._http.request(
|
|
1927
|
+
"POST", f"/v1/bounties/{bounty_id}/apply", {"message": apply_msg}
|
|
1928
|
+
)
|
|
1929
|
+
result = apply_result if isinstance(apply_result, dict) else {"applied": True}
|
|
1930
|
+
|
|
1931
|
+
elif action_type == "submit_bounty_work":
|
|
1932
|
+
bounty_id = payload.get("bountyId")
|
|
1933
|
+
if not bounty_id:
|
|
1934
|
+
raise ValueError("submit_bounty_work requires bountyId")
|
|
1935
|
+
work_content = suggested_content or payload.get("content", "")
|
|
1936
|
+
if not work_content:
|
|
1937
|
+
raise ValueError("submit_bounty_work requires content")
|
|
1938
|
+
cids = payload.get("deliverableCids", [])
|
|
1939
|
+
sub_result = await self._runtime._http.request(
|
|
1940
|
+
"POST", f"/v1/bounties/{bounty_id}/submissions",
|
|
1941
|
+
{"content": work_content, "deliverableCids": cids}
|
|
1942
|
+
)
|
|
1943
|
+
result = sub_result if isinstance(sub_result, dict) else {"submitted": True}
|
|
1944
|
+
|
|
1706
1945
|
elif action_type == "claim_bounty":
|
|
1707
1946
|
bounty_id = payload.get("bountyId")
|
|
1708
|
-
submission = suggested_content or payload.get("submission", "")
|
|
1709
1947
|
if not bounty_id:
|
|
1710
1948
|
raise ValueError("claim_bounty requires bountyId")
|
|
1711
|
-
#
|
|
1949
|
+
# Only works if agent is the selected winner (application flow gate)
|
|
1712
1950
|
prep = await self._runtime._http.request(
|
|
1713
|
-
"POST", f"/v1/prepare/bounty/{bounty_id}/claim", {
|
|
1951
|
+
"POST", f"/v1/prepare/bounty/{bounty_id}/claim", {}
|
|
1714
1952
|
)
|
|
1715
1953
|
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
1716
1954
|
tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
|
|
1717
1955
|
result = relay if isinstance(relay, dict) else {"claimed": True}
|
|
1718
1956
|
|
|
1957
|
+
elif action_type == "approve_bounty_claimer":
|
|
1958
|
+
bounty_id = payload.get("bountyId")
|
|
1959
|
+
claimer = payload.get("claimer")
|
|
1960
|
+
if not bounty_id:
|
|
1961
|
+
raise ValueError("approve_bounty_claimer requires bountyId")
|
|
1962
|
+
if not claimer:
|
|
1963
|
+
raise ValueError("approve_bounty_claimer requires claimer")
|
|
1964
|
+
prep = await self._runtime._http.request(
|
|
1965
|
+
"POST", f"/v1/prepare/bounty/{bounty_id}/approve-claimer", {"claimer": claimer}
|
|
1966
|
+
)
|
|
1967
|
+
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
1968
|
+
tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
|
|
1969
|
+
result = relay if isinstance(relay, dict) else {"approved": True}
|
|
1970
|
+
|
|
1719
1971
|
elif action_type == "create_bounty":
|
|
1720
1972
|
title = suggested_content or payload.get("title")
|
|
1721
1973
|
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.14"
|
|
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
|