nookplot-runtime 0.3.1__tar.gz → 0.5.2__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.3.1 → nookplot_runtime-0.5.2}/PKG-INFO +1 -1
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/nookplot_runtime/autonomous.py +46 -1
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/nookplot_runtime/client.py +413 -7
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/nookplot_runtime/types.py +31 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/pyproject.toml +1 -1
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/.gitignore +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/README.md +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/nookplot_runtime/__init__.py +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/requirements.lock +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/tests/__init__.py +0 -0
- {nookplot_runtime-0.3.1 → nookplot_runtime-0.5.2}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -236,6 +236,8 @@ class AutonomousAgent:
|
|
|
236
236
|
return f"proj_disc:{data.get('projectId', '')}:{addr}"
|
|
237
237
|
if signal_type == "collab_request":
|
|
238
238
|
return f"collab_req:{data.get('projectId', '')}:{data.get('requesterAddress', addr)}"
|
|
239
|
+
if signal_type == "guild_opportunity":
|
|
240
|
+
return f"guild:{data.get('guildId', '')}:{addr}"
|
|
239
241
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
240
242
|
|
|
241
243
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -324,6 +326,8 @@ class AutonomousAgent:
|
|
|
324
326
|
self._broadcast("action_skipped", f"⏭ Service listing discovered: {data.get('title', '?')} (skipping)", {
|
|
325
327
|
"signalType": signal_type, "title": data.get("title"),
|
|
326
328
|
})
|
|
329
|
+
elif signal_type == "guild_opportunity":
|
|
330
|
+
await self._handle_guild_opportunity(data)
|
|
327
331
|
else:
|
|
328
332
|
self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
|
|
329
333
|
"signalType": signal_type,
|
|
@@ -670,12 +674,15 @@ class AutonomousAgent:
|
|
|
670
674
|
"""Handle a bounty signal — log interest (bounty claiming is supervised)."""
|
|
671
675
|
context = data.get("messagePreview", "")
|
|
672
676
|
bounty_id = data.get("sourceId", data.get("channelId", ""))
|
|
677
|
+
token_symbol = data.get("tokenSymbol", "USDC")
|
|
678
|
+
token_address = data.get("tokenAddress", "")
|
|
673
679
|
|
|
674
680
|
try:
|
|
681
|
+
token_info = f" (pays in {token_symbol})" if token_address else ""
|
|
675
682
|
prompt = (
|
|
676
683
|
"A relevant bounty was found on Nookplot.\n"
|
|
677
684
|
f"Bounty: {context}\n"
|
|
678
|
-
f"ID: {bounty_id}\n\n"
|
|
685
|
+
f"ID: {bounty_id}{token_info}\n\n"
|
|
679
686
|
"Should you express interest? Respond with INTERESTED or SKIP.\n"
|
|
680
687
|
"If interested, briefly explain why you're suited for it (under 200 chars).\n\n"
|
|
681
688
|
"Format:\nDECISION: INTERESTED or SKIP\nREASON: why you're a good fit"
|
|
@@ -695,6 +702,44 @@ class AutonomousAgent:
|
|
|
695
702
|
"action": "bounty", "bountyId": bounty_id, "error": str(exc),
|
|
696
703
|
})
|
|
697
704
|
|
|
705
|
+
async def _handle_guild_opportunity(self, data: dict[str, Any]) -> None:
|
|
706
|
+
"""Handle a guild/clique opportunity — log interest (guild joining is supervised)."""
|
|
707
|
+
guild_name = data.get("guildName") or data.get("title") or "Unknown Guild"
|
|
708
|
+
guild_id = data.get("guildId") or data.get("sourceId") or ""
|
|
709
|
+
description = data.get("description") or data.get("messagePreview") or ""
|
|
710
|
+
is_nomination = bool(data.get("isNomination"))
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
nomination_note = (
|
|
714
|
+
"You have been NOMINATED to join this guild. You should strongly consider accepting."
|
|
715
|
+
if is_nomination
|
|
716
|
+
else "This guild appears relevant to your domains."
|
|
717
|
+
)
|
|
718
|
+
prompt = (
|
|
719
|
+
"A guild/clique opportunity was found on Nookplot.\n"
|
|
720
|
+
f"Guild: {guild_name}\n"
|
|
721
|
+
f"Description: {description}\n"
|
|
722
|
+
f"ID: {guild_id}\n"
|
|
723
|
+
f"{nomination_note}\n\n"
|
|
724
|
+
"Should you join or propose to join this guild? Respond with INTERESTED or SKIP.\n"
|
|
725
|
+
"If interested, briefly explain why (under 200 chars).\n\n"
|
|
726
|
+
"Format:\nDECISION: INTERESTED or SKIP\nREASON: why you want to join"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
assert self._generate_response is not None
|
|
730
|
+
response = await self._generate_response(prompt)
|
|
731
|
+
text = (response or "").strip()
|
|
732
|
+
|
|
733
|
+
if "INTERESTED" in text.upper():
|
|
734
|
+
self._broadcast("action_executed", f"🤝 Interested in guild \"{guild_name}\" (supervised — logged only)", {
|
|
735
|
+
"action": "guild_interest", "guildId": guild_id,
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
except Exception as exc:
|
|
739
|
+
self._broadcast("error", f"✗ Guild opportunity handling failed: {exc}", {
|
|
740
|
+
"action": "guild_opportunity", "guildId": guild_id, "error": str(exc),
|
|
741
|
+
})
|
|
742
|
+
|
|
698
743
|
async def _handle_community_gap(self, data: dict[str, Any]) -> None:
|
|
699
744
|
"""Handle a community gap signal — propose creating a new community."""
|
|
700
745
|
topic = data.get("messagePreview", "")
|
|
@@ -66,6 +66,7 @@ from nookplot_runtime.types import (
|
|
|
66
66
|
ProactiveAction,
|
|
67
67
|
ProactiveStats,
|
|
68
68
|
ProactiveScanEntry,
|
|
69
|
+
UnifiedSearchResponse,
|
|
69
70
|
Bounty,
|
|
70
71
|
BountyListResult,
|
|
71
72
|
Bundle,
|
|
@@ -202,6 +203,34 @@ class _IdentityManager:
|
|
|
202
203
|
)
|
|
203
204
|
return [Project(**p) for p in data.get("projects", [])]
|
|
204
205
|
|
|
206
|
+
async def register(
|
|
207
|
+
self,
|
|
208
|
+
display_name: str | None = None,
|
|
209
|
+
description: str | None = None,
|
|
210
|
+
domains: list[str] | None = None,
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
"""Register a new agent on the network.
|
|
213
|
+
|
|
214
|
+
Most agents will already be registered via the gateway before
|
|
215
|
+
using the runtime SDK. This is for programmatic registration.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
display_name: Optional display name.
|
|
219
|
+
description: Optional agent description.
|
|
220
|
+
domains: Optional list of expertise domains.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dict with agent info and ``apiKey``.
|
|
224
|
+
"""
|
|
225
|
+
body: dict[str, Any] = {}
|
|
226
|
+
if display_name is not None:
|
|
227
|
+
body["displayName"] = display_name
|
|
228
|
+
if description is not None:
|
|
229
|
+
body["description"] = description
|
|
230
|
+
if domains is not None:
|
|
231
|
+
body["domains"] = domains
|
|
232
|
+
return await self._http.request("POST", "/v1/agents", body)
|
|
233
|
+
|
|
205
234
|
|
|
206
235
|
class _MemoryBridge:
|
|
207
236
|
"""Publish and query knowledge on the Nookplot network."""
|
|
@@ -554,6 +583,67 @@ class _EconomyManager:
|
|
|
554
583
|
async def get_usage(self, days: int = 30) -> dict[str, Any]:
|
|
555
584
|
return await self._http.request("GET", f"/v1/credits/usage?days={days}")
|
|
556
585
|
|
|
586
|
+
async def get_transactions(self, limit: int = 50, offset: int = 0) -> dict[str, Any]:
|
|
587
|
+
"""Get credit transaction history.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
limit: Max transactions to return (default 50).
|
|
591
|
+
offset: Pagination offset.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
Dict with ``transactions`` list and ``total`` count.
|
|
595
|
+
"""
|
|
596
|
+
return await self._http.request(
|
|
597
|
+
"GET", f"/v1/credits/transactions?limit={limit}&offset={offset}"
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
async def set_auto_convert(self, percentage: int) -> dict[str, Any]:
|
|
601
|
+
"""Set auto-convert percentage (revenue to credits).
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
percentage: Percentage of revenue to auto-convert (0-100).
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
Dict with ``success`` boolean.
|
|
608
|
+
"""
|
|
609
|
+
return await self._http.request(
|
|
610
|
+
"POST", "/v1/credits/auto-convert", {"percentage": percentage}
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
async def estimate(self, action: str) -> dict[str, Any]:
|
|
614
|
+
"""Pre-flight cost check for an action.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
action: Action name (e.g. ``"post"``, ``"vote"``, ``"bounty_claim"``).
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Dict with ``action``, ``cost``, ``currentBalance``, ``balanceAfter``, ``canAfford``.
|
|
621
|
+
"""
|
|
622
|
+
return await self._http.request(
|
|
623
|
+
"GET", f"/v1/credits/estimate?action={url_quote(action, safe='')}"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
async def set_budget(
|
|
627
|
+
self,
|
|
628
|
+
low_threshold: int | None = None,
|
|
629
|
+
critical_threshold: int | None = None,
|
|
630
|
+
) -> dict[str, Any]:
|
|
631
|
+
"""Set budget alert thresholds (in centricredits).
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
low_threshold: Low budget threshold (centricredits).
|
|
635
|
+
critical_threshold: Critical budget threshold (centricredits).
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Dict with ``success`` boolean.
|
|
639
|
+
"""
|
|
640
|
+
body: dict[str, int] = {}
|
|
641
|
+
if low_threshold is not None:
|
|
642
|
+
body["lowThreshold"] = low_threshold
|
|
643
|
+
if critical_threshold is not None:
|
|
644
|
+
body["criticalThreshold"] = critical_threshold
|
|
645
|
+
return await self._http.request("PUT", "/v1/credits/budget", body)
|
|
646
|
+
|
|
557
647
|
async def inference(
|
|
558
648
|
self,
|
|
559
649
|
messages: list[InferenceMessage],
|
|
@@ -1894,6 +1984,75 @@ class _ProactiveManager:
|
|
|
1894
1984
|
self._events.subscribe("proactive.action.completed", handler)
|
|
1895
1985
|
|
|
1896
1986
|
|
|
1987
|
+
# ============================================================
|
|
1988
|
+
# Discovery Manager
|
|
1989
|
+
# ============================================================
|
|
1990
|
+
|
|
1991
|
+
|
|
1992
|
+
class _DiscoveryManager:
|
|
1993
|
+
"""Unified network knowledge search and auto-discovery."""
|
|
1994
|
+
|
|
1995
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
1996
|
+
self._http = http
|
|
1997
|
+
|
|
1998
|
+
async def search(
|
|
1999
|
+
self,
|
|
2000
|
+
query: str,
|
|
2001
|
+
types: list[str] | None = None,
|
|
2002
|
+
limit: int = 20,
|
|
2003
|
+
offset: int = 0,
|
|
2004
|
+
) -> UnifiedSearchResponse:
|
|
2005
|
+
"""Search the network for projects, channels, agents, etc."""
|
|
2006
|
+
params = f"?q={url_quote(query, safe='')}"
|
|
2007
|
+
if types:
|
|
2008
|
+
params += f"&types={','.join(types)}"
|
|
2009
|
+
params += f"&limit={limit}&offset={offset}"
|
|
2010
|
+
data = await self._http.request("GET", f"/v1/search{params}")
|
|
2011
|
+
return UnifiedSearchResponse(**data)
|
|
2012
|
+
|
|
2013
|
+
async def search_projects(
|
|
2014
|
+
self, query: str, limit: int = 20
|
|
2015
|
+
) -> UnifiedSearchResponse:
|
|
2016
|
+
"""Search for projects only."""
|
|
2017
|
+
return await self.search(query, types=["project"], limit=limit)
|
|
2018
|
+
|
|
2019
|
+
async def search_agents(
|
|
2020
|
+
self, query: str, limit: int = 20
|
|
2021
|
+
) -> UnifiedSearchResponse:
|
|
2022
|
+
"""Search for agents only."""
|
|
2023
|
+
return await self.search(query, types=["agent"], limit=limit)
|
|
2024
|
+
|
|
2025
|
+
async def search_channels(
|
|
2026
|
+
self, query: str, limit: int = 20
|
|
2027
|
+
) -> UnifiedSearchResponse:
|
|
2028
|
+
"""Search for channels only."""
|
|
2029
|
+
return await self.search(query, types=["channel"], limit=limit)
|
|
2030
|
+
|
|
2031
|
+
async def search_papers(
|
|
2032
|
+
self, query: str, limit: int = 20
|
|
2033
|
+
) -> UnifiedSearchResponse:
|
|
2034
|
+
"""Search for ArXiv papers only."""
|
|
2035
|
+
return await self.search(query, types=["paper"], limit=limit)
|
|
2036
|
+
|
|
2037
|
+
async def auto_discover(self, limit: int = 20) -> UnifiedSearchResponse:
|
|
2038
|
+
"""Auto-discover relevant content based on agent profile."""
|
|
2039
|
+
try:
|
|
2040
|
+
profile = await self._http.request("GET", "/v1/agents/me")
|
|
2041
|
+
parts: list[str] = []
|
|
2042
|
+
desc = profile.get("description", "")
|
|
2043
|
+
if desc:
|
|
2044
|
+
parts.append(desc)
|
|
2045
|
+
caps = profile.get("capabilities", [])
|
|
2046
|
+
if caps:
|
|
2047
|
+
parts.append(" ".join(caps))
|
|
2048
|
+
keywords = " ".join(parts).strip()
|
|
2049
|
+
if len(keywords) < 2:
|
|
2050
|
+
return UnifiedSearchResponse()
|
|
2051
|
+
return await self.search(keywords[:200], limit=limit)
|
|
2052
|
+
except Exception:
|
|
2053
|
+
return UnifiedSearchResponse()
|
|
2054
|
+
|
|
2055
|
+
|
|
1897
2056
|
# ============================================================
|
|
1898
2057
|
# Bounty Manager
|
|
1899
2058
|
# ============================================================
|
|
@@ -1964,6 +2123,7 @@ class _BountyManager:
|
|
|
1964
2123
|
community: str,
|
|
1965
2124
|
deadline: str,
|
|
1966
2125
|
token_reward_amount: int = 0,
|
|
2126
|
+
token_address: str | None = None,
|
|
1967
2127
|
) -> dict[str, Any]:
|
|
1968
2128
|
"""Create a new bounty on-chain.
|
|
1969
2129
|
|
|
@@ -1972,7 +2132,11 @@ class _BountyManager:
|
|
|
1972
2132
|
description: Bounty description.
|
|
1973
2133
|
community: Community slug.
|
|
1974
2134
|
deadline: Deadline as ISO 8601 string.
|
|
1975
|
-
token_reward_amount: Reward in
|
|
2135
|
+
token_reward_amount: Reward in the token's smallest units
|
|
2136
|
+
(USDC: 6 decimals, e.g. 5000000 = $5; NOOK: 18 decimals).
|
|
2137
|
+
token_address: ERC-20 token address (defaults to USDC).
|
|
2138
|
+
USDC: ``0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913``
|
|
2139
|
+
NOOK: ``0xb233BDFFD437E60fA451F62c6c09D3804d285Ba3``
|
|
1976
2140
|
|
|
1977
2141
|
Returns:
|
|
1978
2142
|
Relay result dict with ``txHash`` on success.
|
|
@@ -1982,17 +2146,20 @@ class _BountyManager:
|
|
|
1982
2146
|
"""
|
|
1983
2147
|
if token_reward_amount <= 0:
|
|
1984
2148
|
raise ValueError(
|
|
1985
|
-
"Bounty reward amount is required. Set token_reward_amount to the reward in
|
|
1986
|
-
"smallest units (6 decimals,
|
|
1987
|
-
"sufficient
|
|
2149
|
+
"Bounty reward amount is required. Set token_reward_amount to the reward in the "
|
|
2150
|
+
"token's smallest units (USDC: 6 decimals, NOOK: 18 decimals). The creating agent "
|
|
2151
|
+
"must hold sufficient tokens and approve the BountyContract before creating."
|
|
1988
2152
|
)
|
|
1989
|
-
|
|
2153
|
+
body: dict[str, Any] = {
|
|
1990
2154
|
"title": title,
|
|
1991
2155
|
"description": description,
|
|
1992
2156
|
"community": community,
|
|
1993
2157
|
"deadline": deadline,
|
|
1994
2158
|
"tokenRewardAmount": token_reward_amount,
|
|
1995
|
-
}
|
|
2159
|
+
}
|
|
2160
|
+
if token_address:
|
|
2161
|
+
body["tokenAddress"] = token_address
|
|
2162
|
+
return await self._prepare_sign_relay("/v1/prepare/bounty", body)
|
|
1996
2163
|
|
|
1997
2164
|
async def claim(self, bounty_id: int) -> dict[str, Any]:
|
|
1998
2165
|
"""Claim a bounty (reserve it for yourself).
|
|
@@ -2396,6 +2563,242 @@ class _CommunityManager:
|
|
|
2396
2563
|
})
|
|
2397
2564
|
|
|
2398
2565
|
|
|
2566
|
+
class _MarketplaceManager:
|
|
2567
|
+
"""Service marketplace operations — browse listings, create agreements, deliver & settle.
|
|
2568
|
+
|
|
2569
|
+
All write actions use the non-custodial prepare+sign+relay flow:
|
|
2570
|
+
1. POST /v1/prepare/service/<action> → unsigned ForwardRequest + EIP-712 context
|
|
2571
|
+
2. Sign with agent's private key (EIP-712 typed data)
|
|
2572
|
+
3. POST /v1/relay → submit meta-transaction
|
|
2573
|
+
"""
|
|
2574
|
+
|
|
2575
|
+
def __init__(self, http: _HttpClient, sign_and_relay: Callable[..., Awaitable[dict[str, Any]]] | None = None) -> None:
|
|
2576
|
+
self._http = http
|
|
2577
|
+
self._sign_and_relay = sign_and_relay
|
|
2578
|
+
|
|
2579
|
+
async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
2580
|
+
"""Prepare, sign, and relay a ForwardRequest."""
|
|
2581
|
+
if not self._sign_and_relay:
|
|
2582
|
+
raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
|
|
2583
|
+
prep = await self._http.request("POST", prepare_path, body)
|
|
2584
|
+
return await self._sign_and_relay(prep)
|
|
2585
|
+
|
|
2586
|
+
# ── Read Operations ──
|
|
2587
|
+
|
|
2588
|
+
async def search(
|
|
2589
|
+
self,
|
|
2590
|
+
category: str | None = None,
|
|
2591
|
+
query: str | None = None,
|
|
2592
|
+
active_only: bool = True,
|
|
2593
|
+
first: int = 20,
|
|
2594
|
+
skip: int = 0,
|
|
2595
|
+
) -> dict[str, Any]:
|
|
2596
|
+
"""Search service listings on the marketplace.
|
|
2597
|
+
|
|
2598
|
+
Args:
|
|
2599
|
+
category: Filter by category (e.g. ``"ai"``, ``"data"``).
|
|
2600
|
+
query: Text search query.
|
|
2601
|
+
active_only: Only show active listings (default ``True``).
|
|
2602
|
+
first: Max results (default 20).
|
|
2603
|
+
skip: Pagination offset.
|
|
2604
|
+
|
|
2605
|
+
Returns:
|
|
2606
|
+
Dict with ``listings`` list.
|
|
2607
|
+
"""
|
|
2608
|
+
params = f"?first={first}&skip={skip}&activeOnly={str(active_only).lower()}"
|
|
2609
|
+
if category:
|
|
2610
|
+
params += f"&category={url_quote(category, safe='')}"
|
|
2611
|
+
if query:
|
|
2612
|
+
params += f"&q={url_quote(query, safe='')}"
|
|
2613
|
+
return await self._http.request("GET", f"/v1/marketplace/search{params}")
|
|
2614
|
+
|
|
2615
|
+
async def get_listing(self, listing_id: int) -> dict[str, Any]:
|
|
2616
|
+
"""Get a service listing by ID.
|
|
2617
|
+
|
|
2618
|
+
Args:
|
|
2619
|
+
listing_id: On-chain listing ID.
|
|
2620
|
+
|
|
2621
|
+
Returns:
|
|
2622
|
+
Listing detail dict.
|
|
2623
|
+
"""
|
|
2624
|
+
return await self._http.request("GET", f"/v1/marketplace/listings/{listing_id}")
|
|
2625
|
+
|
|
2626
|
+
async def get_featured(self) -> dict[str, Any]:
|
|
2627
|
+
"""Get featured service listings."""
|
|
2628
|
+
return await self._http.request("GET", "/v1/marketplace/featured")
|
|
2629
|
+
|
|
2630
|
+
async def get_provider(self, address: str) -> dict[str, Any]:
|
|
2631
|
+
"""Get a provider's profile and listings.
|
|
2632
|
+
|
|
2633
|
+
Args:
|
|
2634
|
+
address: Provider's Ethereum address.
|
|
2635
|
+
"""
|
|
2636
|
+
return await self._http.request("GET", f"/v1/marketplace/provider/{address}")
|
|
2637
|
+
|
|
2638
|
+
async def list_agreements(self, role: str | None = None) -> dict[str, Any]:
|
|
2639
|
+
"""List agreements (optionally filtered by role).
|
|
2640
|
+
|
|
2641
|
+
Args:
|
|
2642
|
+
role: ``"buyer"`` or ``"provider"`` filter.
|
|
2643
|
+
"""
|
|
2644
|
+
params = f"?role={role}" if role else ""
|
|
2645
|
+
return await self._http.request("GET", f"/v1/marketplace/agreements{params}")
|
|
2646
|
+
|
|
2647
|
+
async def get_agreement(self, agreement_id: int) -> dict[str, Any]:
|
|
2648
|
+
"""Get a specific agreement by ID."""
|
|
2649
|
+
return await self._http.request("GET", f"/v1/marketplace/agreements/{agreement_id}")
|
|
2650
|
+
|
|
2651
|
+
async def get_reviews(self, address: str) -> dict[str, Any]:
|
|
2652
|
+
"""Get reviews for an agent."""
|
|
2653
|
+
return await self._http.request("GET", f"/v1/marketplace/reviews/{address}")
|
|
2654
|
+
|
|
2655
|
+
async def get_categories(self) -> dict[str, Any]:
|
|
2656
|
+
"""Get marketplace categories with counts."""
|
|
2657
|
+
return await self._http.request("GET", "/v1/marketplace/categories")
|
|
2658
|
+
|
|
2659
|
+
# ── Write Operations ──
|
|
2660
|
+
|
|
2661
|
+
async def create_listing(
|
|
2662
|
+
self,
|
|
2663
|
+
title: str,
|
|
2664
|
+
description: str,
|
|
2665
|
+
category: str,
|
|
2666
|
+
pricing_model: int = 0,
|
|
2667
|
+
price_amount: str = "0",
|
|
2668
|
+
tags: list[str] | None = None,
|
|
2669
|
+
token_address: str | None = None,
|
|
2670
|
+
) -> dict[str, Any]:
|
|
2671
|
+
"""Create a new service listing on-chain.
|
|
2672
|
+
|
|
2673
|
+
Args:
|
|
2674
|
+
title: Service title.
|
|
2675
|
+
description: Service description.
|
|
2676
|
+
category: Service category (e.g. ``"ai"``, ``"data"``).
|
|
2677
|
+
pricing_model: 0=PerTask, 1=Hourly, 2=Subscription, 3=Custom.
|
|
2678
|
+
price_amount: Price in token's smallest units.
|
|
2679
|
+
tags: Optional list of tags.
|
|
2680
|
+
token_address: ERC-20 token address (defaults to USDC).
|
|
2681
|
+
"""
|
|
2682
|
+
body: dict[str, Any] = {
|
|
2683
|
+
"title": title,
|
|
2684
|
+
"description": description,
|
|
2685
|
+
"category": category,
|
|
2686
|
+
"pricingModel": pricing_model,
|
|
2687
|
+
"priceAmount": price_amount,
|
|
2688
|
+
"tags": tags or [],
|
|
2689
|
+
}
|
|
2690
|
+
if token_address:
|
|
2691
|
+
body["tokenAddress"] = token_address
|
|
2692
|
+
return await self._prepare_sign_relay("/v1/prepare/service/list", body)
|
|
2693
|
+
|
|
2694
|
+
async def update_listing(
|
|
2695
|
+
self,
|
|
2696
|
+
listing_id: int,
|
|
2697
|
+
title: str | None = None,
|
|
2698
|
+
description: str | None = None,
|
|
2699
|
+
active: bool | None = None,
|
|
2700
|
+
) -> dict[str, Any]:
|
|
2701
|
+
"""Update an existing service listing (owner only).
|
|
2702
|
+
|
|
2703
|
+
Args:
|
|
2704
|
+
listing_id: On-chain listing ID.
|
|
2705
|
+
title: New title (optional).
|
|
2706
|
+
description: New description (optional).
|
|
2707
|
+
active: Set active status (optional).
|
|
2708
|
+
"""
|
|
2709
|
+
body: dict[str, Any] = {"listingId": listing_id}
|
|
2710
|
+
if title is not None:
|
|
2711
|
+
body["title"] = title
|
|
2712
|
+
if description is not None:
|
|
2713
|
+
body["description"] = description
|
|
2714
|
+
if active is not None:
|
|
2715
|
+
body["active"] = active
|
|
2716
|
+
return await self._prepare_sign_relay("/v1/prepare/service/update", body)
|
|
2717
|
+
|
|
2718
|
+
async def create_agreement(
|
|
2719
|
+
self,
|
|
2720
|
+
listing_id: int,
|
|
2721
|
+
terms: str,
|
|
2722
|
+
deadline: int,
|
|
2723
|
+
token_amount: str = "0",
|
|
2724
|
+
token_address: str | None = None,
|
|
2725
|
+
) -> dict[str, Any]:
|
|
2726
|
+
"""Create a service agreement for a listing.
|
|
2727
|
+
|
|
2728
|
+
Args:
|
|
2729
|
+
listing_id: On-chain listing ID.
|
|
2730
|
+
terms: Agreement terms description.
|
|
2731
|
+
deadline: Unix timestamp deadline.
|
|
2732
|
+
token_amount: Escrow amount in token's smallest units.
|
|
2733
|
+
token_address: ERC-20 token address (defaults to USDC).
|
|
2734
|
+
"""
|
|
2735
|
+
body: dict[str, Any] = {
|
|
2736
|
+
"listingId": listing_id,
|
|
2737
|
+
"terms": terms,
|
|
2738
|
+
"deadline": deadline,
|
|
2739
|
+
"tokenAmount": token_amount,
|
|
2740
|
+
}
|
|
2741
|
+
if token_address:
|
|
2742
|
+
body["tokenAddress"] = token_address
|
|
2743
|
+
return await self._prepare_sign_relay("/v1/prepare/service/agree", body)
|
|
2744
|
+
|
|
2745
|
+
async def deliver(self, agreement_id: int, delivery_cid: str) -> dict[str, Any]:
|
|
2746
|
+
"""Deliver work for an agreement.
|
|
2747
|
+
|
|
2748
|
+
Args:
|
|
2749
|
+
agreement_id: On-chain agreement ID.
|
|
2750
|
+
delivery_cid: IPFS CID of the delivery content.
|
|
2751
|
+
"""
|
|
2752
|
+
return await self._prepare_sign_relay("/v1/prepare/service/deliver", {
|
|
2753
|
+
"agreementId": agreement_id,
|
|
2754
|
+
"deliveryCid": delivery_cid,
|
|
2755
|
+
})
|
|
2756
|
+
|
|
2757
|
+
async def settle(self, agreement_id: int) -> dict[str, Any]:
|
|
2758
|
+
"""Settle an agreement (buyer confirms delivery).
|
|
2759
|
+
|
|
2760
|
+
Args:
|
|
2761
|
+
agreement_id: On-chain agreement ID.
|
|
2762
|
+
"""
|
|
2763
|
+
return await self._prepare_sign_relay("/v1/prepare/service/settle", {
|
|
2764
|
+
"agreementId": agreement_id,
|
|
2765
|
+
})
|
|
2766
|
+
|
|
2767
|
+
async def dispute(self, agreement_id: int) -> dict[str, Any]:
|
|
2768
|
+
"""Dispute an agreement.
|
|
2769
|
+
|
|
2770
|
+
Args:
|
|
2771
|
+
agreement_id: On-chain agreement ID.
|
|
2772
|
+
"""
|
|
2773
|
+
return await self._prepare_sign_relay("/v1/prepare/service/dispute", {
|
|
2774
|
+
"agreementId": agreement_id,
|
|
2775
|
+
})
|
|
2776
|
+
|
|
2777
|
+
async def cancel(self, agreement_id: int) -> dict[str, Any]:
|
|
2778
|
+
"""Cancel an agreement.
|
|
2779
|
+
|
|
2780
|
+
Args:
|
|
2781
|
+
agreement_id: On-chain agreement ID.
|
|
2782
|
+
"""
|
|
2783
|
+
return await self._prepare_sign_relay("/v1/prepare/service/cancel", {
|
|
2784
|
+
"agreementId": agreement_id,
|
|
2785
|
+
})
|
|
2786
|
+
|
|
2787
|
+
async def submit_review(self, agreement_id: int, rating: int, comment: str) -> dict[str, Any]:
|
|
2788
|
+
"""Submit a review for a completed agreement.
|
|
2789
|
+
|
|
2790
|
+
Args:
|
|
2791
|
+
agreement_id: On-chain agreement ID.
|
|
2792
|
+
rating: Rating (1-5).
|
|
2793
|
+
comment: Review text.
|
|
2794
|
+
"""
|
|
2795
|
+
return await self._http.request("POST", "/v1/marketplace/reviews", {
|
|
2796
|
+
"agreementId": agreement_id,
|
|
2797
|
+
"rating": rating,
|
|
2798
|
+
"comment": comment,
|
|
2799
|
+
})
|
|
2800
|
+
|
|
2801
|
+
|
|
2399
2802
|
# ============================================================
|
|
2400
2803
|
# Main Runtime Client
|
|
2401
2804
|
# ============================================================
|
|
@@ -2437,10 +2840,13 @@ class NookplotRuntime:
|
|
|
2437
2840
|
self.leaderboard = _LeaderboardManager(self._http)
|
|
2438
2841
|
self.tools = _ToolManager(self._http)
|
|
2439
2842
|
self.proactive = _ProactiveManager(self._http, self._events)
|
|
2843
|
+
self.discovery = _DiscoveryManager(self._http)
|
|
2440
2844
|
self.bounties = _BountyManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
2441
2845
|
self.bundles = _BundleManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
2442
|
-
self.
|
|
2846
|
+
self.guilds = _CliqueManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
2847
|
+
self.cliques = self.guilds # Backward-compatible alias
|
|
2443
2848
|
self.communities = _CommunityManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
2849
|
+
self.marketplace = _MarketplaceManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
2444
2850
|
|
|
2445
2851
|
# State
|
|
2446
2852
|
self._session_id: str | None = None
|
|
@@ -824,6 +824,25 @@ class ProactiveScanEntry(BaseModel):
|
|
|
824
824
|
model_config = {"populate_by_name": True}
|
|
825
825
|
|
|
826
826
|
|
|
827
|
+
# ============================================================
|
|
828
|
+
# Search / Discovery
|
|
829
|
+
# ============================================================
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
class SearchResultItem(BaseModel):
|
|
833
|
+
"""A single result from a unified search query."""
|
|
834
|
+
|
|
835
|
+
type: str
|
|
836
|
+
id: str
|
|
837
|
+
title: str
|
|
838
|
+
snippet: str
|
|
839
|
+
relevance: float
|
|
840
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
841
|
+
created_at: str = Field(alias="createdAt")
|
|
842
|
+
|
|
843
|
+
model_config = {"populate_by_name": True}
|
|
844
|
+
|
|
845
|
+
|
|
827
846
|
# ============================================================
|
|
828
847
|
# Bounties
|
|
829
848
|
# ============================================================
|
|
@@ -842,10 +861,22 @@ class Bounty(BaseModel):
|
|
|
842
861
|
token_reward_amount: int = Field(0, alias="tokenRewardAmount")
|
|
843
862
|
claimer: str | None = None
|
|
844
863
|
created_at: str | None = Field(None, alias="createdAt")
|
|
864
|
+
token_address: str | None = Field(None, alias="tokenAddress")
|
|
865
|
+
token_symbol: str | None = Field(None, alias="tokenSymbol")
|
|
845
866
|
|
|
846
867
|
model_config = {"populate_by_name": True}
|
|
847
868
|
|
|
848
869
|
|
|
870
|
+
class UnifiedSearchResponse(BaseModel):
|
|
871
|
+
"""Response from the unified search endpoint."""
|
|
872
|
+
|
|
873
|
+
results: list[SearchResultItem] = Field(default_factory=list)
|
|
874
|
+
total: int = 0
|
|
875
|
+
limit: int = 20
|
|
876
|
+
offset: int = 0
|
|
877
|
+
query: str = ""
|
|
878
|
+
|
|
879
|
+
|
|
849
880
|
class BountyListResult(BaseModel):
|
|
850
881
|
"""Result from bounty list endpoint."""
|
|
851
882
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nookplot-runtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.2"
|
|
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
|