agentnode-sdk 0.1.0__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.
- agentnode_sdk-0.1.0/.gitignore +46 -0
- agentnode_sdk-0.1.0/PKG-INFO +26 -0
- agentnode_sdk-0.1.0/README.md +3 -0
- agentnode_sdk-0.1.0/agentnode_sdk/__init__.py +33 -0
- agentnode_sdk-0.1.0/agentnode_sdk/client.py +371 -0
- agentnode_sdk-0.1.0/agentnode_sdk/exceptions.py +25 -0
- agentnode_sdk-0.1.0/agentnode_sdk/models.py +112 -0
- agentnode_sdk-0.1.0/pyproject.toml +34 -0
- agentnode_sdk-0.1.0/tests/__init__.py +0 -0
- agentnode_sdk-0.1.0/tests/test_client.py +152 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# Node
|
|
11
|
+
node_modules/
|
|
12
|
+
cli/dist/
|
|
13
|
+
|
|
14
|
+
# Next.js
|
|
15
|
+
web/.next/
|
|
16
|
+
web/out/
|
|
17
|
+
|
|
18
|
+
# Environment
|
|
19
|
+
.env
|
|
20
|
+
.env.local
|
|
21
|
+
.env.production
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
*.swo
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
|
|
33
|
+
# Test
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.coverage
|
|
36
|
+
htmlcov/
|
|
37
|
+
|
|
38
|
+
# Build artifacts
|
|
39
|
+
*.tar.gz
|
|
40
|
+
*.whl
|
|
41
|
+
|
|
42
|
+
# Spec documents
|
|
43
|
+
*.docx
|
|
44
|
+
|
|
45
|
+
# Claude Code
|
|
46
|
+
.claude/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentnode-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
|
|
5
|
+
Project-URL: Homepage, https://agentnode.net
|
|
6
|
+
Project-URL: Repository, https://github.com/agentnode-ai/agentnode
|
|
7
|
+
Project-URL: Documentation, https://agentnode.net/docs
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: agent,agentnode,ai,capabilities,langchain,mcp
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.25
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
21
|
+
Requires-Dist: respx; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# agentnode-sdk
|
|
25
|
+
|
|
26
|
+
Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""AgentNode Python SDK — discover, resolve, and install AI agent capabilities."""
|
|
2
|
+
|
|
3
|
+
from agentnode_sdk.client import AgentNode, AgentNodeClient
|
|
4
|
+
from agentnode_sdk.exceptions import (
|
|
5
|
+
AgentNodeError,
|
|
6
|
+
AuthError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
)
|
|
10
|
+
from agentnode_sdk.models import (
|
|
11
|
+
InstallMetadata,
|
|
12
|
+
PackageDetail,
|
|
13
|
+
ResolvedPackage,
|
|
14
|
+
ResolveResult,
|
|
15
|
+
SearchHit,
|
|
16
|
+
SearchResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AgentNode",
|
|
22
|
+
"AgentNodeClient",
|
|
23
|
+
"AgentNodeError",
|
|
24
|
+
"NotFoundError",
|
|
25
|
+
"AuthError",
|
|
26
|
+
"ValidationError",
|
|
27
|
+
"PackageDetail",
|
|
28
|
+
"SearchResult",
|
|
29
|
+
"SearchHit",
|
|
30
|
+
"ResolveResult",
|
|
31
|
+
"ResolvedPackage",
|
|
32
|
+
"InstallMetadata",
|
|
33
|
+
]
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""AgentNode API client. Spec §14."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from agentnode_sdk.exceptions import (
|
|
7
|
+
AgentNodeError,
|
|
8
|
+
AuthError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
from agentnode_sdk.models import (
|
|
13
|
+
ArtifactInfo,
|
|
14
|
+
CapabilityInfo,
|
|
15
|
+
DependencyInfo,
|
|
16
|
+
InstallMetadata,
|
|
17
|
+
PackageDetail,
|
|
18
|
+
PermissionsInfo,
|
|
19
|
+
ResolvedPackage,
|
|
20
|
+
ResolveResult,
|
|
21
|
+
ScoreBreakdown,
|
|
22
|
+
SearchHit,
|
|
23
|
+
SearchResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://api.agentnode.net/v1"
|
|
27
|
+
|
|
28
|
+
ERROR_CLASS_MAP = {
|
|
29
|
+
401: AuthError,
|
|
30
|
+
403: AuthError,
|
|
31
|
+
404: NotFoundError,
|
|
32
|
+
422: ValidationError,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentNode:
|
|
37
|
+
"""Spec-compliant SDK client (§14.3). Returns plain dicts."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL):
|
|
40
|
+
self._client = httpx.Client(
|
|
41
|
+
base_url=base_url.rstrip("/"),
|
|
42
|
+
headers={"X-API-Key": api_key},
|
|
43
|
+
timeout=30,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def close(self):
|
|
47
|
+
self._client.close()
|
|
48
|
+
|
|
49
|
+
def __enter__(self):
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, *args):
|
|
53
|
+
self.close()
|
|
54
|
+
|
|
55
|
+
# --- Public API ---
|
|
56
|
+
|
|
57
|
+
def search(
|
|
58
|
+
self,
|
|
59
|
+
query: str = "",
|
|
60
|
+
capability_id: str = "",
|
|
61
|
+
framework: str = "",
|
|
62
|
+
sort_by: str = "relevance",
|
|
63
|
+
page: int = 1,
|
|
64
|
+
) -> dict:
|
|
65
|
+
body = {k: v for k, v in {
|
|
66
|
+
"q": query,
|
|
67
|
+
"capability_id": capability_id,
|
|
68
|
+
"framework": framework,
|
|
69
|
+
"sort_by": sort_by,
|
|
70
|
+
"page": page,
|
|
71
|
+
}.items() if v}
|
|
72
|
+
return self._handle(self._client.post("/search", json=body))
|
|
73
|
+
|
|
74
|
+
def resolve_upgrade(
|
|
75
|
+
self,
|
|
76
|
+
missing_capability: str,
|
|
77
|
+
framework: str = "",
|
|
78
|
+
runtime: str = "",
|
|
79
|
+
current_capabilities: list[str] | None = None,
|
|
80
|
+
policy: dict | None = None,
|
|
81
|
+
) -> dict:
|
|
82
|
+
return self._post(
|
|
83
|
+
"/resolve-upgrade",
|
|
84
|
+
missing_capability=missing_capability,
|
|
85
|
+
current_capabilities=current_capabilities or [],
|
|
86
|
+
framework=framework,
|
|
87
|
+
runtime=runtime,
|
|
88
|
+
policy=policy or {},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def check_policy(
|
|
92
|
+
self,
|
|
93
|
+
package_slug: str,
|
|
94
|
+
framework: str = "",
|
|
95
|
+
policy: dict | None = None,
|
|
96
|
+
) -> dict:
|
|
97
|
+
return self._post(
|
|
98
|
+
"/check-policy",
|
|
99
|
+
package_slug=package_slug,
|
|
100
|
+
framework=framework,
|
|
101
|
+
policy=policy or {},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def get_install_metadata(self, package_slug: str, version: str = "") -> dict:
|
|
105
|
+
"""Read-only install metadata. Does NOT create installation records."""
|
|
106
|
+
params = {"version": version} if version else {}
|
|
107
|
+
return self._handle(
|
|
108
|
+
self._client.get(f"/packages/{package_slug}/install-info", params=params)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def get_package(self, slug: str) -> dict:
|
|
112
|
+
return self._handle(self._client.get(f"/packages/{slug}"))
|
|
113
|
+
|
|
114
|
+
def validate(self, manifest: dict) -> dict:
|
|
115
|
+
return self._handle(
|
|
116
|
+
self._client.post("/packages/validate", json={"manifest": manifest})
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def install(
|
|
120
|
+
self,
|
|
121
|
+
package_slug: str,
|
|
122
|
+
version: str = "",
|
|
123
|
+
source: str = "sdk",
|
|
124
|
+
event_type: str = "install",
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Create installation record and get artifact URL."""
|
|
127
|
+
body: dict = {"source": source, "event_type": event_type}
|
|
128
|
+
if version:
|
|
129
|
+
body["version"] = version
|
|
130
|
+
return self._handle(
|
|
131
|
+
self._client.post(f"/packages/{package_slug}/install", json=body)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def recommend(
|
|
135
|
+
self,
|
|
136
|
+
missing_capabilities: list[str],
|
|
137
|
+
framework: str = "",
|
|
138
|
+
runtime: str = "",
|
|
139
|
+
) -> dict:
|
|
140
|
+
return self._post(
|
|
141
|
+
"/recommend",
|
|
142
|
+
missing_capabilities=missing_capabilities,
|
|
143
|
+
framework=framework,
|
|
144
|
+
runtime=runtime,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# --- Internal ---
|
|
148
|
+
|
|
149
|
+
def _post(self, path: str, **kwargs) -> dict:
|
|
150
|
+
# Keep lists/dicts even if empty; only filter out None and empty strings
|
|
151
|
+
body = {k: v for k, v in kwargs.items() if v is not None and v != ""}
|
|
152
|
+
return self._handle(self._client.post(path, json=body))
|
|
153
|
+
|
|
154
|
+
def _handle(self, response: httpx.Response) -> dict:
|
|
155
|
+
"""Parse response. Raise typed AgentNodeError on API errors."""
|
|
156
|
+
if response.status_code >= 400:
|
|
157
|
+
try:
|
|
158
|
+
body = response.json()
|
|
159
|
+
err = body.get("error", {})
|
|
160
|
+
code = err.get("code", "UNKNOWN")
|
|
161
|
+
message = err.get("message", response.text)
|
|
162
|
+
except (ValueError, KeyError):
|
|
163
|
+
code, message = "UNKNOWN", response.text
|
|
164
|
+
exc_class = ERROR_CLASS_MAP.get(response.status_code, AgentNodeError)
|
|
165
|
+
raise exc_class(code, message)
|
|
166
|
+
return response.json()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class AgentNodeClient:
|
|
170
|
+
"""Extended client returning typed dataclass models. Backward-compatible."""
|
|
171
|
+
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
175
|
+
api_key: str | None = None,
|
|
176
|
+
token: str | None = None,
|
|
177
|
+
timeout: float = 30.0,
|
|
178
|
+
):
|
|
179
|
+
self.base_url = base_url.rstrip("/")
|
|
180
|
+
headers = {}
|
|
181
|
+
if api_key:
|
|
182
|
+
headers["X-API-Key"] = api_key
|
|
183
|
+
elif token:
|
|
184
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
185
|
+
self._client = httpx.Client(
|
|
186
|
+
base_url=self.base_url, headers=headers, timeout=timeout
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def close(self):
|
|
190
|
+
self._client.close()
|
|
191
|
+
|
|
192
|
+
def __enter__(self):
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, *args):
|
|
196
|
+
self.close()
|
|
197
|
+
|
|
198
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
199
|
+
resp = self._client.request(method, path, **kwargs)
|
|
200
|
+
if resp.status_code >= 400:
|
|
201
|
+
data = resp.json()
|
|
202
|
+
err = data.get("error", {})
|
|
203
|
+
exc_class = ERROR_CLASS_MAP.get(resp.status_code, AgentNodeError)
|
|
204
|
+
raise exc_class(
|
|
205
|
+
code=err.get("code", "UNKNOWN"),
|
|
206
|
+
message=err.get("message", resp.text),
|
|
207
|
+
)
|
|
208
|
+
return resp.json()
|
|
209
|
+
|
|
210
|
+
# --- Search ---
|
|
211
|
+
|
|
212
|
+
def search(
|
|
213
|
+
self,
|
|
214
|
+
query: str = "",
|
|
215
|
+
package_type: str | None = None,
|
|
216
|
+
capability_id: str | None = None,
|
|
217
|
+
framework: str | None = None,
|
|
218
|
+
sort_by: str | None = None,
|
|
219
|
+
page: int = 1,
|
|
220
|
+
per_page: int = 20,
|
|
221
|
+
) -> SearchResult:
|
|
222
|
+
body: dict = {"q": query, "page": page, "per_page": per_page}
|
|
223
|
+
if package_type:
|
|
224
|
+
body["package_type"] = package_type
|
|
225
|
+
if capability_id:
|
|
226
|
+
body["capability_id"] = capability_id
|
|
227
|
+
if framework:
|
|
228
|
+
body["framework"] = framework
|
|
229
|
+
if sort_by:
|
|
230
|
+
body["sort_by"] = sort_by
|
|
231
|
+
|
|
232
|
+
data = self._request("POST", "/search", json=body)
|
|
233
|
+
hits = [
|
|
234
|
+
SearchHit(
|
|
235
|
+
slug=h["slug"],
|
|
236
|
+
name=h["name"],
|
|
237
|
+
package_type=h["package_type"],
|
|
238
|
+
summary=h["summary"],
|
|
239
|
+
publisher_slug=h.get("publisher_slug", ""),
|
|
240
|
+
trust_level=h.get("trust_level", "unverified"),
|
|
241
|
+
latest_version=h.get("latest_version"),
|
|
242
|
+
runtime=h.get("runtime"),
|
|
243
|
+
capability_ids=h.get("capability_ids", []),
|
|
244
|
+
download_count=h.get("download_count", 0),
|
|
245
|
+
)
|
|
246
|
+
for h in data.get("hits", [])
|
|
247
|
+
]
|
|
248
|
+
return SearchResult(query=data["query"], hits=hits, total=data["total"])
|
|
249
|
+
|
|
250
|
+
# --- Resolve ---
|
|
251
|
+
|
|
252
|
+
def resolve(
|
|
253
|
+
self,
|
|
254
|
+
capabilities: list[str],
|
|
255
|
+
framework: str | None = None,
|
|
256
|
+
runtime: str | None = None,
|
|
257
|
+
package_type: str | None = None,
|
|
258
|
+
limit: int = 10,
|
|
259
|
+
) -> ResolveResult:
|
|
260
|
+
body: dict = {"capabilities": capabilities, "limit": limit}
|
|
261
|
+
if framework:
|
|
262
|
+
body["framework"] = framework
|
|
263
|
+
if runtime:
|
|
264
|
+
body["runtime"] = runtime
|
|
265
|
+
if package_type:
|
|
266
|
+
body["package_type"] = package_type
|
|
267
|
+
|
|
268
|
+
data = self._request("POST", "/resolve", json=body)
|
|
269
|
+
results = [
|
|
270
|
+
ResolvedPackage(
|
|
271
|
+
slug=r["slug"],
|
|
272
|
+
name=r["name"],
|
|
273
|
+
package_type=r["package_type"],
|
|
274
|
+
summary=r["summary"],
|
|
275
|
+
version=r["version"],
|
|
276
|
+
publisher_slug=r["publisher_slug"],
|
|
277
|
+
trust_level=r["trust_level"],
|
|
278
|
+
score=r["score"],
|
|
279
|
+
breakdown=ScoreBreakdown(**r["breakdown"]),
|
|
280
|
+
matched_capabilities=r.get("matched_capabilities", []),
|
|
281
|
+
)
|
|
282
|
+
for r in data.get("results", [])
|
|
283
|
+
]
|
|
284
|
+
return ResolveResult(results=results, total=data["total"])
|
|
285
|
+
|
|
286
|
+
# --- Package detail ---
|
|
287
|
+
|
|
288
|
+
def get_package(self, slug: str) -> PackageDetail:
|
|
289
|
+
data = self._request("GET", f"/packages/{slug}")
|
|
290
|
+
lv = data.get("latest_version")
|
|
291
|
+
return PackageDetail(
|
|
292
|
+
slug=data["slug"],
|
|
293
|
+
name=data["name"],
|
|
294
|
+
package_type=data["package_type"],
|
|
295
|
+
summary=data["summary"],
|
|
296
|
+
description=data.get("description"),
|
|
297
|
+
download_count=data["download_count"],
|
|
298
|
+
is_deprecated=data["is_deprecated"],
|
|
299
|
+
latest_version=lv["version_number"] if lv else None,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# --- Install metadata ---
|
|
303
|
+
|
|
304
|
+
def get_install_metadata(
|
|
305
|
+
self, slug: str, version: str | None = None
|
|
306
|
+
) -> InstallMetadata:
|
|
307
|
+
params = {}
|
|
308
|
+
if version:
|
|
309
|
+
params["version"] = version
|
|
310
|
+
data = self._request("GET", f"/packages/{slug}/install-info", params=params)
|
|
311
|
+
|
|
312
|
+
artifact = None
|
|
313
|
+
if data.get("artifact"):
|
|
314
|
+
a = data["artifact"]
|
|
315
|
+
artifact = ArtifactInfo(
|
|
316
|
+
url=a.get("url"),
|
|
317
|
+
hash_sha256=a.get("hash_sha256"),
|
|
318
|
+
size_bytes=a.get("size_bytes"),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
caps = [
|
|
322
|
+
CapabilityInfo(
|
|
323
|
+
name=c["name"],
|
|
324
|
+
capability_id=c["capability_id"],
|
|
325
|
+
capability_type=c["capability_type"],
|
|
326
|
+
)
|
|
327
|
+
for c in data.get("capabilities", [])
|
|
328
|
+
]
|
|
329
|
+
deps = [
|
|
330
|
+
DependencyInfo(
|
|
331
|
+
package_slug=d["package_slug"],
|
|
332
|
+
role=d.get("role"),
|
|
333
|
+
is_required=d["is_required"],
|
|
334
|
+
min_version=d.get("min_version"),
|
|
335
|
+
)
|
|
336
|
+
for d in data.get("dependencies", [])
|
|
337
|
+
]
|
|
338
|
+
perms = None
|
|
339
|
+
if data.get("permissions"):
|
|
340
|
+
p = data["permissions"]
|
|
341
|
+
perms = PermissionsInfo(
|
|
342
|
+
network_level=p["network_level"],
|
|
343
|
+
filesystem_level=p["filesystem_level"],
|
|
344
|
+
code_execution_level=p["code_execution_level"],
|
|
345
|
+
data_access_level=p["data_access_level"],
|
|
346
|
+
user_approval_level=p["user_approval_level"],
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return InstallMetadata(
|
|
350
|
+
slug=data["slug"],
|
|
351
|
+
version=data["version"],
|
|
352
|
+
package_type=data["package_type"],
|
|
353
|
+
install_mode=data["install_mode"],
|
|
354
|
+
hosting_type=data["hosting_type"],
|
|
355
|
+
runtime=data["runtime"],
|
|
356
|
+
entrypoint=data.get("entrypoint"),
|
|
357
|
+
artifact=artifact,
|
|
358
|
+
capabilities=caps,
|
|
359
|
+
dependencies=deps,
|
|
360
|
+
permissions=perms,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# --- Download ---
|
|
364
|
+
|
|
365
|
+
def download(self, slug: str, version: str | None = None) -> str | None:
|
|
366
|
+
"""Track download and return artifact URL."""
|
|
367
|
+
params = {}
|
|
368
|
+
if version:
|
|
369
|
+
params["version"] = version
|
|
370
|
+
data = self._request("POST", f"/packages/{slug}/download", params=params)
|
|
371
|
+
return data.get("download_url")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Exception hierarchy for AgentNode SDK. Spec §14.1."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentNodeError(Exception):
|
|
5
|
+
"""Base error for all AgentNode API errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, code: str, message: str):
|
|
8
|
+
self.code = code
|
|
9
|
+
self.message = message
|
|
10
|
+
super().__init__(f"[{code}] {message}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotFoundError(AgentNodeError):
|
|
14
|
+
"""Package or resource not found (404)."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthError(AgentNodeError):
|
|
19
|
+
"""Authentication or authorization failure (401/403)."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationError(AgentNodeError):
|
|
24
|
+
"""Manifest or input validation failure (422)."""
|
|
25
|
+
pass
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Data models for the AgentNode SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SearchHit:
|
|
9
|
+
slug: str
|
|
10
|
+
name: str
|
|
11
|
+
package_type: str
|
|
12
|
+
summary: str
|
|
13
|
+
publisher_slug: str
|
|
14
|
+
trust_level: str
|
|
15
|
+
latest_version: str | None = None
|
|
16
|
+
runtime: str | None = None
|
|
17
|
+
capability_ids: list[str] = field(default_factory=list)
|
|
18
|
+
download_count: int = 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SearchResult:
|
|
23
|
+
query: str
|
|
24
|
+
hits: list[SearchHit]
|
|
25
|
+
total: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ScoreBreakdown:
|
|
30
|
+
capability: float
|
|
31
|
+
framework: float
|
|
32
|
+
runtime: float
|
|
33
|
+
trust: float
|
|
34
|
+
permissions: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ResolvedPackage:
|
|
39
|
+
slug: str
|
|
40
|
+
name: str
|
|
41
|
+
package_type: str
|
|
42
|
+
summary: str
|
|
43
|
+
version: str
|
|
44
|
+
publisher_slug: str
|
|
45
|
+
trust_level: str
|
|
46
|
+
score: float
|
|
47
|
+
breakdown: ScoreBreakdown
|
|
48
|
+
matched_capabilities: list[str] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ResolveResult:
|
|
53
|
+
results: list[ResolvedPackage]
|
|
54
|
+
total: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ArtifactInfo:
|
|
59
|
+
url: str | None
|
|
60
|
+
hash_sha256: str | None
|
|
61
|
+
size_bytes: int | None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class CapabilityInfo:
|
|
66
|
+
name: str
|
|
67
|
+
capability_id: str
|
|
68
|
+
capability_type: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class DependencyInfo:
|
|
73
|
+
package_slug: str
|
|
74
|
+
role: str | None
|
|
75
|
+
is_required: bool
|
|
76
|
+
min_version: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class PermissionsInfo:
|
|
81
|
+
network_level: str
|
|
82
|
+
filesystem_level: str
|
|
83
|
+
code_execution_level: str
|
|
84
|
+
data_access_level: str
|
|
85
|
+
user_approval_level: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class InstallMetadata:
|
|
90
|
+
slug: str
|
|
91
|
+
version: str
|
|
92
|
+
package_type: str
|
|
93
|
+
install_mode: str
|
|
94
|
+
hosting_type: str
|
|
95
|
+
runtime: str
|
|
96
|
+
entrypoint: str | None
|
|
97
|
+
artifact: ArtifactInfo | None
|
|
98
|
+
capabilities: list[CapabilityInfo] = field(default_factory=list)
|
|
99
|
+
dependencies: list[DependencyInfo] = field(default_factory=list)
|
|
100
|
+
permissions: PermissionsInfo | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class PackageDetail:
|
|
105
|
+
slug: str
|
|
106
|
+
name: str
|
|
107
|
+
package_type: str
|
|
108
|
+
summary: str
|
|
109
|
+
description: str | None
|
|
110
|
+
download_count: int
|
|
111
|
+
is_deprecated: bool
|
|
112
|
+
latest_version: str | None = None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentnode-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["ai", "agent", "capabilities", "agentnode", "mcp", "langchain"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Topic :: Software Development :: Libraries",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.25",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://agentnode.net"
|
|
27
|
+
Repository = "https://github.com/agentnode-ai/agentnode"
|
|
28
|
+
Documentation = "https://agentnode.net/docs"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest", "pytest-asyncio", "respx"]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["agentnode_sdk"]
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Unit tests for AgentNode SDK client."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import pytest
|
|
6
|
+
import respx
|
|
7
|
+
|
|
8
|
+
from agentnode_sdk import AgentNodeClient
|
|
9
|
+
|
|
10
|
+
BASE = "https://api.agentnode.net"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@respx.mock
|
|
14
|
+
def test_search():
|
|
15
|
+
respx.get(f"{BASE}/v1/search").mock(return_value=httpx.Response(200, json={
|
|
16
|
+
"query": "pdf",
|
|
17
|
+
"hits": [{
|
|
18
|
+
"slug": "pdf-reader",
|
|
19
|
+
"name": "PDF Reader",
|
|
20
|
+
"package_type": "toolpack",
|
|
21
|
+
"summary": "Read PDFs",
|
|
22
|
+
"publisher_slug": "test",
|
|
23
|
+
"trust_level": "verified",
|
|
24
|
+
"latest_version": "1.0.0",
|
|
25
|
+
"runtime": "python",
|
|
26
|
+
"capability_ids": ["pdf_extraction"],
|
|
27
|
+
"download_count": 100,
|
|
28
|
+
}],
|
|
29
|
+
"total": 1,
|
|
30
|
+
"limit": 20,
|
|
31
|
+
"offset": 0,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
with AgentNodeClient() as client:
|
|
35
|
+
result = client.search("pdf")
|
|
36
|
+
assert result.total == 1
|
|
37
|
+
assert result.hits[0].slug == "pdf-reader"
|
|
38
|
+
assert result.hits[0].capability_ids == ["pdf_extraction"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@respx.mock
|
|
42
|
+
def test_resolve():
|
|
43
|
+
respx.post(f"{BASE}/v1/resolve").mock(return_value=httpx.Response(200, json={
|
|
44
|
+
"results": [{
|
|
45
|
+
"slug": "pdf-reader",
|
|
46
|
+
"name": "PDF Reader",
|
|
47
|
+
"package_type": "toolpack",
|
|
48
|
+
"summary": "Read PDFs",
|
|
49
|
+
"version": "1.0.0",
|
|
50
|
+
"publisher_slug": "test",
|
|
51
|
+
"trust_level": "verified",
|
|
52
|
+
"score": 0.85,
|
|
53
|
+
"breakdown": {
|
|
54
|
+
"capability": 1.0,
|
|
55
|
+
"framework": 1.0,
|
|
56
|
+
"runtime": 1.0,
|
|
57
|
+
"trust": 0.5,
|
|
58
|
+
"permissions": 0.9,
|
|
59
|
+
},
|
|
60
|
+
"matched_capabilities": ["pdf_extraction"],
|
|
61
|
+
}],
|
|
62
|
+
"total": 1,
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
with AgentNodeClient() as client:
|
|
66
|
+
result = client.resolve(["pdf_extraction"])
|
|
67
|
+
assert result.total == 1
|
|
68
|
+
assert result.results[0].score == 0.85
|
|
69
|
+
assert result.results[0].breakdown.capability == 1.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@respx.mock
|
|
73
|
+
def test_get_package():
|
|
74
|
+
respx.get(f"{BASE}/v1/packages/pdf-reader").mock(return_value=httpx.Response(200, json={
|
|
75
|
+
"slug": "pdf-reader",
|
|
76
|
+
"name": "PDF Reader",
|
|
77
|
+
"package_type": "toolpack",
|
|
78
|
+
"summary": "Read PDFs",
|
|
79
|
+
"description": "A great PDF reader",
|
|
80
|
+
"download_count": 42,
|
|
81
|
+
"is_deprecated": False,
|
|
82
|
+
"latest_version": {"version_number": "1.0.0", "channel": "stable", "published_at": "2026-01-01T00:00:00Z"},
|
|
83
|
+
"publisher": {"slug": "test", "display_name": "Test", "trust_level": "verified"},
|
|
84
|
+
"blocks": {},
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
with AgentNodeClient() as client:
|
|
88
|
+
pkg = client.get_package("pdf-reader")
|
|
89
|
+
assert pkg.slug == "pdf-reader"
|
|
90
|
+
assert pkg.latest_version == "1.0.0"
|
|
91
|
+
assert pkg.download_count == 42
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@respx.mock
|
|
95
|
+
def test_get_install_metadata():
|
|
96
|
+
respx.get(f"{BASE}/v1/packages/pdf-reader/install").mock(return_value=httpx.Response(200, json={
|
|
97
|
+
"slug": "pdf-reader",
|
|
98
|
+
"version": "1.0.0",
|
|
99
|
+
"package_type": "toolpack",
|
|
100
|
+
"install_mode": "package",
|
|
101
|
+
"hosting_type": "agentnode_hosted",
|
|
102
|
+
"runtime": "python",
|
|
103
|
+
"entrypoint": "pdf_reader.tool",
|
|
104
|
+
"artifact": {"url": "https://s3.example.com/artifact.tar.gz", "hash_sha256": "abc123", "size_bytes": 1000},
|
|
105
|
+
"capabilities": [{"name": "extract", "capability_id": "pdf_extraction", "capability_type": "tool"}],
|
|
106
|
+
"dependencies": [],
|
|
107
|
+
"permissions": {
|
|
108
|
+
"network_level": "none",
|
|
109
|
+
"filesystem_level": "temp",
|
|
110
|
+
"code_execution_level": "none",
|
|
111
|
+
"data_access_level": "input_only",
|
|
112
|
+
"user_approval_level": "never",
|
|
113
|
+
},
|
|
114
|
+
"published_at": "2026-01-01T00:00:00Z",
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
with AgentNodeClient() as client:
|
|
118
|
+
meta = client.get_install_metadata("pdf-reader")
|
|
119
|
+
assert meta.slug == "pdf-reader"
|
|
120
|
+
assert meta.entrypoint == "pdf_reader.tool"
|
|
121
|
+
assert meta.artifact.url == "https://s3.example.com/artifact.tar.gz"
|
|
122
|
+
assert len(meta.capabilities) == 1
|
|
123
|
+
assert meta.permissions.network_level == "none"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@respx.mock
|
|
127
|
+
def test_error_handling():
|
|
128
|
+
respx.get(f"{BASE}/v1/packages/nonexistent").mock(return_value=httpx.Response(404, json={
|
|
129
|
+
"error": {"code": "PACKAGE_NOT_FOUND", "message": "Not found", "details": {}}
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
with AgentNodeClient() as client:
|
|
133
|
+
with pytest.raises(Exception) as exc_info:
|
|
134
|
+
client.get_package("nonexistent")
|
|
135
|
+
assert "PACKAGE_NOT_FOUND" in str(exc_info.value)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@respx.mock
|
|
139
|
+
def test_api_key_auth():
|
|
140
|
+
route = respx.get(f"{BASE}/v1/packages/test").mock(return_value=httpx.Response(200, json={
|
|
141
|
+
"slug": "test", "name": "Test", "package_type": "toolpack",
|
|
142
|
+
"summary": "Test", "description": None, "download_count": 0,
|
|
143
|
+
"is_deprecated": False, "latest_version": None,
|
|
144
|
+
"publisher": {"slug": "t", "display_name": "T", "trust_level": "unverified"},
|
|
145
|
+
"blocks": {},
|
|
146
|
+
}))
|
|
147
|
+
|
|
148
|
+
with AgentNodeClient(api_key="ank_test123") as client:
|
|
149
|
+
client.get_package("test")
|
|
150
|
+
|
|
151
|
+
assert route.called
|
|
152
|
+
assert route.calls[0].request.headers["x-api-key"] == "ank_test123"
|