authora-agentnet 0.1.4__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.
- authora_agentnet-0.1.4/.github/workflows/publish.yml +30 -0
- authora_agentnet-0.1.4/.gitignore +14 -0
- authora_agentnet-0.1.4/LICENSE +21 -0
- authora_agentnet-0.1.4/PKG-INFO +114 -0
- authora_agentnet-0.1.4/README.md +88 -0
- authora_agentnet-0.1.4/agentnet/__init__.py +109 -0
- authora_agentnet-0.1.4/agentnet/_http.py +171 -0
- authora_agentnet-0.1.4/agentnet/errors.py +62 -0
- authora_agentnet-0.1.4/agentnet/resources/__init__.py +3 -0
- authora_agentnet-0.1.4/agentnet/resources/tasks.py +192 -0
- authora_agentnet-0.1.4/pyproject.toml +38 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.12'
|
|
22
|
+
|
|
23
|
+
- name: Install build tools
|
|
24
|
+
run: pip install build
|
|
25
|
+
|
|
26
|
+
- name: Build package
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentNet (Authora)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authora-agentnet
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Official Python SDK for AgentNet -- AI engineering work as a service.
|
|
5
|
+
Project-URL: Homepage, https://net.authora.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/authora-dev/agentnet-python
|
|
7
|
+
Project-URL: Issues, https://github.com/authora-dev/agentnet-python/issues
|
|
8
|
+
Author-email: AgentNet <sdk@authora.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agentnet,ai,authora,automation,code-review,sdk
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Provides-Extra: async
|
|
24
|
+
Requires-Dist: aiohttp>=3.9; extra == 'async'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# authora-agentnet
|
|
28
|
+
|
|
29
|
+
Official Python SDK for [AgentNet](https://net.authora.dev) -- AI engineering work as a service.
|
|
30
|
+
|
|
31
|
+
- **Complete runtime** -- not a REST wrapper
|
|
32
|
+
- **Sync + Async** clients
|
|
33
|
+
- **Zero dependencies** for sync (uses `urllib`), optional `aiohttp` for async
|
|
34
|
+
- **Python 3.9+**
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install authora-agentnet
|
|
40
|
+
|
|
41
|
+
# For async support:
|
|
42
|
+
pip install authora-agentnet[async]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from agentnet import AgentNetClient
|
|
49
|
+
|
|
50
|
+
client = AgentNetClient(api_key="ank_live_...")
|
|
51
|
+
|
|
52
|
+
result = client.tasks.submit_and_wait(
|
|
53
|
+
skill="code-review",
|
|
54
|
+
input='function auth(u,p) { return db.query("SELECT * WHERE user="+u); }',
|
|
55
|
+
description="Review for SQL injection",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(result["output"])
|
|
59
|
+
print(f"Cost: ${result['cost']['actual_usdc']}")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Async
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from agentnet import AsyncAgentNetClient
|
|
66
|
+
|
|
67
|
+
client = AsyncAgentNetClient(api_key="ank_live_...")
|
|
68
|
+
|
|
69
|
+
result = await client.tasks.submit_and_wait(
|
|
70
|
+
skill="code-review",
|
|
71
|
+
input=code,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
await client.close()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Stream Events
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
task = client.tasks.submit(skill="code-review", input=code)
|
|
81
|
+
|
|
82
|
+
for event in client.tasks.stream(task["id"]):
|
|
83
|
+
if event["type"] == "action_required":
|
|
84
|
+
print(event["message"])
|
|
85
|
+
event["_acknowledge"]() # proceed
|
|
86
|
+
elif event["type"] == "completed":
|
|
87
|
+
print(event["result"]["output"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Batch
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
results = client.tasks.submit_batch([
|
|
94
|
+
{"skill": "code-review", "input": file1},
|
|
95
|
+
{"skill": "code-review", "input": file2},
|
|
96
|
+
], concurrency=5)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Error Handling
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from agentnet import InsufficientFundsError, NoWorkersError
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
client.tasks.submit(skill="code-review", input=code)
|
|
106
|
+
except InsufficientFundsError as e:
|
|
107
|
+
print(f"Need more funds. Balance: {e.balance_cents}c")
|
|
108
|
+
except NoWorkersError as e:
|
|
109
|
+
print(f"Try regions: {e.alternative_regions}")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# authora-agentnet
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [AgentNet](https://net.authora.dev) -- AI engineering work as a service.
|
|
4
|
+
|
|
5
|
+
- **Complete runtime** -- not a REST wrapper
|
|
6
|
+
- **Sync + Async** clients
|
|
7
|
+
- **Zero dependencies** for sync (uses `urllib`), optional `aiohttp` for async
|
|
8
|
+
- **Python 3.9+**
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install authora-agentnet
|
|
14
|
+
|
|
15
|
+
# For async support:
|
|
16
|
+
pip install authora-agentnet[async]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from agentnet import AgentNetClient
|
|
23
|
+
|
|
24
|
+
client = AgentNetClient(api_key="ank_live_...")
|
|
25
|
+
|
|
26
|
+
result = client.tasks.submit_and_wait(
|
|
27
|
+
skill="code-review",
|
|
28
|
+
input='function auth(u,p) { return db.query("SELECT * WHERE user="+u); }',
|
|
29
|
+
description="Review for SQL injection",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
print(result["output"])
|
|
33
|
+
print(f"Cost: ${result['cost']['actual_usdc']}")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Async
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from agentnet import AsyncAgentNetClient
|
|
40
|
+
|
|
41
|
+
client = AsyncAgentNetClient(api_key="ank_live_...")
|
|
42
|
+
|
|
43
|
+
result = await client.tasks.submit_and_wait(
|
|
44
|
+
skill="code-review",
|
|
45
|
+
input=code,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
await client.close()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Stream Events
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
task = client.tasks.submit(skill="code-review", input=code)
|
|
55
|
+
|
|
56
|
+
for event in client.tasks.stream(task["id"]):
|
|
57
|
+
if event["type"] == "action_required":
|
|
58
|
+
print(event["message"])
|
|
59
|
+
event["_acknowledge"]() # proceed
|
|
60
|
+
elif event["type"] == "completed":
|
|
61
|
+
print(event["result"]["output"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Batch
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
results = client.tasks.submit_batch([
|
|
68
|
+
{"skill": "code-review", "input": file1},
|
|
69
|
+
{"skill": "code-review", "input": file2},
|
|
70
|
+
], concurrency=5)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Error Handling
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from agentnet import InsufficientFundsError, NoWorkersError
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
client.tasks.submit(skill="code-review", input=code)
|
|
80
|
+
except InsufficientFundsError as e:
|
|
81
|
+
print(f"Need more funds. Balance: {e.balance_cents}c")
|
|
82
|
+
except NoWorkersError as e:
|
|
83
|
+
print(f"Try regions: {e.alternative_regions}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""AgentNet Python SDK -- AI engineering work as a service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._http import SyncHttpClient, AsyncHttpClient
|
|
8
|
+
from .errors import (
|
|
9
|
+
AgentNetError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
AuthorizationError,
|
|
12
|
+
InsufficientFundsError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
NoWorkersError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
TaskError,
|
|
18
|
+
TimeoutError,
|
|
19
|
+
)
|
|
20
|
+
from .resources.tasks import TasksResource, AsyncTasksResource
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.4"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"AgentNetClient",
|
|
26
|
+
"AsyncAgentNetClient",
|
|
27
|
+
"AgentNetError",
|
|
28
|
+
"AuthenticationError",
|
|
29
|
+
"AuthorizationError",
|
|
30
|
+
"InsufficientFundsError",
|
|
31
|
+
"NetworkError",
|
|
32
|
+
"NoWorkersError",
|
|
33
|
+
"NotFoundError",
|
|
34
|
+
"RateLimitError",
|
|
35
|
+
"TaskError",
|
|
36
|
+
"TimeoutError",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
DEFAULT_BASE_URL = "https://net.authora.dev/api/v1"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AgentNetClient:
|
|
43
|
+
"""Synchronous AgentNet client.
|
|
44
|
+
|
|
45
|
+
Example::
|
|
46
|
+
|
|
47
|
+
from agentnet import AgentNetClient
|
|
48
|
+
|
|
49
|
+
client = AgentNetClient(api_key="ank_live_...")
|
|
50
|
+
result = client.tasks.submit_and_wait(skill="code-review", input="eval(x)")
|
|
51
|
+
print(result["output"])
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, timeout: int = 30):
|
|
55
|
+
if not api_key:
|
|
56
|
+
raise ValueError("api_key is required")
|
|
57
|
+
self._http = SyncHttpClient(base_url, api_key, timeout)
|
|
58
|
+
self.tasks = TasksResource(self._http)
|
|
59
|
+
|
|
60
|
+
def quote(self, skill: str, region: str | None = None, priority: str = "standard") -> dict[str, Any]:
|
|
61
|
+
"""Get a price quote and worker availability."""
|
|
62
|
+
body: dict[str, Any] = {"skillId": skill, "slaTier": priority}
|
|
63
|
+
if region:
|
|
64
|
+
body["region"] = region
|
|
65
|
+
return self._http.post("/tasks/quote", body=body)
|
|
66
|
+
|
|
67
|
+
def skills(self) -> list[dict[str, Any]]:
|
|
68
|
+
"""List available skills."""
|
|
69
|
+
result = self._http.get("/registry/skills")
|
|
70
|
+
return result if isinstance(result, list) else result.get("items", [])
|
|
71
|
+
|
|
72
|
+
def balance(self) -> dict[str, Any]:
|
|
73
|
+
"""Get account balance."""
|
|
74
|
+
return self._http.get("/account/credits")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AsyncAgentNetClient:
|
|
78
|
+
"""Async AgentNet client (requires aiohttp).
|
|
79
|
+
|
|
80
|
+
Example::
|
|
81
|
+
|
|
82
|
+
from agentnet import AsyncAgentNetClient
|
|
83
|
+
|
|
84
|
+
client = AsyncAgentNetClient(api_key="ank_live_...")
|
|
85
|
+
result = await client.tasks.submit_and_wait(skill="code-review", input="eval(x)")
|
|
86
|
+
print(result["output"])
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, timeout: int = 30):
|
|
90
|
+
if not api_key:
|
|
91
|
+
raise ValueError("api_key is required")
|
|
92
|
+
self._http = AsyncHttpClient(base_url, api_key, timeout)
|
|
93
|
+
self.tasks = AsyncTasksResource(self._http)
|
|
94
|
+
|
|
95
|
+
async def quote(self, skill: str, region: str | None = None, priority: str = "standard") -> dict[str, Any]:
|
|
96
|
+
body: dict[str, Any] = {"skillId": skill, "slaTier": priority}
|
|
97
|
+
if region:
|
|
98
|
+
body["region"] = region
|
|
99
|
+
return await self._http.post("/tasks/quote", body=body)
|
|
100
|
+
|
|
101
|
+
async def skills(self) -> list[dict[str, Any]]:
|
|
102
|
+
result = await self._http.get("/registry/skills")
|
|
103
|
+
return result if isinstance(result, list) else result.get("items", [])
|
|
104
|
+
|
|
105
|
+
async def balance(self) -> dict[str, Any]:
|
|
106
|
+
return await self._http.get("/account/credits")
|
|
107
|
+
|
|
108
|
+
async def close(self):
|
|
109
|
+
await self._http.close()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""HTTP client for AgentNet API -- sync and async variants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlencode, urljoin
|
|
8
|
+
from urllib.request import Request, urlopen
|
|
9
|
+
from urllib.error import URLError, HTTPError
|
|
10
|
+
|
|
11
|
+
from .errors import (
|
|
12
|
+
AgentNetError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
AuthorizationError,
|
|
15
|
+
InsufficientFundsError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
TimeoutError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SyncHttpClient:
|
|
24
|
+
"""Synchronous HTTP client using urllib (zero dependencies)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, base_url: str, api_key: str, timeout: int = 30):
|
|
27
|
+
self.base_url = base_url.rstrip("/")
|
|
28
|
+
self.api_key = api_key
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
|
|
31
|
+
def get(self, path: str, query: dict[str, Any] | None = None) -> Any:
|
|
32
|
+
return self._request("GET", path, query=query)
|
|
33
|
+
|
|
34
|
+
def post(self, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
|
|
35
|
+
return self._request("POST", path, body=body, query=query)
|
|
36
|
+
|
|
37
|
+
def put(self, path: str, body: Any = None) -> Any:
|
|
38
|
+
return self._request("PUT", path, body=body)
|
|
39
|
+
|
|
40
|
+
def delete(self, path: str) -> Any:
|
|
41
|
+
return self._request("DELETE", path)
|
|
42
|
+
|
|
43
|
+
def _request(self, method: str, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
|
|
44
|
+
url = f"{self.base_url}{path}"
|
|
45
|
+
if query:
|
|
46
|
+
params = {k: str(v) for k, v in query.items() if v is not None}
|
|
47
|
+
if params:
|
|
48
|
+
url = f"{url}?{urlencode(params)}"
|
|
49
|
+
|
|
50
|
+
headers = {
|
|
51
|
+
"Accept": "application/json",
|
|
52
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
data = None
|
|
56
|
+
if body is not None and method != "GET":
|
|
57
|
+
headers["Content-Type"] = "application/json"
|
|
58
|
+
data = json.dumps(body).encode("utf-8")
|
|
59
|
+
|
|
60
|
+
req = Request(url, data=data, headers=headers, method=method)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with urlopen(req, timeout=self.timeout) as resp:
|
|
64
|
+
raw = resp.read().decode("utf-8")
|
|
65
|
+
result = json.loads(raw) if raw else {}
|
|
66
|
+
return self._unwrap(result)
|
|
67
|
+
except HTTPError as e:
|
|
68
|
+
body_text = e.read().decode("utf-8", errors="replace")
|
|
69
|
+
try:
|
|
70
|
+
err_body = json.loads(body_text)
|
|
71
|
+
except (json.JSONDecodeError, ValueError):
|
|
72
|
+
err_body = {"message": body_text}
|
|
73
|
+
self._throw_for_status(e.code, err_body, method, path)
|
|
74
|
+
except URLError as e:
|
|
75
|
+
if "timed out" in str(e.reason):
|
|
76
|
+
raise TimeoutError(f"Request to {method} {path} timed out after {self.timeout}s")
|
|
77
|
+
raise NetworkError(f"Request to {method} {path} failed: {e.reason}")
|
|
78
|
+
|
|
79
|
+
def _unwrap(self, body: Any) -> Any:
|
|
80
|
+
if isinstance(body, dict) and "data" in body:
|
|
81
|
+
data = body["data"]
|
|
82
|
+
if isinstance(data, list):
|
|
83
|
+
pagination = body.get("pagination") or body.get("meta")
|
|
84
|
+
if pagination:
|
|
85
|
+
return {"items": data, "total": pagination.get("total", len(data))}
|
|
86
|
+
return {"items": data}
|
|
87
|
+
return data
|
|
88
|
+
return body
|
|
89
|
+
|
|
90
|
+
def _throw_for_status(self, status: int, body: Any, method: str, path: str) -> None:
|
|
91
|
+
parsed = self._parse_error(body)
|
|
92
|
+
prefix = f"{method} {path}"
|
|
93
|
+
|
|
94
|
+
if status == 401:
|
|
95
|
+
raise AuthenticationError(parsed.get("message", f"{prefix}: Authentication failed"))
|
|
96
|
+
elif status == 402:
|
|
97
|
+
raise InsufficientFundsError(parsed.get("message", f"{prefix}: Insufficient funds"))
|
|
98
|
+
elif status == 403:
|
|
99
|
+
raise AuthorizationError(parsed.get("message", f"{prefix}: Forbidden"))
|
|
100
|
+
elif status == 404:
|
|
101
|
+
raise NotFoundError(parsed.get("message", f"{prefix}: Not found"))
|
|
102
|
+
elif status == 429:
|
|
103
|
+
raise RateLimitError(parsed.get("message", f"{prefix}: Rate limit exceeded"))
|
|
104
|
+
else:
|
|
105
|
+
raise AgentNetError(parsed.get("message", f"{prefix}: Status {status}"), status, parsed.get("code"))
|
|
106
|
+
|
|
107
|
+
def _parse_error(self, body: Any) -> dict[str, Any]:
|
|
108
|
+
if isinstance(body, dict):
|
|
109
|
+
if "error" in body and isinstance(body["error"], dict):
|
|
110
|
+
return body["error"]
|
|
111
|
+
return body
|
|
112
|
+
return {"message": str(body)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AsyncHttpClient:
|
|
116
|
+
"""Async HTTP client using aiohttp (optional dependency)."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, base_url: str, api_key: str, timeout: int = 30):
|
|
119
|
+
self.base_url = base_url.rstrip("/")
|
|
120
|
+
self.api_key = api_key
|
|
121
|
+
self.timeout = timeout
|
|
122
|
+
self._session = None
|
|
123
|
+
|
|
124
|
+
async def _get_session(self):
|
|
125
|
+
if self._session is None or self._session.closed:
|
|
126
|
+
try:
|
|
127
|
+
import aiohttp
|
|
128
|
+
except ImportError:
|
|
129
|
+
raise ImportError("Install aiohttp for async support: pip install agentnet[async]")
|
|
130
|
+
self._session = aiohttp.ClientSession(
|
|
131
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
132
|
+
headers={
|
|
133
|
+
"Accept": "application/json",
|
|
134
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
return self._session
|
|
138
|
+
|
|
139
|
+
async def get(self, path: str, query: dict[str, Any] | None = None) -> Any:
|
|
140
|
+
return await self._request("GET", path, query=query)
|
|
141
|
+
|
|
142
|
+
async def post(self, path: str, body: Any = None) -> Any:
|
|
143
|
+
return await self._request("POST", path, body=body)
|
|
144
|
+
|
|
145
|
+
async def delete(self, path: str) -> Any:
|
|
146
|
+
return await self._request("DELETE", path)
|
|
147
|
+
|
|
148
|
+
async def _request(self, method: str, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
|
|
149
|
+
session = await self._get_session()
|
|
150
|
+
url = f"{self.base_url}{path}"
|
|
151
|
+
kwargs: dict[str, Any] = {}
|
|
152
|
+
if body is not None:
|
|
153
|
+
kwargs["json"] = body
|
|
154
|
+
if query:
|
|
155
|
+
kwargs["params"] = {k: str(v) for k, v in query.items() if v is not None}
|
|
156
|
+
|
|
157
|
+
async with session.request(method, url, **kwargs) as resp:
|
|
158
|
+
if resp.content_type == "application/json":
|
|
159
|
+
data = await resp.json()
|
|
160
|
+
else:
|
|
161
|
+
data = await resp.text()
|
|
162
|
+
|
|
163
|
+
if resp.status >= 400:
|
|
164
|
+
err = data if isinstance(data, dict) else {"message": str(data)}
|
|
165
|
+
SyncHttpClient._throw_for_status(None, resp.status, err, method, path) # type: ignore
|
|
166
|
+
|
|
167
|
+
return SyncHttpClient._unwrap(None, data) # type: ignore
|
|
168
|
+
|
|
169
|
+
async def close(self):
|
|
170
|
+
if self._session and not self._session.closed:
|
|
171
|
+
await self._session.close()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""AgentNet SDK error hierarchy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentNetError(Exception):
|
|
5
|
+
"""Base error for all AgentNet SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int = 0, code: str | None = None, details: object = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.code = code
|
|
11
|
+
self.details = details
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NetworkError(AgentNetError):
|
|
15
|
+
def __init__(self, message: str = "Network error", details: object = None):
|
|
16
|
+
super().__init__(message, 0, "NETWORK_ERROR", details)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TimeoutError(AgentNetError):
|
|
20
|
+
def __init__(self, message: str = "Request timed out"):
|
|
21
|
+
super().__init__(message, 408, "TIMEOUT")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(AgentNetError):
|
|
25
|
+
def __init__(self, message: str = "Authentication failed", details: object = None):
|
|
26
|
+
super().__init__(message, 401, "AUTHENTICATION_ERROR", details)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthorizationError(AgentNetError):
|
|
30
|
+
def __init__(self, message: str = "Forbidden", details: object = None):
|
|
31
|
+
super().__init__(message, 403, "AUTHORIZATION_ERROR", details)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NotFoundError(AgentNetError):
|
|
35
|
+
def __init__(self, message: str = "Resource not found", details: object = None):
|
|
36
|
+
super().__init__(message, 404, "NOT_FOUND", details)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RateLimitError(AgentNetError):
|
|
40
|
+
def __init__(self, message: str = "Rate limit exceeded", retry_after: int | None = None, details: object = None):
|
|
41
|
+
super().__init__(message, 429, "RATE_LIMIT", details)
|
|
42
|
+
self.retry_after = retry_after
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InsufficientFundsError(AgentNetError):
|
|
46
|
+
def __init__(self, message: str, balance_cents: int = 0, required_cents: int = 0, details: object = None):
|
|
47
|
+
super().__init__(message, 402, "INSUFFICIENT_FUNDS", details)
|
|
48
|
+
self.balance_cents = balance_cents
|
|
49
|
+
self.required_cents = required_cents
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NoWorkersError(AgentNetError):
|
|
53
|
+
def __init__(self, message: str, region: str | None = None, alternative_regions: list[str] | None = None):
|
|
54
|
+
super().__init__(message, 503, "NO_WORKERS")
|
|
55
|
+
self.region = region
|
|
56
|
+
self.alternative_regions = alternative_regions or []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TaskError(AgentNetError):
|
|
60
|
+
def __init__(self, message: str, task_id: str, code: str | None = None):
|
|
61
|
+
super().__init__(message, 500, code or "TASK_ERROR")
|
|
62
|
+
self.task_id = task_id
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Tasks resource -- submit, wait, stream, batch, cancel, retry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Iterator
|
|
7
|
+
|
|
8
|
+
from ..errors import TaskError, TimeoutError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TasksResource:
|
|
12
|
+
"""Sync tasks resource."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, http):
|
|
15
|
+
self._http = http
|
|
16
|
+
|
|
17
|
+
def submit(self, *, skill: str, input: str, description: str = "", priority: str = "standard",
|
|
18
|
+
region: str | None = None, min_trust_score: int | None = None) -> dict[str, Any]:
|
|
19
|
+
"""Submit a task for execution."""
|
|
20
|
+
body = {"skillId": skill, "inputEncrypted": input, "description": description, "slaTier": priority}
|
|
21
|
+
if region:
|
|
22
|
+
body["region"] = region
|
|
23
|
+
if min_trust_score is not None:
|
|
24
|
+
body["minTrustScore"] = min_trust_score
|
|
25
|
+
return self._http.post("/tasks", body=body)
|
|
26
|
+
|
|
27
|
+
def get(self, task_id: str) -> dict[str, Any]:
|
|
28
|
+
"""Get task details."""
|
|
29
|
+
return self._http.get(f"/tasks/{task_id}")
|
|
30
|
+
|
|
31
|
+
def status(self, task_id: str) -> dict[str, Any]:
|
|
32
|
+
"""Get task status with customer events."""
|
|
33
|
+
return self._http.get(f"/tasks/{task_id}/status")
|
|
34
|
+
|
|
35
|
+
def cancel(self, task_id: str) -> None:
|
|
36
|
+
"""Cancel a task."""
|
|
37
|
+
self._http.post(f"/tasks/{task_id}/cancel")
|
|
38
|
+
|
|
39
|
+
def retry(self, task_id: str) -> dict[str, Any]:
|
|
40
|
+
"""Retry a failed task."""
|
|
41
|
+
return self._http.post(f"/tasks/{task_id}/retry")
|
|
42
|
+
|
|
43
|
+
def acknowledge(self, task_id: str) -> None:
|
|
44
|
+
"""Acknowledge an action_required event."""
|
|
45
|
+
self._http.post(f"/tasks/{task_id}/acknowledge")
|
|
46
|
+
|
|
47
|
+
def wait(self, task_id: str, timeout: int = 300, poll_interval: int = 3) -> dict[str, Any]:
|
|
48
|
+
"""Wait for task completion by polling."""
|
|
49
|
+
deadline = time.time() + timeout
|
|
50
|
+
while time.time() < deadline:
|
|
51
|
+
task = self.get(task_id)
|
|
52
|
+
if task.get("status") == "completed":
|
|
53
|
+
return self._build_result(task)
|
|
54
|
+
if task.get("status") in ("failed", "cancelled"):
|
|
55
|
+
raise TaskError(f"Task {task_id} {task['status']}", task_id, task["status"])
|
|
56
|
+
time.sleep(poll_interval)
|
|
57
|
+
raise TimeoutError(f"Task {task_id} did not complete within {timeout}s")
|
|
58
|
+
|
|
59
|
+
def submit_and_wait(self, *, skill: str, input: str, description: str = "",
|
|
60
|
+
priority: str = "standard", timeout: int = 300, **kwargs) -> dict[str, Any]:
|
|
61
|
+
"""Submit a task and wait for completion."""
|
|
62
|
+
task = self.submit(skill=skill, input=input, description=description, priority=priority, **kwargs)
|
|
63
|
+
return self.wait(task["id"], timeout=timeout)
|
|
64
|
+
|
|
65
|
+
def stream(self, task_id: str, poll_interval: int = 2) -> Iterator[dict[str, Any]]:
|
|
66
|
+
"""Stream task events. Yields event dicts with type, message, actions."""
|
|
67
|
+
last_count = 0
|
|
68
|
+
while True:
|
|
69
|
+
data = self.status(task_id)
|
|
70
|
+
events = data.get("customerEvents", [])
|
|
71
|
+
for i in range(last_count, len(events)):
|
|
72
|
+
evt = events[i]
|
|
73
|
+
evt["_acknowledge"] = lambda: self.acknowledge(task_id)
|
|
74
|
+
evt["_cancel"] = lambda: self.cancel(task_id)
|
|
75
|
+
yield evt
|
|
76
|
+
last_count = len(events)
|
|
77
|
+
|
|
78
|
+
if data.get("status") == "completed":
|
|
79
|
+
result = self._build_result(data)
|
|
80
|
+
yield {"type": "completed", "message": "Task completed", "result": result}
|
|
81
|
+
return
|
|
82
|
+
if data.get("status") in ("failed", "cancelled"):
|
|
83
|
+
yield {"type": "failed", "message": f"Task {data['status']}"}
|
|
84
|
+
return
|
|
85
|
+
time.sleep(poll_interval)
|
|
86
|
+
|
|
87
|
+
def submit_batch(self, tasks: list[dict[str, Any]], concurrency: int = 5,
|
|
88
|
+
on_progress=None) -> dict[str, Any]:
|
|
89
|
+
"""Submit multiple tasks and wait for all. Returns results and failures."""
|
|
90
|
+
import concurrent.futures
|
|
91
|
+
|
|
92
|
+
results = []
|
|
93
|
+
failed = []
|
|
94
|
+
completed = 0
|
|
95
|
+
|
|
96
|
+
def run_one(params):
|
|
97
|
+
nonlocal completed
|
|
98
|
+
try:
|
|
99
|
+
result = self.submit_and_wait(**params)
|
|
100
|
+
results.append(result)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
failed.append({"params": params, "error": str(e)})
|
|
103
|
+
finally:
|
|
104
|
+
completed += 1
|
|
105
|
+
if on_progress:
|
|
106
|
+
on_progress(completed, len(tasks))
|
|
107
|
+
|
|
108
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool:
|
|
109
|
+
pool.map(run_one, tasks)
|
|
110
|
+
|
|
111
|
+
return {"results": results, "failed": failed}
|
|
112
|
+
|
|
113
|
+
def _build_result(self, task: dict[str, Any]) -> dict[str, Any]:
|
|
114
|
+
full = self._http.get(f"/tasks/{task['id']}")
|
|
115
|
+
deliverables = []
|
|
116
|
+
try:
|
|
117
|
+
art_data = self._http.get(f"/tasks/{task['id']}/artifacts")
|
|
118
|
+
deliverables = art_data.get("artifacts", [])
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"id": task["id"],
|
|
124
|
+
"status": task.get("status", "completed"),
|
|
125
|
+
"output": full.get("outputEncrypted", ""),
|
|
126
|
+
"cost": {
|
|
127
|
+
"estimate_usdc": float(full.get("priceEstimateUsdc", 0)),
|
|
128
|
+
"actual_usdc": float(full.get("actualCostUsdc", 0)),
|
|
129
|
+
},
|
|
130
|
+
"duration_seconds": full.get("durationSeconds", 0),
|
|
131
|
+
"model": full.get("model"),
|
|
132
|
+
"deliverables": deliverables,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class AsyncTasksResource:
|
|
137
|
+
"""Async tasks resource."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, http):
|
|
140
|
+
self._http = http
|
|
141
|
+
|
|
142
|
+
async def submit(self, *, skill: str, input: str, description: str = "",
|
|
143
|
+
priority: str = "standard", **kwargs) -> dict[str, Any]:
|
|
144
|
+
body = {"skillId": skill, "inputEncrypted": input, "description": description, "slaTier": priority}
|
|
145
|
+
body.update(kwargs)
|
|
146
|
+
return await self._http.post("/tasks", body=body)
|
|
147
|
+
|
|
148
|
+
async def get(self, task_id: str) -> dict[str, Any]:
|
|
149
|
+
return await self._http.get(f"/tasks/{task_id}")
|
|
150
|
+
|
|
151
|
+
async def status(self, task_id: str) -> dict[str, Any]:
|
|
152
|
+
return await self._http.get(f"/tasks/{task_id}/status")
|
|
153
|
+
|
|
154
|
+
async def cancel(self, task_id: str) -> None:
|
|
155
|
+
await self._http.post(f"/tasks/{task_id}/cancel")
|
|
156
|
+
|
|
157
|
+
async def acknowledge(self, task_id: str) -> None:
|
|
158
|
+
await self._http.post(f"/tasks/{task_id}/acknowledge")
|
|
159
|
+
|
|
160
|
+
async def wait(self, task_id: str, timeout: int = 300, poll_interval: int = 3) -> dict[str, Any]:
|
|
161
|
+
import asyncio
|
|
162
|
+
deadline = time.time() + timeout
|
|
163
|
+
while time.time() < deadline:
|
|
164
|
+
task = await self.get(task_id)
|
|
165
|
+
if task.get("status") == "completed":
|
|
166
|
+
return await self._build_result(task)
|
|
167
|
+
if task.get("status") in ("failed", "cancelled"):
|
|
168
|
+
raise TaskError(f"Task {task_id} {task['status']}", task_id, task["status"])
|
|
169
|
+
await asyncio.sleep(poll_interval)
|
|
170
|
+
raise TimeoutError(f"Task {task_id} did not complete within {timeout}s")
|
|
171
|
+
|
|
172
|
+
async def submit_and_wait(self, *, skill: str, input: str, timeout: int = 300, **kwargs) -> dict[str, Any]:
|
|
173
|
+
task = await self.submit(skill=skill, input=input, **kwargs)
|
|
174
|
+
return await self.wait(task["id"], timeout=timeout)
|
|
175
|
+
|
|
176
|
+
async def _build_result(self, task: dict[str, Any]) -> dict[str, Any]:
|
|
177
|
+
full = await self._http.get(f"/tasks/{task['id']}")
|
|
178
|
+
deliverables = []
|
|
179
|
+
try:
|
|
180
|
+
art_data = await self._http.get(f"/tasks/{task['id']}/artifacts")
|
|
181
|
+
deliverables = art_data.get("artifacts", [])
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
return {
|
|
185
|
+
"id": task["id"],
|
|
186
|
+
"status": task.get("status", "completed"),
|
|
187
|
+
"output": full.get("outputEncrypted", ""),
|
|
188
|
+
"cost": {"estimate_usdc": float(full.get("priceEstimateUsdc", 0)), "actual_usdc": float(full.get("actualCostUsdc", 0))},
|
|
189
|
+
"duration_seconds": full.get("durationSeconds", 0),
|
|
190
|
+
"model": full.get("model"),
|
|
191
|
+
"deliverables": deliverables,
|
|
192
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "authora-agentnet"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "Official Python SDK for AgentNet -- AI engineering work as a service."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "AgentNet", email = "sdk@authora.dev" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
keywords = ["agentnet", "authora", "ai", "code-review", "automation", "sdk"]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["agentnet"]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
async = ["aiohttp>=3.9"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://net.authora.dev"
|
|
37
|
+
Repository = "https://github.com/authora-dev/agentnet-python"
|
|
38
|
+
Issues = "https://github.com/authora-dev/agentnet-python/issues"
|