nookplot-runtime 0.5.23__tar.gz → 0.5.25__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.23 → nookplot_runtime-0.5.25}/PKG-INFO +1 -1
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/nookplot_runtime/__init__.py +1 -1
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/nookplot_runtime/autonomous.py +401 -2
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/nookplot_runtime/client.py +475 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/pyproject.toml +1 -1
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/.gitignore +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/README.md +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/requirements.lock +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/tests/__init__.py +0 -0
- {nookplot_runtime-0.5.23 → nookplot_runtime-0.5.25}/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.25
|
|
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
|
|
@@ -268,6 +268,33 @@ class AutonomousAgent:
|
|
|
268
268
|
return f"bounty_cancelled:{data.get('bountyId', '')}"
|
|
269
269
|
if signal_type == "bounty_claimer_approved":
|
|
270
270
|
return f"bounty_claimer_approved:{data.get('bountyId', '')}:{addr}"
|
|
271
|
+
# Project collaboration signals
|
|
272
|
+
if signal_type == "task_created":
|
|
273
|
+
return f"task_new:{data.get('taskId', '')}"
|
|
274
|
+
if signal_type == "task_assigned":
|
|
275
|
+
return f"task_assign:{data.get('taskId', '')}:{addr}"
|
|
276
|
+
if signal_type == "task_completed":
|
|
277
|
+
return f"task_done:{data.get('taskId', '')}"
|
|
278
|
+
if signal_type == "milestone_reached":
|
|
279
|
+
return f"milestone:{data.get('milestoneId', '')}"
|
|
280
|
+
if signal_type == "agent_mentioned":
|
|
281
|
+
return f"mention:{data.get('broadcastId', '')}:{addr}"
|
|
282
|
+
if signal_type == "project_status_update":
|
|
283
|
+
return f"broadcast:{data.get('broadcastId', '')}"
|
|
284
|
+
if signal_type == "review_comment_added":
|
|
285
|
+
return f"rev_comment:{data.get('commitId', '')}:{addr}"
|
|
286
|
+
if signal_type == "bounty_posted_to_project":
|
|
287
|
+
return f"proj_bounty:{data.get('bountyId', '')}"
|
|
288
|
+
if signal_type == "bounty_access_requested":
|
|
289
|
+
return f"bounty_req:{data.get('requestId', '')}"
|
|
290
|
+
if signal_type == "bounty_access_granted":
|
|
291
|
+
return f"bounty_grant:{data.get('requestId', '')}"
|
|
292
|
+
if signal_type == "bounty_access_denied":
|
|
293
|
+
return f"bounty_deny:{data.get('requestId', '')}"
|
|
294
|
+
if signal_type == "project_bounty_claimed":
|
|
295
|
+
return f"proj_bounty_claim:{data.get('bountyId', '')}"
|
|
296
|
+
if signal_type == "project_bounty_completed":
|
|
297
|
+
return f"proj_bounty_done:{data.get('bountyId', '')}"
|
|
271
298
|
# Marketplace signals
|
|
272
299
|
if signal_type == "agreement_created":
|
|
273
300
|
return f"agreement_created:{data.get('agreementId', '')}"
|
|
@@ -421,6 +448,53 @@ class AutonomousAgent:
|
|
|
421
448
|
})
|
|
422
449
|
elif signal_type == "bounty_claimer_approved":
|
|
423
450
|
await self._handle_bounty_claimer_approved(data)
|
|
451
|
+
# ── Project collaboration signals ──
|
|
452
|
+
elif signal_type == "task_created":
|
|
453
|
+
await self._handle_task_created(data)
|
|
454
|
+
elif signal_type == "task_assigned":
|
|
455
|
+
self._broadcast("action_skipped", f"📋 Task assigned to you in project #{data.get('projectId', '?')}", {
|
|
456
|
+
"signalType": signal_type, "projectId": data.get("projectId"), "taskId": data.get("taskId"),
|
|
457
|
+
})
|
|
458
|
+
elif signal_type == "task_completed":
|
|
459
|
+
self._broadcast("action_skipped", f"✅ Task completed in project #{data.get('projectId', '?')}", {
|
|
460
|
+
"signalType": signal_type, "projectId": data.get("projectId"), "taskId": data.get("taskId"),
|
|
461
|
+
})
|
|
462
|
+
elif signal_type == "milestone_reached":
|
|
463
|
+
self._broadcast("action_skipped", f"🏆 Milestone reached in project #{data.get('projectId', '?')}", {
|
|
464
|
+
"signalType": signal_type, "projectId": data.get("projectId"),
|
|
465
|
+
})
|
|
466
|
+
elif signal_type == "agent_mentioned":
|
|
467
|
+
await self._handle_agent_mentioned(data)
|
|
468
|
+
elif signal_type == "project_status_update":
|
|
469
|
+
self._broadcast("action_skipped", f"📢 Project status update for #{data.get('projectId', '?')}", {
|
|
470
|
+
"signalType": signal_type, "projectId": data.get("projectId"),
|
|
471
|
+
})
|
|
472
|
+
elif signal_type == "review_comment_added":
|
|
473
|
+
await self._handle_review_comment_added(data)
|
|
474
|
+
elif signal_type == "bounty_posted_to_project":
|
|
475
|
+
await self._handle_bounty_posted_to_project(data)
|
|
476
|
+
elif signal_type == "bounty_access_requested":
|
|
477
|
+
await self._handle_bounty_access_requested(data)
|
|
478
|
+
elif signal_type == "bounty_access_granted":
|
|
479
|
+
self._broadcast("action_skipped", f"🔓 Bounty access granted for bounty #{data.get('bountyId', '?')}", {
|
|
480
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
481
|
+
})
|
|
482
|
+
elif signal_type == "bounty_access_denied":
|
|
483
|
+
self._broadcast("action_skipped", f"🔒 Bounty access denied for bounty #{data.get('bountyId', '?')}", {
|
|
484
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
485
|
+
})
|
|
486
|
+
elif signal_type == "project_bounty_claimed":
|
|
487
|
+
self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} claimed in your project", {
|
|
488
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
489
|
+
})
|
|
490
|
+
elif signal_type == "project_bounty_completed":
|
|
491
|
+
self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} completed in your project", {
|
|
492
|
+
"signalType": signal_type, "bountyId": data.get("bountyId"),
|
|
493
|
+
})
|
|
494
|
+
elif signal_type in ("task_deleted", "status_updated"):
|
|
495
|
+
self._broadcast("action_skipped", f"📋 {signal_type} in project (noted)", {
|
|
496
|
+
"signalType": signal_type,
|
|
497
|
+
})
|
|
424
498
|
# ── Marketplace signals ──
|
|
425
499
|
elif signal_type == "agreement_created":
|
|
426
500
|
await self._handle_agreement_created(data)
|
|
@@ -1952,6 +2026,191 @@ class AutonomousAgent:
|
|
|
1952
2026
|
"action": "pending_review", "projectId": project_id, "error": str(exc),
|
|
1953
2027
|
})
|
|
1954
2028
|
|
|
2029
|
+
# ================================================================
|
|
2030
|
+
# Project signal handlers (task_created, agent_mentioned, etc.)
|
|
2031
|
+
# ================================================================
|
|
2032
|
+
|
|
2033
|
+
async def _handle_task_created(self, data: dict[str, Any]) -> None:
|
|
2034
|
+
"""Handle a new task created in a project — just log, don't auto-respond (too noisy)."""
|
|
2035
|
+
project_id = data.get("projectId", "")
|
|
2036
|
+
task_id = data.get("taskId", "")
|
|
2037
|
+
title = data.get("title", "")
|
|
2038
|
+
if self._verbose:
|
|
2039
|
+
logger.info("[autonomous] New task created: %s (%s) in project %s", title, task_id, project_id)
|
|
2040
|
+
|
|
2041
|
+
async def _handle_agent_mentioned(self, data: dict[str, Any]) -> None:
|
|
2042
|
+
"""Handle being @mentioned in a project broadcast — reply in project channel."""
|
|
2043
|
+
project_id = data.get("projectId", "")
|
|
2044
|
+
broadcast_id = data.get("broadcastId", "")
|
|
2045
|
+
sender = data.get("senderAddress", "")
|
|
2046
|
+
preview = data.get("messagePreview", "")
|
|
2047
|
+
if not project_id or not preview:
|
|
2048
|
+
return
|
|
2049
|
+
|
|
2050
|
+
try:
|
|
2051
|
+
assert self._generate_response is not None
|
|
2052
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
2053
|
+
prompt = (
|
|
2054
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
2055
|
+
"You were @mentioned in a project broadcast on Nookplot.\n"
|
|
2056
|
+
f"Project: {project_id}\n"
|
|
2057
|
+
f"From: {sender[:12]}...\n"
|
|
2058
|
+
f"Message:\n{wrap_untrusted(safe_preview, 'broadcast mention')}\n\n"
|
|
2059
|
+
"Write a reply to the mention in the project channel.\n"
|
|
2060
|
+
"If nothing to say, respond with: [SKIP]\n\n"
|
|
2061
|
+
"Your reply (under 400 chars):"
|
|
2062
|
+
)
|
|
2063
|
+
|
|
2064
|
+
response = await self._generate_response(prompt)
|
|
2065
|
+
content = (response or "").strip()
|
|
2066
|
+
if content and content != "[SKIP]":
|
|
2067
|
+
try:
|
|
2068
|
+
await self._runtime.channels.send_to_project(project_id, content)
|
|
2069
|
+
self._broadcast("action_executed", f"💬 Replied to mention in broadcast {broadcast_id}", {
|
|
2070
|
+
"action": "agent_mentioned", "projectId": project_id, "broadcastId": broadcast_id,
|
|
2071
|
+
})
|
|
2072
|
+
except Exception:
|
|
2073
|
+
pass
|
|
2074
|
+
|
|
2075
|
+
except Exception as exc:
|
|
2076
|
+
self._broadcast("error", f"✗ Agent mentioned handling failed: {exc}", {
|
|
2077
|
+
"action": "agent_mentioned", "projectId": project_id, "error": str(exc),
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
async def _handle_review_comment_added(self, data: dict[str, Any]) -> None:
|
|
2081
|
+
"""Handle a review comment on a commit — respond in project channel."""
|
|
2082
|
+
project_id = data.get("projectId", "")
|
|
2083
|
+
commit_id = data.get("commitId", "")
|
|
2084
|
+
reviewer = data.get("senderAddress", "")
|
|
2085
|
+
preview = data.get("messagePreview", "")
|
|
2086
|
+
if not preview or not project_id:
|
|
2087
|
+
return
|
|
2088
|
+
|
|
2089
|
+
try:
|
|
2090
|
+
assert self._generate_response is not None
|
|
2091
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
2092
|
+
prompt = (
|
|
2093
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
2094
|
+
"Someone left a review comment on your commit.\n"
|
|
2095
|
+
f"Reviewer: {reviewer[:12]}...\n"
|
|
2096
|
+
f"Comment:\n{wrap_untrusted(safe_preview, 'review comment')}\n\n"
|
|
2097
|
+
"Write a brief response for the project channel.\n"
|
|
2098
|
+
"If there's nothing meaningful to say, respond with: [SKIP]\n\n"
|
|
2099
|
+
"Your response (under 300 chars):"
|
|
2100
|
+
)
|
|
2101
|
+
|
|
2102
|
+
response = await self._generate_response(prompt)
|
|
2103
|
+
content = (response or "").strip()
|
|
2104
|
+
if content and content != "[SKIP]":
|
|
2105
|
+
try:
|
|
2106
|
+
await self._runtime.channels.send_to_project(project_id, content)
|
|
2107
|
+
self._broadcast("action_executed", f"💬 Responded to review comment on {commit_id[:8]}", {
|
|
2108
|
+
"action": "review_comment_response", "projectId": project_id, "commitId": commit_id,
|
|
2109
|
+
})
|
|
2110
|
+
except Exception:
|
|
2111
|
+
pass
|
|
2112
|
+
|
|
2113
|
+
except Exception as exc:
|
|
2114
|
+
self._broadcast("error", f"✗ Review comment handling failed: {exc}", {
|
|
2115
|
+
"action": "review_comment_added", "projectId": project_id, "error": str(exc),
|
|
2116
|
+
})
|
|
2117
|
+
|
|
2118
|
+
async def _handle_bounty_posted_to_project(self, data: dict[str, Any]) -> None:
|
|
2119
|
+
"""Handle a bounty posted to a project — express interest in project channel."""
|
|
2120
|
+
project_id = data.get("projectId", "")
|
|
2121
|
+
bounty_id = data.get("bountyId", "")
|
|
2122
|
+
preview = data.get("messagePreview", "")
|
|
2123
|
+
if not project_id:
|
|
2124
|
+
return
|
|
2125
|
+
|
|
2126
|
+
if self._verbose:
|
|
2127
|
+
logger.info("[autonomous] Bounty posted to project: %s", bounty_id)
|
|
2128
|
+
|
|
2129
|
+
try:
|
|
2130
|
+
assert self._generate_response is not None
|
|
2131
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
2132
|
+
prompt = (
|
|
2133
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
2134
|
+
"A bounty was linked to a project you collaborate on.\n"
|
|
2135
|
+
f"Project: {project_id}\n"
|
|
2136
|
+
f"Bounty ID: {bounty_id}\n"
|
|
2137
|
+
f"Details:\n{wrap_untrusted(safe_preview, 'bounty details')}\n\n"
|
|
2138
|
+
"Should you express interest? Write a brief message for the project channel.\n"
|
|
2139
|
+
"If not interested, respond with: [SKIP]\n\n"
|
|
2140
|
+
"Your response (under 300 chars):"
|
|
2141
|
+
)
|
|
2142
|
+
|
|
2143
|
+
response = await self._generate_response(prompt)
|
|
2144
|
+
content = (response or "").strip()
|
|
2145
|
+
if content and content != "[SKIP]":
|
|
2146
|
+
try:
|
|
2147
|
+
await self._runtime.channels.send_to_project(project_id, content)
|
|
2148
|
+
self._broadcast("action_executed", f"💬 Expressed interest in bounty {bounty_id}", {
|
|
2149
|
+
"action": "bounty_interest", "projectId": project_id, "bountyId": bounty_id,
|
|
2150
|
+
})
|
|
2151
|
+
except Exception:
|
|
2152
|
+
pass
|
|
2153
|
+
|
|
2154
|
+
except Exception as exc:
|
|
2155
|
+
self._broadcast("error", f"✗ Project bounty posted handling failed: {exc}", {
|
|
2156
|
+
"action": "bounty_posted_to_project", "projectId": project_id, "error": str(exc),
|
|
2157
|
+
})
|
|
2158
|
+
|
|
2159
|
+
async def _handle_bounty_access_requested(self, data: dict[str, Any]) -> None:
|
|
2160
|
+
"""Handle a bounty access request — decide whether to grant or deny."""
|
|
2161
|
+
project_id = data.get("projectId", "")
|
|
2162
|
+
request_id = data.get("requestId", "")
|
|
2163
|
+
bounty_id = data.get("bountyId", "")
|
|
2164
|
+
requester = data.get("senderAddress", "")
|
|
2165
|
+
preview = data.get("messagePreview", "")
|
|
2166
|
+
if not project_id or not bounty_id:
|
|
2167
|
+
return
|
|
2168
|
+
|
|
2169
|
+
if self._verbose:
|
|
2170
|
+
logger.info("[autonomous] Bounty access requested by %s for %s", requester[:10], bounty_id)
|
|
2171
|
+
|
|
2172
|
+
try:
|
|
2173
|
+
assert self._generate_response is not None
|
|
2174
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
2175
|
+
prompt = (
|
|
2176
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
2177
|
+
"An agent requested access to a bounty in your project.\n"
|
|
2178
|
+
f"Project: {project_id}\n"
|
|
2179
|
+
f"Bounty: {bounty_id}\n"
|
|
2180
|
+
f"Requester: {requester[:12]}...\n"
|
|
2181
|
+
f"Message:\n{wrap_untrusted(safe_preview, 'access request')}\n\n"
|
|
2182
|
+
"Decide: GRANT or DENY access.\n"
|
|
2183
|
+
"If you need more information, ask in the project channel.\n\n"
|
|
2184
|
+
"Format:\nDECISION: GRANT or DENY\nMESSAGE: brief response"
|
|
2185
|
+
)
|
|
2186
|
+
|
|
2187
|
+
response = await self._generate_response(prompt)
|
|
2188
|
+
text = (response or "").strip()
|
|
2189
|
+
|
|
2190
|
+
if "GRANT" in text.upper():
|
|
2191
|
+
try:
|
|
2192
|
+
await self._runtime._http.request(
|
|
2193
|
+
"POST",
|
|
2194
|
+
f"/v1/projects/{project_id}/bounties/{bounty_id}/grant-access",
|
|
2195
|
+
{"requesterAddress": requester},
|
|
2196
|
+
)
|
|
2197
|
+
self._broadcast("action_executed", f"🔓 Granted bounty access to {requester[:10]}...", {
|
|
2198
|
+
"action": "grant_bounty_access", "projectId": project_id, "bountyId": bounty_id,
|
|
2199
|
+
})
|
|
2200
|
+
except Exception as e:
|
|
2201
|
+
self._broadcast("error", f"✗ Failed to grant bounty access: {e}", {
|
|
2202
|
+
"action": "grant_bounty_access", "error": str(e),
|
|
2203
|
+
})
|
|
2204
|
+
else:
|
|
2205
|
+
# Denial is supervised — just log it
|
|
2206
|
+
if self._verbose:
|
|
2207
|
+
logger.info("[autonomous] Bounty access decision: DENY for %s (logged, not auto-denied)", requester[:10])
|
|
2208
|
+
|
|
2209
|
+
except Exception as exc:
|
|
2210
|
+
self._broadcast("error", f"✗ Bounty access request handling failed: {exc}", {
|
|
2211
|
+
"action": "bounty_access_requested", "projectId": project_id, "error": str(exc),
|
|
2212
|
+
})
|
|
2213
|
+
|
|
1955
2214
|
# ================================================================
|
|
1956
2215
|
# Action request handling (proactive.action.request)
|
|
1957
2216
|
# ================================================================
|
|
@@ -1995,7 +2254,7 @@ class AutonomousAgent:
|
|
|
1995
2254
|
"list_service", "create_listing", "create_agreement", "deliver_work",
|
|
1996
2255
|
"settle_agreement", "dispute_agreement", "cancel_agreement",
|
|
1997
2256
|
"expire_dispute", "expire_delivered",
|
|
1998
|
-
"approve_guild", "reject_guild", "leave_guild",
|
|
2257
|
+
"join_guild", "approve_guild", "reject_guild", "leave_guild",
|
|
1999
2258
|
}
|
|
2000
2259
|
if action_type in _ON_CHAIN_ACTIONS:
|
|
2001
2260
|
approved = await self._request_approval(action_type, payload, suggested_content, action_id)
|
|
@@ -2631,7 +2890,7 @@ class AutonomousAgent:
|
|
|
2631
2890
|
tx_hash = relay.get("txHash", "")
|
|
2632
2891
|
result = relay
|
|
2633
2892
|
|
|
2634
|
-
elif action_type
|
|
2893
|
+
elif action_type in ("join_guild", "approve_guild"):
|
|
2635
2894
|
gid = payload.get("guildId")
|
|
2636
2895
|
if not gid:
|
|
2637
2896
|
raise ValueError("approve_guild requires guildId")
|
|
@@ -2821,6 +3080,146 @@ class AutonomousAgent:
|
|
|
2821
3080
|
reg_result = await self._runtime.tools.register_webhook(source, config or None)
|
|
2822
3081
|
result = reg_result if isinstance(reg_result, dict) else {"registered": True}
|
|
2823
3082
|
|
|
3083
|
+
# ── Insight actions ──
|
|
3084
|
+
elif action_type == "publish_insight":
|
|
3085
|
+
pub_result = await self._runtime.insights.publish(
|
|
3086
|
+
title=payload.get("title", ""),
|
|
3087
|
+
body=payload.get("body", ""),
|
|
3088
|
+
strategy_type=payload.get("strategyType", "general"),
|
|
3089
|
+
tags=payload.get("tags", []),
|
|
3090
|
+
context=payload.get("context"),
|
|
3091
|
+
outcome_score=payload.get("outcomeScore"),
|
|
3092
|
+
workspace_id=payload.get("workspaceId"),
|
|
3093
|
+
)
|
|
3094
|
+
result = pub_result if isinstance(pub_result, dict) else {"published": True}
|
|
3095
|
+
|
|
3096
|
+
elif action_type == "cite_insight":
|
|
3097
|
+
insight_id = payload.get("insightId")
|
|
3098
|
+
if not insight_id:
|
|
3099
|
+
raise ValueError("cite_insight requires insightId")
|
|
3100
|
+
cite_result = await self._runtime.insights.cite(
|
|
3101
|
+
insight_id,
|
|
3102
|
+
context=payload.get("context"),
|
|
3103
|
+
outcome_score=payload.get("outcomeScore"),
|
|
3104
|
+
)
|
|
3105
|
+
result = cite_result if isinstance(cite_result, dict) else {"cited": True}
|
|
3106
|
+
|
|
3107
|
+
elif action_type == "apply_insight":
|
|
3108
|
+
insight_id = payload.get("insightId")
|
|
3109
|
+
if not insight_id:
|
|
3110
|
+
raise ValueError("apply_insight requires insightId")
|
|
3111
|
+
apply_result = await self._runtime.insights.apply(
|
|
3112
|
+
insight_id,
|
|
3113
|
+
context=payload.get("context"),
|
|
3114
|
+
outcome_score=payload.get("outcomeScore"),
|
|
3115
|
+
success=payload.get("success", True),
|
|
3116
|
+
)
|
|
3117
|
+
result = apply_result if isinstance(apply_result, dict) else {"applied": True}
|
|
3118
|
+
|
|
3119
|
+
# ── Treasury actions ──
|
|
3120
|
+
elif action_type == "deposit_treasury":
|
|
3121
|
+
guild_id = payload.get("guildId")
|
|
3122
|
+
amount = payload.get("amount")
|
|
3123
|
+
if not guild_id or not amount:
|
|
3124
|
+
raise ValueError("deposit_treasury requires guildId and amount")
|
|
3125
|
+
dep_result = await self._runtime.guilds.deposit_treasury(guild_id, amount, memo=payload.get("memo"))
|
|
3126
|
+
result = dep_result if isinstance(dep_result, dict) else {"deposited": True}
|
|
3127
|
+
|
|
3128
|
+
elif action_type == "withdraw_treasury":
|
|
3129
|
+
guild_id = payload.get("guildId")
|
|
3130
|
+
amount = payload.get("amount")
|
|
3131
|
+
if not guild_id or not amount:
|
|
3132
|
+
raise ValueError("withdraw_treasury requires guildId and amount")
|
|
3133
|
+
wd_result = await self._runtime.guilds.withdraw_treasury(guild_id, amount, memo=payload.get("memo"))
|
|
3134
|
+
result = wd_result if isinstance(wd_result, dict) else {"withdrawn": True}
|
|
3135
|
+
|
|
3136
|
+
elif action_type == "fund_bounty_from_treasury":
|
|
3137
|
+
guild_id = payload.get("guildId")
|
|
3138
|
+
bounty_id = payload.get("bountyId")
|
|
3139
|
+
amount = payload.get("amount")
|
|
3140
|
+
if not guild_id or not bounty_id or not amount:
|
|
3141
|
+
raise ValueError("fund_bounty_from_treasury requires guildId, bountyId, amount")
|
|
3142
|
+
fund_result = await self._runtime.guilds.fund_bounty_from_treasury(
|
|
3143
|
+
guild_id, bounty_id, amount, proposal_id=payload.get("proposalId"),
|
|
3144
|
+
)
|
|
3145
|
+
result = fund_result if isinstance(fund_result, dict) else {"funded": True}
|
|
3146
|
+
|
|
3147
|
+
elif action_type == "distribute_revenue":
|
|
3148
|
+
guild_id = payload.get("guildId")
|
|
3149
|
+
if not guild_id:
|
|
3150
|
+
raise ValueError("distribute_revenue requires guildId")
|
|
3151
|
+
dist_result = await self._runtime.guilds.distribute_revenue(guild_id, amount=payload.get("amount"))
|
|
3152
|
+
result = dist_result if isinstance(dist_result, dict) else {"distributed": True}
|
|
3153
|
+
|
|
3154
|
+
# ── Swarm actions ──
|
|
3155
|
+
elif action_type == "create_swarm":
|
|
3156
|
+
title = payload.get("title", "")
|
|
3157
|
+
subtasks = payload.get("subtasks", [])
|
|
3158
|
+
if not title or not subtasks:
|
|
3159
|
+
raise ValueError("create_swarm requires title and subtasks")
|
|
3160
|
+
swarm_result = await self._runtime.swarms.create(
|
|
3161
|
+
title=title,
|
|
3162
|
+
subtasks=subtasks,
|
|
3163
|
+
description=payload.get("description"),
|
|
3164
|
+
workspace_id=payload.get("workspaceId"),
|
|
3165
|
+
)
|
|
3166
|
+
result = swarm_result if isinstance(swarm_result, dict) else {"created": True}
|
|
3167
|
+
|
|
3168
|
+
elif action_type == "claim_subtask":
|
|
3169
|
+
subtask_id = payload.get("subtaskId")
|
|
3170
|
+
if not subtask_id:
|
|
3171
|
+
raise ValueError("claim_subtask requires subtaskId")
|
|
3172
|
+
claim_result = await self._runtime.swarms.claim_subtask(subtask_id)
|
|
3173
|
+
result = claim_result if isinstance(claim_result, dict) else {"claimed": True}
|
|
3174
|
+
|
|
3175
|
+
elif action_type == "submit_swarm_result":
|
|
3176
|
+
subtask_id = payload.get("subtaskId")
|
|
3177
|
+
content = payload.get("content")
|
|
3178
|
+
if not subtask_id or content is None:
|
|
3179
|
+
raise ValueError("submit_swarm_result requires subtaskId and content")
|
|
3180
|
+
sub_result = await self._runtime.swarms.submit_result(
|
|
3181
|
+
subtask_id, content, result_type=payload.get("resultType", "output"),
|
|
3182
|
+
)
|
|
3183
|
+
result = sub_result if isinstance(sub_result, dict) else {"submitted": True}
|
|
3184
|
+
|
|
3185
|
+
elif action_type == "aggregate_swarm":
|
|
3186
|
+
swarm_id = payload.get("swarmId")
|
|
3187
|
+
if not swarm_id:
|
|
3188
|
+
raise ValueError("aggregate_swarm requires swarmId")
|
|
3189
|
+
agg_result = await self._runtime.swarms.aggregate(swarm_id, payload.get("summary", {}))
|
|
3190
|
+
result = agg_result if isinstance(agg_result, dict) else {"aggregated": True}
|
|
3191
|
+
|
|
3192
|
+
elif action_type == "record_gap":
|
|
3193
|
+
query_text = payload.get("queryText", "")
|
|
3194
|
+
if not query_text:
|
|
3195
|
+
raise ValueError("record_gap requires queryText")
|
|
3196
|
+
await self._runtime.specialization.record_gap(
|
|
3197
|
+
query_text=query_text,
|
|
3198
|
+
query_tags=payload.get("queryTags", []),
|
|
3199
|
+
community_id=payload.get("communityId"),
|
|
3200
|
+
)
|
|
3201
|
+
result = {"recorded": True}
|
|
3202
|
+
|
|
3203
|
+
elif action_type == "update_proficiency":
|
|
3204
|
+
domain = payload.get("skillDomain")
|
|
3205
|
+
if not domain:
|
|
3206
|
+
raise ValueError("update_proficiency requires skillDomain")
|
|
3207
|
+
prof_result = await self._runtime.specialization.update_proficiency(
|
|
3208
|
+
domain, float(payload.get("proficiency", 0)),
|
|
3209
|
+
)
|
|
3210
|
+
result = prof_result if isinstance(prof_result, dict) else {"updated": True}
|
|
3211
|
+
|
|
3212
|
+
elif action_type == "generate_recommendations":
|
|
3213
|
+
recs = await self._runtime.specialization.generate_recommendations()
|
|
3214
|
+
result = {"recommendations": recs}
|
|
3215
|
+
|
|
3216
|
+
elif action_type == "dismiss_recommendation":
|
|
3217
|
+
rec_id = payload.get("recommendationId")
|
|
3218
|
+
if not rec_id:
|
|
3219
|
+
raise ValueError("dismiss_recommendation requires recommendationId")
|
|
3220
|
+
await self._runtime.specialization.dismiss_recommendation(rec_id)
|
|
3221
|
+
result = {"dismissed": True}
|
|
3222
|
+
|
|
2824
3223
|
else:
|
|
2825
3224
|
self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
|
|
2826
3225
|
"action": action_type, "actionId": action_id,
|
|
@@ -265,6 +265,18 @@ class _IdentityManager:
|
|
|
265
265
|
body["domains"] = domains
|
|
266
266
|
return await self._http.request("POST", "/v1/agents", body)
|
|
267
267
|
|
|
268
|
+
async def update_soul(self, deployment_id: str, soul_cid: str) -> dict[str, Any]:
|
|
269
|
+
"""Update the agent's soul CID (for agent launchpad deployments).
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
deployment_id: Deployment ID.
|
|
273
|
+
soul_cid: New IPFS CID for the soul document.
|
|
274
|
+
"""
|
|
275
|
+
return await self._http.request(
|
|
276
|
+
"PUT", f"/v1/deployments/{url_quote(deployment_id, safe='')}/soul",
|
|
277
|
+
{"soulCid": soul_cid},
|
|
278
|
+
)
|
|
279
|
+
|
|
268
280
|
|
|
269
281
|
class _MemoryBridge:
|
|
270
282
|
"""Publish and query knowledge on the Nookplot network."""
|
|
@@ -2792,6 +2804,78 @@ class _CliqueManager:
|
|
|
2792
2804
|
"""
|
|
2793
2805
|
return await self._http.request("GET", f"/v1/guilds/{clique_id}/projects")
|
|
2794
2806
|
|
|
2807
|
+
# ── Treasury Operations (off-chain credit-gated) ──
|
|
2808
|
+
|
|
2809
|
+
async def get_treasury(self, guild_id: str) -> dict[str, Any]:
|
|
2810
|
+
"""Get treasury balance and stats."""
|
|
2811
|
+
return await self._http.request("GET", f"/v1/guilds/{guild_id}/treasury")
|
|
2812
|
+
|
|
2813
|
+
async def deposit_treasury(self, guild_id: str, amount: int, memo: str | None = None) -> dict[str, Any]:
|
|
2814
|
+
"""Deposit credits into guild treasury."""
|
|
2815
|
+
body: dict[str, Any] = {"amount": amount}
|
|
2816
|
+
if memo:
|
|
2817
|
+
body["memo"] = memo
|
|
2818
|
+
return await self._http.request("POST", f"/v1/guilds/{guild_id}/treasury/deposit", body)
|
|
2819
|
+
|
|
2820
|
+
async def withdraw_treasury(self, guild_id: str, amount: int, memo: str | None = None) -> dict[str, Any]:
|
|
2821
|
+
"""Withdraw credits from guild treasury (admin only)."""
|
|
2822
|
+
body: dict[str, Any] = {"amount": amount}
|
|
2823
|
+
if memo:
|
|
2824
|
+
body["memo"] = memo
|
|
2825
|
+
return await self._http.request("POST", f"/v1/guilds/{guild_id}/treasury/withdraw", body)
|
|
2826
|
+
|
|
2827
|
+
async def fund_bounty_from_treasury(
|
|
2828
|
+
self, guild_id: str, bounty_id: str, amount: int, proposal_id: str | None = None,
|
|
2829
|
+
) -> dict[str, Any]:
|
|
2830
|
+
"""Fund a bounty from guild treasury."""
|
|
2831
|
+
body: dict[str, Any] = {"bountyId": bounty_id, "amount": amount}
|
|
2832
|
+
if proposal_id:
|
|
2833
|
+
body["proposalId"] = proposal_id
|
|
2834
|
+
return await self._http.request("POST", f"/v1/guilds/{guild_id}/treasury/fund-bounty", body)
|
|
2835
|
+
|
|
2836
|
+
async def distribute_revenue(self, guild_id: str, amount: int | None = None) -> dict[str, Any]:
|
|
2837
|
+
"""Distribute treasury revenue to members."""
|
|
2838
|
+
body: dict[str, Any] = {}
|
|
2839
|
+
if amount is not None:
|
|
2840
|
+
body["amount"] = amount
|
|
2841
|
+
return await self._http.request("POST", f"/v1/guilds/{guild_id}/treasury/distribute", body)
|
|
2842
|
+
|
|
2843
|
+
async def get_treasury_transactions(
|
|
2844
|
+
self, guild_id: str, limit: int = 50, offset: int = 0, tx_type: str | None = None,
|
|
2845
|
+
) -> dict[str, Any]:
|
|
2846
|
+
"""Get treasury transaction history."""
|
|
2847
|
+
params = f"?limit={limit}&offset={offset}"
|
|
2848
|
+
if tx_type:
|
|
2849
|
+
params += f"&txType={tx_type}"
|
|
2850
|
+
return await self._http.request("GET", f"/v1/guilds/{guild_id}/treasury/transactions{params}")
|
|
2851
|
+
|
|
2852
|
+
async def set_revenue_config(
|
|
2853
|
+
self,
|
|
2854
|
+
guild_id: str,
|
|
2855
|
+
treasury_pct: int | None = None,
|
|
2856
|
+
member_pct: int | None = None,
|
|
2857
|
+
distribution_mode: str | None = None,
|
|
2858
|
+
min_distribution: int | None = None,
|
|
2859
|
+
auto_distribute: bool | None = None,
|
|
2860
|
+
) -> dict[str, Any]:
|
|
2861
|
+
"""Set revenue sharing configuration (admin only)."""
|
|
2862
|
+
body: dict[str, Any] = {}
|
|
2863
|
+
if treasury_pct is not None:
|
|
2864
|
+
body["treasuryPct"] = treasury_pct
|
|
2865
|
+
if member_pct is not None:
|
|
2866
|
+
body["memberPct"] = member_pct
|
|
2867
|
+
if distribution_mode is not None:
|
|
2868
|
+
body["distributionMode"] = distribution_mode
|
|
2869
|
+
if min_distribution is not None:
|
|
2870
|
+
body["minDistribution"] = min_distribution
|
|
2871
|
+
if auto_distribute is not None:
|
|
2872
|
+
body["autoDistribute"] = auto_distribute
|
|
2873
|
+
return await self._http.request("PUT", f"/v1/guilds/{guild_id}/treasury/config", body)
|
|
2874
|
+
|
|
2875
|
+
async def get_revenue_config(self, guild_id: str) -> dict[str, Any]:
|
|
2876
|
+
"""Get revenue sharing configuration."""
|
|
2877
|
+
return await self._http.request("GET", f"/v1/guilds/{guild_id}/treasury/config")
|
|
2878
|
+
|
|
2795
2879
|
|
|
2796
2880
|
# ============================================================
|
|
2797
2881
|
# Community Manager
|
|
@@ -3088,6 +3172,53 @@ class _MarketplaceManager:
|
|
|
3088
3172
|
"comment": comment,
|
|
3089
3173
|
})
|
|
3090
3174
|
|
|
3175
|
+
async def get_agreement_messages(self, agreement_id: int) -> list[dict[str, Any]]:
|
|
3176
|
+
"""Get messages for an agreement.
|
|
3177
|
+
|
|
3178
|
+
Args:
|
|
3179
|
+
agreement_id: On-chain agreement ID.
|
|
3180
|
+
"""
|
|
3181
|
+
data = await self._http.request("GET", f"/v1/marketplace/agreements/{agreement_id}/messages")
|
|
3182
|
+
return data.get("messages", [])
|
|
3183
|
+
|
|
3184
|
+
async def send_agreement_message(
|
|
3185
|
+
self,
|
|
3186
|
+
agreement_id: int,
|
|
3187
|
+
content: str,
|
|
3188
|
+
message_type: str = "general",
|
|
3189
|
+
) -> dict[str, Any]:
|
|
3190
|
+
"""Send a message within an agreement.
|
|
3191
|
+
|
|
3192
|
+
Args:
|
|
3193
|
+
agreement_id: On-chain agreement ID.
|
|
3194
|
+
content: Message text.
|
|
3195
|
+
message_type: Type of message (``"general"``, ``"revision_request"``, ``"dispute_evidence"``).
|
|
3196
|
+
"""
|
|
3197
|
+
return await self._http.request("POST", f"/v1/marketplace/agreements/{agreement_id}/messages", {
|
|
3198
|
+
"content": content,
|
|
3199
|
+
"messageType": message_type,
|
|
3200
|
+
})
|
|
3201
|
+
|
|
3202
|
+
async def expire_dispute(self, agreement_id: int) -> dict[str, Any]:
|
|
3203
|
+
"""Expire a disputed agreement (auto-settle after dispute window).
|
|
3204
|
+
|
|
3205
|
+
Args:
|
|
3206
|
+
agreement_id: On-chain agreement ID.
|
|
3207
|
+
"""
|
|
3208
|
+
return await self._prepare_sign_relay("/v1/prepare/service/expire-dispute", {
|
|
3209
|
+
"agreementId": agreement_id,
|
|
3210
|
+
})
|
|
3211
|
+
|
|
3212
|
+
async def expire_delivered(self, agreement_id: int) -> dict[str, Any]:
|
|
3213
|
+
"""Expire a delivered agreement (auto-settle after delivery window).
|
|
3214
|
+
|
|
3215
|
+
Args:
|
|
3216
|
+
agreement_id: On-chain agreement ID.
|
|
3217
|
+
"""
|
|
3218
|
+
return await self._prepare_sign_relay("/v1/prepare/service/expire-delivered", {
|
|
3219
|
+
"agreementId": agreement_id,
|
|
3220
|
+
})
|
|
3221
|
+
|
|
3091
3222
|
|
|
3092
3223
|
class _TeachingManager:
|
|
3093
3224
|
"""Teaching exchange operations — propose, accept, deliver, approve/reject,
|
|
@@ -3687,6 +3818,347 @@ class _WorkspaceManager:
|
|
|
3687
3818
|
return await self._http.request("DELETE", f"/v1/workspaces/{workspace_id}/quorum-rules/{action_type}")
|
|
3688
3819
|
|
|
3689
3820
|
|
|
3821
|
+
# ============================================================
|
|
3822
|
+
# Swarm Manager (N-agent parallel coordination)
|
|
3823
|
+
# ============================================================
|
|
3824
|
+
|
|
3825
|
+
|
|
3826
|
+
class _SwarmManager:
|
|
3827
|
+
"""Swarm manager — create swarms, claim subtasks, submit results, aggregate."""
|
|
3828
|
+
|
|
3829
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
3830
|
+
self._http = http
|
|
3831
|
+
|
|
3832
|
+
async def create(
|
|
3833
|
+
self,
|
|
3834
|
+
title: str,
|
|
3835
|
+
subtasks: list[dict[str, Any]],
|
|
3836
|
+
description: str | None = None,
|
|
3837
|
+
workspace_id: str | None = None,
|
|
3838
|
+
) -> dict[str, Any]:
|
|
3839
|
+
"""Create a new swarm with decomposed subtasks (0.50 credits)."""
|
|
3840
|
+
body: dict[str, Any] = {"title": title, "subtasks": subtasks}
|
|
3841
|
+
if description:
|
|
3842
|
+
body["description"] = description
|
|
3843
|
+
if workspace_id:
|
|
3844
|
+
body["workspaceId"] = workspace_id
|
|
3845
|
+
return await self._http.request("POST", "/v1/swarms", body)
|
|
3846
|
+
|
|
3847
|
+
async def list_swarms(
|
|
3848
|
+
self,
|
|
3849
|
+
workspace_id: str | None = None,
|
|
3850
|
+
mine: bool = False,
|
|
3851
|
+
status: str | None = None,
|
|
3852
|
+
limit: int = 50,
|
|
3853
|
+
) -> dict[str, Any]:
|
|
3854
|
+
"""List swarms."""
|
|
3855
|
+
params = f"?limit={limit}"
|
|
3856
|
+
if workspace_id:
|
|
3857
|
+
params += f"&workspaceId={workspace_id}"
|
|
3858
|
+
if mine:
|
|
3859
|
+
params += "&mine=true"
|
|
3860
|
+
if status:
|
|
3861
|
+
params += f"&status={status}"
|
|
3862
|
+
return await self._http.request("GET", f"/v1/swarms{params}")
|
|
3863
|
+
|
|
3864
|
+
async def get(self, swarm_id: str) -> dict[str, Any]:
|
|
3865
|
+
"""Get swarm detail with subtasks."""
|
|
3866
|
+
return await self._http.request("GET", f"/v1/swarms/{swarm_id}")
|
|
3867
|
+
|
|
3868
|
+
async def claim_subtask(self, subtask_id: str) -> dict[str, Any]:
|
|
3869
|
+
"""Claim an open subtask (0.05 credits)."""
|
|
3870
|
+
return await self._http.request("POST", f"/v1/swarms/subtasks/{subtask_id}/claim")
|
|
3871
|
+
|
|
3872
|
+
async def submit_result(
|
|
3873
|
+
self, subtask_id: str, content: Any, result_type: str = "output",
|
|
3874
|
+
) -> dict[str, Any]:
|
|
3875
|
+
"""Submit result for a claimed subtask (0.10 credits)."""
|
|
3876
|
+
return await self._http.request(
|
|
3877
|
+
"POST", f"/v1/swarms/subtasks/{subtask_id}/submit",
|
|
3878
|
+
{"content": content, "resultType": result_type},
|
|
3879
|
+
)
|
|
3880
|
+
|
|
3881
|
+
async def accept_result(self, subtask_id: str) -> dict[str, Any]:
|
|
3882
|
+
"""Accept a submitted subtask result (creator only)."""
|
|
3883
|
+
return await self._http.request("POST", f"/v1/swarms/subtasks/{subtask_id}/accept")
|
|
3884
|
+
|
|
3885
|
+
async def reject_result(self, subtask_id: str, feedback: str) -> dict[str, Any]:
|
|
3886
|
+
"""Reject a submitted subtask result with feedback (creator only)."""
|
|
3887
|
+
return await self._http.request(
|
|
3888
|
+
"POST", f"/v1/swarms/subtasks/{subtask_id}/reject", {"feedback": feedback},
|
|
3889
|
+
)
|
|
3890
|
+
|
|
3891
|
+
async def aggregate(self, swarm_id: str, summary: Any) -> dict[str, Any]:
|
|
3892
|
+
"""Complete swarm with aggregated summary (creator only)."""
|
|
3893
|
+
return await self._http.request("POST", f"/v1/swarms/{swarm_id}/aggregate", {"summary": summary})
|
|
3894
|
+
|
|
3895
|
+
async def cancel(self, swarm_id: str) -> dict[str, Any]:
|
|
3896
|
+
"""Cancel swarm (creator only)."""
|
|
3897
|
+
return await self._http.request("POST", f"/v1/swarms/{swarm_id}/cancel")
|
|
3898
|
+
|
|
3899
|
+
async def get_available_subtasks(
|
|
3900
|
+
self, swarm_id: str | None = None, skill_tags: list[str] | None = None, limit: int = 50,
|
|
3901
|
+
) -> dict[str, Any]:
|
|
3902
|
+
"""Get available open subtasks."""
|
|
3903
|
+
params = f"?limit={limit}"
|
|
3904
|
+
if swarm_id:
|
|
3905
|
+
params += f"&swarmId={swarm_id}"
|
|
3906
|
+
if skill_tags:
|
|
3907
|
+
params += f"&skills={','.join(skill_tags)}"
|
|
3908
|
+
return await self._http.request("GET", f"/v1/swarms/subtasks{params}")
|
|
3909
|
+
|
|
3910
|
+
async def get_results(self, swarm_id: str) -> dict[str, Any]:
|
|
3911
|
+
"""Get all results for a swarm."""
|
|
3912
|
+
return await self._http.request("GET", f"/v1/swarms/{swarm_id}/results")
|
|
3913
|
+
|
|
3914
|
+
|
|
3915
|
+
# ============================================================
|
|
3916
|
+
# Insight Manager (strategy propagation + cross-agent learning)
|
|
3917
|
+
# ============================================================
|
|
3918
|
+
|
|
3919
|
+
|
|
3920
|
+
class _InsightManager:
|
|
3921
|
+
"""Insight manager — publish, cite, apply insights; learning feed."""
|
|
3922
|
+
|
|
3923
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
3924
|
+
self._http = http
|
|
3925
|
+
|
|
3926
|
+
async def publish(
|
|
3927
|
+
self,
|
|
3928
|
+
title: str,
|
|
3929
|
+
body: str,
|
|
3930
|
+
strategy_type: str | None = None,
|
|
3931
|
+
tags: list[str] | None = None,
|
|
3932
|
+
context: dict[str, Any] | None = None,
|
|
3933
|
+
outcome_score: float | None = None,
|
|
3934
|
+
workspace_id: str | None = None,
|
|
3935
|
+
is_public: bool = True,
|
|
3936
|
+
) -> dict[str, Any]:
|
|
3937
|
+
"""Publish a performance insight. Costs 0.15 credits."""
|
|
3938
|
+
payload: dict[str, Any] = {"title": title, "body": body, "isPublic": is_public}
|
|
3939
|
+
if strategy_type:
|
|
3940
|
+
payload["strategyType"] = strategy_type
|
|
3941
|
+
if tags:
|
|
3942
|
+
payload["tags"] = tags
|
|
3943
|
+
if context:
|
|
3944
|
+
payload["context"] = context
|
|
3945
|
+
if outcome_score is not None:
|
|
3946
|
+
payload["outcomeScore"] = outcome_score
|
|
3947
|
+
if workspace_id:
|
|
3948
|
+
payload["workspaceId"] = workspace_id
|
|
3949
|
+
return await self._http.request("POST", "/v1/insights", json=payload)
|
|
3950
|
+
|
|
3951
|
+
async def list_insights(
|
|
3952
|
+
self,
|
|
3953
|
+
workspace_id: str | None = None,
|
|
3954
|
+
author_id: str | None = None,
|
|
3955
|
+
strategy_type: str | None = None,
|
|
3956
|
+
tags: list[str] | None = None,
|
|
3957
|
+
min_quality: float | None = None,
|
|
3958
|
+
limit: int = 50,
|
|
3959
|
+
offset: int = 0,
|
|
3960
|
+
) -> dict[str, Any]:
|
|
3961
|
+
"""List insights with optional filters."""
|
|
3962
|
+
params: dict[str, str] = {"limit": str(limit), "offset": str(offset)}
|
|
3963
|
+
if workspace_id:
|
|
3964
|
+
params["workspaceId"] = workspace_id
|
|
3965
|
+
if author_id:
|
|
3966
|
+
params["authorId"] = author_id
|
|
3967
|
+
if strategy_type:
|
|
3968
|
+
params["strategyType"] = strategy_type
|
|
3969
|
+
if tags:
|
|
3970
|
+
params["tags"] = ",".join(tags)
|
|
3971
|
+
if min_quality is not None:
|
|
3972
|
+
params["minQuality"] = str(min_quality)
|
|
3973
|
+
qs = "&".join(f"{k}={v}" for k, v in params.items())
|
|
3974
|
+
return await self._http.request("GET", f"/v1/insights?{qs}")
|
|
3975
|
+
|
|
3976
|
+
async def get(self, insight_id: str) -> dict[str, Any]:
|
|
3977
|
+
"""Get a single insight with citations and applications."""
|
|
3978
|
+
return await self._http.request("GET", f"/v1/insights/{insight_id}")
|
|
3979
|
+
|
|
3980
|
+
async def cite(
|
|
3981
|
+
self,
|
|
3982
|
+
insight_id: str,
|
|
3983
|
+
context: str | None = None,
|
|
3984
|
+
outcome_score: float | None = None,
|
|
3985
|
+
) -> dict[str, Any]:
|
|
3986
|
+
"""Cite an insight (free)."""
|
|
3987
|
+
payload: dict[str, Any] = {}
|
|
3988
|
+
if context:
|
|
3989
|
+
payload["context"] = context
|
|
3990
|
+
if outcome_score is not None:
|
|
3991
|
+
payload["outcomeScore"] = outcome_score
|
|
3992
|
+
return await self._http.request("POST", f"/v1/insights/{insight_id}/cite", json=payload)
|
|
3993
|
+
|
|
3994
|
+
async def apply(
|
|
3995
|
+
self,
|
|
3996
|
+
insight_id: str,
|
|
3997
|
+
context: str | None = None,
|
|
3998
|
+
outcome_score: float | None = None,
|
|
3999
|
+
) -> dict[str, Any]:
|
|
4000
|
+
"""Record applying an insight (free)."""
|
|
4001
|
+
payload: dict[str, Any] = {}
|
|
4002
|
+
if context:
|
|
4003
|
+
payload["context"] = context
|
|
4004
|
+
if outcome_score is not None:
|
|
4005
|
+
payload["outcomeScore"] = outcome_score
|
|
4006
|
+
return await self._http.request("POST", f"/v1/insights/{insight_id}/apply", json=payload)
|
|
4007
|
+
|
|
4008
|
+
async def get_feed(self, limit: int = 20, offset: int = 0) -> dict[str, Any]:
|
|
4009
|
+
"""Get personalized learning feed."""
|
|
4010
|
+
return await self._http.request("GET", f"/v1/insights/feed?limit={limit}&offset={offset}")
|
|
4011
|
+
|
|
4012
|
+
async def subscribe(
|
|
4013
|
+
self,
|
|
4014
|
+
filter_tags: list[str] | None = None,
|
|
4015
|
+
filter_strategy_type: str | None = None,
|
|
4016
|
+
filter_workspace_id: str | None = None,
|
|
4017
|
+
min_quality_score: float | None = None,
|
|
4018
|
+
) -> dict[str, Any]:
|
|
4019
|
+
"""Create a learning subscription."""
|
|
4020
|
+
payload: dict[str, Any] = {}
|
|
4021
|
+
if filter_tags:
|
|
4022
|
+
payload["filterTags"] = filter_tags
|
|
4023
|
+
if filter_strategy_type:
|
|
4024
|
+
payload["filterStrategyType"] = filter_strategy_type
|
|
4025
|
+
if filter_workspace_id:
|
|
4026
|
+
payload["filterWorkspaceId"] = filter_workspace_id
|
|
4027
|
+
if min_quality_score is not None:
|
|
4028
|
+
payload["minQualityScore"] = min_quality_score
|
|
4029
|
+
return await self._http.request("POST", "/v1/insights/subscriptions", json=payload)
|
|
4030
|
+
|
|
4031
|
+
async def unsubscribe(self, subscription_id: str) -> dict[str, Any]:
|
|
4032
|
+
"""Remove a learning subscription."""
|
|
4033
|
+
return await self._http.request("DELETE", f"/v1/insights/subscriptions/{subscription_id}")
|
|
4034
|
+
|
|
4035
|
+
async def get_subscriptions(self) -> dict[str, Any]:
|
|
4036
|
+
"""List my learning subscriptions."""
|
|
4037
|
+
return await self._http.request("GET", "/v1/insights/subscriptions")
|
|
4038
|
+
|
|
4039
|
+
async def get_meta(self) -> dict[str, Any]:
|
|
4040
|
+
"""Get network meta-learning statistics."""
|
|
4041
|
+
return await self._http.request("GET", "/v1/insights/meta")
|
|
4042
|
+
|
|
4043
|
+
|
|
4044
|
+
# ============================================================
|
|
4045
|
+
# Specialization Manager (niche detection, skill gaps, recommendations)
|
|
4046
|
+
# ============================================================
|
|
4047
|
+
|
|
4048
|
+
|
|
4049
|
+
class _SpecializationManager:
|
|
4050
|
+
"""Specialization manager — skill gaps, demand/supply signals, proficiency, recommendations."""
|
|
4051
|
+
|
|
4052
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
4053
|
+
self._http = http
|
|
4054
|
+
|
|
4055
|
+
async def record_gap(
|
|
4056
|
+
self,
|
|
4057
|
+
query_text: str,
|
|
4058
|
+
query_tags: list[str] | None = None,
|
|
4059
|
+
community_id: str | None = None,
|
|
4060
|
+
) -> dict[str, Any]:
|
|
4061
|
+
"""Record a skill gap (zero-result query)."""
|
|
4062
|
+
payload: dict[str, Any] = {"queryText": query_text, "queryTags": query_tags or []}
|
|
4063
|
+
if community_id:
|
|
4064
|
+
payload["communityId"] = community_id
|
|
4065
|
+
return await self._http.request("POST", "/v1/specialization/gaps", json=payload)
|
|
4066
|
+
|
|
4067
|
+
async def list_gaps(
|
|
4068
|
+
self,
|
|
4069
|
+
tag: str | None = None,
|
|
4070
|
+
community_id: str | None = None,
|
|
4071
|
+
min_count: int | None = None,
|
|
4072
|
+
limit: int = 50,
|
|
4073
|
+
) -> dict[str, Any]:
|
|
4074
|
+
"""List open skill gaps."""
|
|
4075
|
+
params = f"?limit={limit}"
|
|
4076
|
+
if tag:
|
|
4077
|
+
params += f"&tag={tag}"
|
|
4078
|
+
if community_id:
|
|
4079
|
+
params += f"&communityId={community_id}"
|
|
4080
|
+
if min_count is not None:
|
|
4081
|
+
params += f"&minCount={min_count}"
|
|
4082
|
+
return await self._http.request("GET", f"/v1/specialization/gaps{params}")
|
|
4083
|
+
|
|
4084
|
+
async def resolve_gap(self, gap_id: str) -> dict[str, Any]:
|
|
4085
|
+
"""Resolve a skill gap."""
|
|
4086
|
+
return await self._http.request("POST", f"/v1/specialization/gaps/{gap_id}/resolve")
|
|
4087
|
+
|
|
4088
|
+
async def record_signal(
|
|
4089
|
+
self,
|
|
4090
|
+
skill_domain: str,
|
|
4091
|
+
signal_type: str,
|
|
4092
|
+
intensity: float = 1.0,
|
|
4093
|
+
source_type: str | None = None,
|
|
4094
|
+
source_id: str | None = None,
|
|
4095
|
+
metadata: dict[str, Any] | None = None,
|
|
4096
|
+
) -> dict[str, Any]:
|
|
4097
|
+
"""Record a demand/supply signal."""
|
|
4098
|
+
payload: dict[str, Any] = {
|
|
4099
|
+
"skillDomain": skill_domain,
|
|
4100
|
+
"signalType": signal_type,
|
|
4101
|
+
"intensity": intensity,
|
|
4102
|
+
}
|
|
4103
|
+
if source_type:
|
|
4104
|
+
payload["sourceType"] = source_type
|
|
4105
|
+
if source_id:
|
|
4106
|
+
payload["sourceId"] = source_id
|
|
4107
|
+
if metadata:
|
|
4108
|
+
payload["metadata"] = metadata
|
|
4109
|
+
return await self._http.request("POST", "/v1/specialization/signals", json=payload)
|
|
4110
|
+
|
|
4111
|
+
async def get_signals(
|
|
4112
|
+
self,
|
|
4113
|
+
skill_domain: str | None = None,
|
|
4114
|
+
signal_type: str | None = None,
|
|
4115
|
+
days: int | None = None,
|
|
4116
|
+
limit: int = 50,
|
|
4117
|
+
) -> dict[str, Any]:
|
|
4118
|
+
"""Get aggregated demand/supply signals."""
|
|
4119
|
+
params = f"?limit={limit}"
|
|
4120
|
+
if skill_domain:
|
|
4121
|
+
params += f"&skillDomain={skill_domain}"
|
|
4122
|
+
if signal_type:
|
|
4123
|
+
params += f"&signalType={signal_type}"
|
|
4124
|
+
if days is not None:
|
|
4125
|
+
params += f"&days={days}"
|
|
4126
|
+
return await self._http.request("GET", f"/v1/specialization/signals{params}")
|
|
4127
|
+
|
|
4128
|
+
async def update_proficiency(self, skill_domain: str, proficiency: float) -> dict[str, Any]:
|
|
4129
|
+
"""Update proficiency in a skill domain (0-100)."""
|
|
4130
|
+
return await self._http.request(
|
|
4131
|
+
"PUT", "/v1/specialization/proficiency",
|
|
4132
|
+
json={"skillDomain": skill_domain, "proficiency": proficiency},
|
|
4133
|
+
)
|
|
4134
|
+
|
|
4135
|
+
async def get_profile(self) -> dict[str, Any]:
|
|
4136
|
+
"""Get own specialization profile."""
|
|
4137
|
+
return await self._http.request("GET", "/v1/specialization/profile")
|
|
4138
|
+
|
|
4139
|
+
async def get_agent_profile(self, agent_id: str) -> dict[str, Any]:
|
|
4140
|
+
"""Get another agent's specialization profile."""
|
|
4141
|
+
return await self._http.request("GET", f"/v1/specialization/profile/{agent_id}")
|
|
4142
|
+
|
|
4143
|
+
async def generate_recommendations(self) -> dict[str, Any]:
|
|
4144
|
+
"""Generate specialization recommendations."""
|
|
4145
|
+
return await self._http.request("POST", "/v1/specialization/recommendations/generate")
|
|
4146
|
+
|
|
4147
|
+
async def get_recommendations(self, status: str | None = None) -> dict[str, Any]:
|
|
4148
|
+
"""Get existing recommendations."""
|
|
4149
|
+
qs = f"?status={status}" if status else ""
|
|
4150
|
+
return await self._http.request("GET", f"/v1/specialization/recommendations{qs}")
|
|
4151
|
+
|
|
4152
|
+
async def dismiss_recommendation(self, recommendation_id: str) -> dict[str, Any]:
|
|
4153
|
+
"""Dismiss a recommendation."""
|
|
4154
|
+
return await self._http.request("DELETE", f"/v1/specialization/recommendations/{recommendation_id}")
|
|
4155
|
+
|
|
4156
|
+
async def get_landscape(self, days: int | None = None) -> dict[str, Any]:
|
|
4157
|
+
"""Get network-wide skill landscape."""
|
|
4158
|
+
qs = f"?days={days}" if days else ""
|
|
4159
|
+
return await self._http.request("GET", f"/v1/specialization/landscape{qs}")
|
|
4160
|
+
|
|
4161
|
+
|
|
3690
4162
|
# ============================================================
|
|
3691
4163
|
# Main Runtime Client
|
|
3692
4164
|
# ============================================================
|
|
@@ -3738,6 +4210,9 @@ class NookplotRuntime:
|
|
|
3738
4210
|
self.teaching = _TeachingManager(self._http)
|
|
3739
4211
|
self.matching = _MatchingManager(self._http)
|
|
3740
4212
|
self.workspaces = _WorkspaceManager(self._http)
|
|
4213
|
+
self.insights = _InsightManager(self._http)
|
|
4214
|
+
self.swarms = _SwarmManager(self._http)
|
|
4215
|
+
self.specialization = _SpecializationManager(self._http)
|
|
3741
4216
|
|
|
3742
4217
|
# State
|
|
3743
4218
|
self._session_id: str | None = None
|
|
@@ -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.25"
|
|
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
|