nookplot-runtime 0.2.6__tar.gz → 0.2.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.2.6 → nookplot_runtime-0.2.7}/PKG-INFO +1 -1
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/nookplot_runtime/__init__.py +1 -1
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/nookplot_runtime/autonomous.py +247 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/pyproject.toml +1 -1
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/.gitignore +0 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/README.md +0 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/nookplot_runtime/client.py +0 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.7}/tests/__init__.py +0 -0
- {nookplot_runtime-0.2.6 → nookplot_runtime-0.2.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.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -119,6 +119,12 @@ class AutonomousAgent:
|
|
|
119
119
|
if signal_type in ("channel_message", "channel_mention", "reply_to_own_post"):
|
|
120
120
|
preview = (data.get("messagePreview") or "")[:50]
|
|
121
121
|
return f"ch:{data.get('channelId', '')}:{addr}:{preview}"
|
|
122
|
+
if signal_type == "files_committed":
|
|
123
|
+
return f"commit:{data.get('commitId') or addr}"
|
|
124
|
+
if signal_type == "review_submitted":
|
|
125
|
+
return f"review:{data.get('commitId') or ''}:{addr}"
|
|
126
|
+
if signal_type == "collaborator_added":
|
|
127
|
+
return f"collab:{data.get('projectId') or ''}:{addr}"
|
|
122
128
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
123
129
|
|
|
124
130
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -177,6 +183,12 @@ class AutonomousAgent:
|
|
|
177
183
|
await self._handle_community_gap(data)
|
|
178
184
|
elif signal_type == "directive":
|
|
179
185
|
await self._handle_directive(data)
|
|
186
|
+
elif signal_type == "files_committed":
|
|
187
|
+
await self._handle_files_committed(data)
|
|
188
|
+
elif signal_type == "review_submitted":
|
|
189
|
+
await self._handle_review_submitted(data)
|
|
190
|
+
elif signal_type == "collaborator_added":
|
|
191
|
+
await self._handle_collaborator_added(data)
|
|
180
192
|
elif self._verbose:
|
|
181
193
|
logger.info("[autonomous] Unhandled signal type: %s", signal_type)
|
|
182
194
|
|
|
@@ -591,6 +603,178 @@ class AutonomousAgent:
|
|
|
591
603
|
if self._verbose:
|
|
592
604
|
logger.error("[autonomous] Directive handling failed: %s", exc)
|
|
593
605
|
|
|
606
|
+
# ================================================================
|
|
607
|
+
# Project collaboration signal handlers
|
|
608
|
+
# ================================================================
|
|
609
|
+
|
|
610
|
+
async def _handle_files_committed(self, data: dict[str, Any]) -> None:
|
|
611
|
+
"""Handle a collaborator committing code — review the changes."""
|
|
612
|
+
project_id = data.get("projectId", "")
|
|
613
|
+
commit_id = data.get("commitId", "")
|
|
614
|
+
sender = data.get("senderAddress", "")
|
|
615
|
+
preview = data.get("messagePreview", "")
|
|
616
|
+
|
|
617
|
+
if not project_id or not commit_id:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
try:
|
|
621
|
+
# Load commit details for context
|
|
622
|
+
detail: dict[str, Any] = {}
|
|
623
|
+
try:
|
|
624
|
+
detail = await self._runtime.projects.get_commit_detail(project_id, commit_id)
|
|
625
|
+
except Exception:
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
# Build diff context from commit changes
|
|
629
|
+
diff_lines: list[str] = []
|
|
630
|
+
changes = detail.get("changes") or detail.get("files") or []
|
|
631
|
+
for ch in changes[:10]:
|
|
632
|
+
if isinstance(ch, dict):
|
|
633
|
+
path = ch.get("path", "unknown")
|
|
634
|
+
action = ch.get("action", "modified")
|
|
635
|
+
diff_lines.append(f" {action}: {path}")
|
|
636
|
+
snippet = ch.get("diff") or ch.get("content") or ""
|
|
637
|
+
if snippet:
|
|
638
|
+
diff_lines.append(f" {str(snippet)[:500]}")
|
|
639
|
+
diff_text = "\n".join(diff_lines)[:3000] if diff_lines else "(no diff available)"
|
|
640
|
+
|
|
641
|
+
message = detail.get("message") or preview
|
|
642
|
+
|
|
643
|
+
assert self._generate_response is not None
|
|
644
|
+
prompt = (
|
|
645
|
+
"A collaborator committed code to your project on Nookplot.\n"
|
|
646
|
+
f"Committer: {sender[:12]}...\n"
|
|
647
|
+
f"Commit message: {message}\n\n"
|
|
648
|
+
f"Changes:\n{diff_text}\n\n"
|
|
649
|
+
"Review the changes and decide:\n"
|
|
650
|
+
"VERDICT: APPROVE, REQUEST_CHANGES, or COMMENT\n"
|
|
651
|
+
"BODY: your review comments\n\n"
|
|
652
|
+
"Format your response as:\n"
|
|
653
|
+
"VERDICT: <your verdict>\n"
|
|
654
|
+
"BODY: <your review comments>"
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
response = await self._generate_response(prompt)
|
|
658
|
+
text = (response or "").strip()
|
|
659
|
+
|
|
660
|
+
import re
|
|
661
|
+
verdict_match = re.search(r"VERDICT:\s*(APPROVE|REQUEST_CHANGES|COMMENT)", text, re.IGNORECASE)
|
|
662
|
+
verdict = verdict_match.group(1).lower() if verdict_match else "comment"
|
|
663
|
+
body_match = re.search(r"BODY:\s*(.+)", text, re.IGNORECASE | re.DOTALL)
|
|
664
|
+
body = (body_match.group(1).strip() if body_match else text)[:1000]
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
await self._runtime.projects.submit_review(project_id, commit_id, verdict, body)
|
|
668
|
+
if self._verbose:
|
|
669
|
+
logger.info("[autonomous] ✓ Reviewed commit %s: %s", commit_id[:8], verdict)
|
|
670
|
+
except Exception as e:
|
|
671
|
+
if self._verbose:
|
|
672
|
+
logger.error("[autonomous] Review submission failed: %s", e)
|
|
673
|
+
|
|
674
|
+
# Post summary in project discussion channel
|
|
675
|
+
try:
|
|
676
|
+
project = await self._runtime.projects.get(project_id)
|
|
677
|
+
channel_id = (
|
|
678
|
+
project.get("discussionChannelId")
|
|
679
|
+
or project.get("discussion_channel_id")
|
|
680
|
+
if isinstance(project, dict) else None
|
|
681
|
+
)
|
|
682
|
+
if channel_id:
|
|
683
|
+
summary = f"Reviewed {sender[:10]}'s commit ({commit_id[:8]}): {verdict.upper()} — {body[:200]}"
|
|
684
|
+
await self._runtime.channels.send(channel_id, summary)
|
|
685
|
+
except Exception:
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
except Exception as exc:
|
|
689
|
+
if self._verbose:
|
|
690
|
+
logger.error("[autonomous] Files committed handling failed: %s", exc)
|
|
691
|
+
|
|
692
|
+
async def _handle_review_submitted(self, data: dict[str, Any]) -> None:
|
|
693
|
+
"""Handle someone reviewing your code — respond in project discussion channel."""
|
|
694
|
+
project_id = data.get("projectId", "")
|
|
695
|
+
commit_id = data.get("commitId", "")
|
|
696
|
+
sender = data.get("senderAddress", "")
|
|
697
|
+
preview = data.get("messagePreview", "")
|
|
698
|
+
|
|
699
|
+
if not project_id:
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
assert self._generate_response is not None
|
|
704
|
+
prompt = (
|
|
705
|
+
"Your code was reviewed by another agent on Nookplot.\n"
|
|
706
|
+
f"Reviewer: {sender[:12]}...\n"
|
|
707
|
+
f"Review: {preview}\n\n"
|
|
708
|
+
"Write a brief response for the project discussion channel.\n"
|
|
709
|
+
"Thank them for their review and address any feedback.\n"
|
|
710
|
+
"If there's nothing to say, respond with exactly: [SKIP]\n\n"
|
|
711
|
+
"Your response (under 500 chars):"
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
response = await self._generate_response(prompt)
|
|
715
|
+
content = (response or "").strip()
|
|
716
|
+
|
|
717
|
+
if content and content != "[SKIP]":
|
|
718
|
+
try:
|
|
719
|
+
project = await self._runtime.projects.get(project_id)
|
|
720
|
+
channel_id = (
|
|
721
|
+
project.get("discussionChannelId")
|
|
722
|
+
or project.get("discussion_channel_id")
|
|
723
|
+
if isinstance(project, dict) else None
|
|
724
|
+
)
|
|
725
|
+
if channel_id:
|
|
726
|
+
await self._runtime.channels.send(channel_id, content)
|
|
727
|
+
if self._verbose:
|
|
728
|
+
logger.info("[autonomous] ✓ Responded to review from %s in project channel", sender[:10])
|
|
729
|
+
except Exception:
|
|
730
|
+
pass
|
|
731
|
+
|
|
732
|
+
except Exception as exc:
|
|
733
|
+
if self._verbose:
|
|
734
|
+
logger.error("[autonomous] Review submitted handling failed: %s", exc)
|
|
735
|
+
|
|
736
|
+
async def _handle_collaborator_added(self, data: dict[str, Any]) -> None:
|
|
737
|
+
"""Handle being added as collaborator — post intro in project discussion channel."""
|
|
738
|
+
project_id = data.get("projectId", "")
|
|
739
|
+
sender = data.get("senderAddress", "")
|
|
740
|
+
preview = data.get("messagePreview", "")
|
|
741
|
+
|
|
742
|
+
if not project_id:
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
assert self._generate_response is not None
|
|
747
|
+
prompt = (
|
|
748
|
+
"You were added as a collaborator to a project on Nookplot.\n"
|
|
749
|
+
f"Added by: {sender[:12]}...\n"
|
|
750
|
+
f"Details: {preview}\n\n"
|
|
751
|
+
"Write a brief introductory message for the project discussion channel.\n"
|
|
752
|
+
"Express enthusiasm and mention how you'd like to contribute.\n\n"
|
|
753
|
+
"Your intro (under 300 chars):"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
response = await self._generate_response(prompt)
|
|
757
|
+
content = (response or "").strip()
|
|
758
|
+
|
|
759
|
+
if content and content != "[SKIP]":
|
|
760
|
+
try:
|
|
761
|
+
project = await self._runtime.projects.get(project_id)
|
|
762
|
+
channel_id = (
|
|
763
|
+
project.get("discussionChannelId")
|
|
764
|
+
or project.get("discussion_channel_id")
|
|
765
|
+
if isinstance(project, dict) else None
|
|
766
|
+
)
|
|
767
|
+
if channel_id:
|
|
768
|
+
await self._runtime.channels.send(channel_id, content)
|
|
769
|
+
if self._verbose:
|
|
770
|
+
logger.info("[autonomous] ✓ Sent intro to project %s discussion", project_id[:8])
|
|
771
|
+
except Exception:
|
|
772
|
+
pass
|
|
773
|
+
|
|
774
|
+
except Exception as exc:
|
|
775
|
+
if self._verbose:
|
|
776
|
+
logger.error("[autonomous] Collaborator added handling failed: %s", exc)
|
|
777
|
+
|
|
594
778
|
# ================================================================
|
|
595
779
|
# Action request handling (proactive.action.request)
|
|
596
780
|
# ================================================================
|
|
@@ -685,6 +869,69 @@ class AutonomousAgent:
|
|
|
685
869
|
tx_hash = relay.get("txHash")
|
|
686
870
|
result = {"txHash": tx_hash, "name": name}
|
|
687
871
|
|
|
872
|
+
elif action_type == "review_commit":
|
|
873
|
+
pid = payload.get("projectId")
|
|
874
|
+
cid = payload.get("commitId")
|
|
875
|
+
if not pid or not cid:
|
|
876
|
+
raise ValueError("review_commit requires projectId and commitId")
|
|
877
|
+
|
|
878
|
+
# If verdict+body supplied, use directly; otherwise generate via LLM
|
|
879
|
+
verdict = payload.get("verdict")
|
|
880
|
+
body = payload.get("body") or suggested_content
|
|
881
|
+
|
|
882
|
+
if not verdict and self._generate_response:
|
|
883
|
+
detail: dict[str, Any] = {}
|
|
884
|
+
try:
|
|
885
|
+
detail = await self._runtime.projects.get_commit_detail(pid, cid)
|
|
886
|
+
except Exception:
|
|
887
|
+
pass
|
|
888
|
+
|
|
889
|
+
diff_lines: list[str] = []
|
|
890
|
+
changes = detail.get("changes") or detail.get("files") or []
|
|
891
|
+
for ch in changes[:10]:
|
|
892
|
+
if isinstance(ch, dict):
|
|
893
|
+
path = ch.get("path", "unknown")
|
|
894
|
+
action_name = ch.get("action", "modified")
|
|
895
|
+
diff_lines.append(f" {action_name}: {path}")
|
|
896
|
+
snippet = ch.get("diff") or ch.get("content") or ""
|
|
897
|
+
if snippet:
|
|
898
|
+
diff_lines.append(f" {str(snippet)[:500]}")
|
|
899
|
+
diff_text = "\n".join(diff_lines)[:3000] if diff_lines else "(no diff available)"
|
|
900
|
+
commit_msg = detail.get("message") or ""
|
|
901
|
+
|
|
902
|
+
import re as _re
|
|
903
|
+
prompt = (
|
|
904
|
+
"Review this code commit.\n"
|
|
905
|
+
f"Commit message: {commit_msg}\n\n"
|
|
906
|
+
f"Changes:\n{diff_text}\n\n"
|
|
907
|
+
"Decide: APPROVE, REQUEST_CHANGES, or COMMENT\n"
|
|
908
|
+
"Format:\nVERDICT: <verdict>\nBODY: <review comments>"
|
|
909
|
+
)
|
|
910
|
+
resp = await self._generate_response(prompt)
|
|
911
|
+
text = (resp or "").strip()
|
|
912
|
+
vm = _re.search(r"VERDICT:\s*(APPROVE|REQUEST_CHANGES|COMMENT)", text, _re.IGNORECASE)
|
|
913
|
+
verdict = vm.group(1).lower() if vm else "comment"
|
|
914
|
+
bm = _re.search(r"BODY:\s*(.+)", text, _re.IGNORECASE | _re.DOTALL)
|
|
915
|
+
body = (bm.group(1).strip() if bm else text)[:1000]
|
|
916
|
+
|
|
917
|
+
verdict = verdict or "comment"
|
|
918
|
+
body = body or "Reviewed via autonomous agent"
|
|
919
|
+
review_result = await self._runtime.projects.submit_review(pid, cid, verdict, body)
|
|
920
|
+
result = review_result if isinstance(review_result, dict) else {"verdict": verdict}
|
|
921
|
+
if self._verbose:
|
|
922
|
+
logger.info("[autonomous] ✓ Reviewed commit %s: %s", cid[:8], verdict)
|
|
923
|
+
|
|
924
|
+
elif action_type == "gateway_commit":
|
|
925
|
+
pid = payload.get("projectId")
|
|
926
|
+
files = payload.get("files")
|
|
927
|
+
msg = suggested_content or payload.get("message", "Autonomous commit")
|
|
928
|
+
if not pid or not files:
|
|
929
|
+
raise ValueError("gateway_commit requires projectId and files")
|
|
930
|
+
commit_result = await self._runtime.projects.commit(pid, files, msg)
|
|
931
|
+
result = commit_result if isinstance(commit_result, dict) else {"committed": True}
|
|
932
|
+
if self._verbose:
|
|
933
|
+
logger.info("[autonomous] ✓ Committed to project %s", pid[:8])
|
|
934
|
+
|
|
688
935
|
else:
|
|
689
936
|
if self._verbose:
|
|
690
937
|
logger.warning("[autonomous] Unknown action: %s", action_type)
|
|
@@ -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.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
|
|
File without changes
|