axon-protocol 0.1.0__py3-none-any.whl
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.
- axon/__init__.py +51 -0
- axon/_base.py +142 -0
- axon/client.py +132 -0
- axon/coordination.py +161 -0
- axon/events.py +116 -0
- axon/exceptions.py +51 -0
- axon/integrations/__init__.py +15 -0
- axon/integrations/crewai.py +232 -0
- axon/integrations/langchain.py +233 -0
- axon/memory.py +150 -0
- axon/messages.py +84 -0
- axon/receipts.py +193 -0
- axon/types.py +97 -0
- axon_protocol-0.1.0.dist-info/METADATA +158 -0
- axon_protocol-0.1.0.dist-info/RECORD +18 -0
- axon_protocol-0.1.0.dist-info/WHEEL +5 -0
- axon_protocol-0.1.0.dist-info/licenses/LICENSE +21 -0
- axon_protocol-0.1.0.dist-info/top_level.txt +1 -0
axon/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from axon.client import AxonClient, AxonSyncClient
|
|
2
|
+
from axon.messages import MessagesClient, SyncMessagesClient
|
|
3
|
+
from axon.types import (
|
|
4
|
+
MemoryResult,
|
|
5
|
+
MemorySearchResponse,
|
|
6
|
+
StoredMemory,
|
|
7
|
+
LockInfo,
|
|
8
|
+
LockStatus,
|
|
9
|
+
ReasoningStep,
|
|
10
|
+
StepsLogger,
|
|
11
|
+
ReceiptInfo,
|
|
12
|
+
ReceiptVerifyResult,
|
|
13
|
+
)
|
|
14
|
+
from axon.exceptions import (
|
|
15
|
+
AxonError,
|
|
16
|
+
AuthError,
|
|
17
|
+
LockConflictError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
AxonPermissionError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
AxonConnectionError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AxonClient",
|
|
29
|
+
"AxonSyncClient",
|
|
30
|
+
"MessagesClient",
|
|
31
|
+
"SyncMessagesClient",
|
|
32
|
+
# Types
|
|
33
|
+
"MemoryResult",
|
|
34
|
+
"MemorySearchResponse",
|
|
35
|
+
"StoredMemory",
|
|
36
|
+
"LockInfo",
|
|
37
|
+
"LockStatus",
|
|
38
|
+
"ReasoningStep",
|
|
39
|
+
"StepsLogger",
|
|
40
|
+
"ReceiptInfo",
|
|
41
|
+
"ReceiptVerifyResult",
|
|
42
|
+
# Exceptions
|
|
43
|
+
"AxonError",
|
|
44
|
+
"AuthError",
|
|
45
|
+
"LockConflictError",
|
|
46
|
+
"NotFoundError",
|
|
47
|
+
"AxonPermissionError",
|
|
48
|
+
"RateLimitError",
|
|
49
|
+
"ServerError",
|
|
50
|
+
"AxonConnectionError",
|
|
51
|
+
]
|
axon/_base.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import time
|
|
3
|
+
import asyncio
|
|
4
|
+
from axon.exceptions import (
|
|
5
|
+
AuthError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
AxonPermissionError,
|
|
8
|
+
LockConflictError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
ServerError,
|
|
11
|
+
AxonConnectionError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _BaseClient:
|
|
16
|
+
def __init__(self, http: httpx.AsyncClient, base_url: str):
|
|
17
|
+
self._http = http
|
|
18
|
+
self._base_url = base_url.rstrip("/")
|
|
19
|
+
|
|
20
|
+
async def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
21
|
+
url = f"{self._base_url}{path}"
|
|
22
|
+
retries = 3
|
|
23
|
+
backoff = 0.5
|
|
24
|
+
|
|
25
|
+
for attempt in range(retries):
|
|
26
|
+
try:
|
|
27
|
+
response = await self._http.request(method, url, **kwargs)
|
|
28
|
+
if response.status_code >= 500 and attempt < retries - 1:
|
|
29
|
+
await asyncio.sleep(backoff * (2 ** attempt))
|
|
30
|
+
continue
|
|
31
|
+
break
|
|
32
|
+
except httpx.ConnectError:
|
|
33
|
+
if attempt == retries - 1:
|
|
34
|
+
raise AxonConnectionError(
|
|
35
|
+
f"Cannot connect to Axon server at {self._base_url}. "
|
|
36
|
+
f"Make sure the server is running."
|
|
37
|
+
)
|
|
38
|
+
await asyncio.sleep(backoff * (2 ** attempt))
|
|
39
|
+
except httpx.TimeoutException:
|
|
40
|
+
if attempt == retries - 1:
|
|
41
|
+
raise AxonConnectionError(
|
|
42
|
+
f"Request to {url} timed out. Server may be overloaded."
|
|
43
|
+
)
|
|
44
|
+
await asyncio.sleep(backoff * (2 ** attempt))
|
|
45
|
+
|
|
46
|
+
# Success
|
|
47
|
+
if response.status_code == 200:
|
|
48
|
+
return response.json()
|
|
49
|
+
|
|
50
|
+
# Parse error detail from response body
|
|
51
|
+
try:
|
|
52
|
+
detail = response.json().get("detail", response.text)
|
|
53
|
+
except Exception:
|
|
54
|
+
detail = response.text
|
|
55
|
+
|
|
56
|
+
# Map status codes to specific exceptions
|
|
57
|
+
if response.status_code == 401:
|
|
58
|
+
raise AuthError(f"Authentication failed: {detail}", 401)
|
|
59
|
+
elif response.status_code == 403:
|
|
60
|
+
raise AxonPermissionError(f"Permission denied: {detail}", 403)
|
|
61
|
+
elif response.status_code == 404:
|
|
62
|
+
raise NotFoundError(f"Not found: {detail}", 404)
|
|
63
|
+
elif response.status_code == 409:
|
|
64
|
+
raise LockConflictError("", detail=detail)
|
|
65
|
+
elif response.status_code == 429:
|
|
66
|
+
retry_after = int(response.headers.get("Retry-After", 60))
|
|
67
|
+
raise RateLimitError(retry_after)
|
|
68
|
+
elif response.status_code >= 500:
|
|
69
|
+
raise ServerError(
|
|
70
|
+
f"Server error ({response.status_code}): {detail}",
|
|
71
|
+
response.status_code,
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
raise ServerError(
|
|
75
|
+
f"Unexpected status {response.status_code}: {detail}",
|
|
76
|
+
response.status_code,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _BaseSyncClient:
|
|
81
|
+
def __init__(self, http: httpx.Client, base_url: str):
|
|
82
|
+
self._http = http
|
|
83
|
+
self._base_url = base_url.rstrip("/")
|
|
84
|
+
|
|
85
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
86
|
+
url = f"{self._base_url}{path}"
|
|
87
|
+
retries = 3
|
|
88
|
+
backoff = 0.5
|
|
89
|
+
|
|
90
|
+
for attempt in range(retries):
|
|
91
|
+
try:
|
|
92
|
+
response = self._http.request(method, url, **kwargs)
|
|
93
|
+
if response.status_code >= 500 and attempt < retries - 1:
|
|
94
|
+
time.sleep(backoff * (2 ** attempt))
|
|
95
|
+
continue
|
|
96
|
+
break
|
|
97
|
+
except httpx.ConnectError:
|
|
98
|
+
if attempt == retries - 1:
|
|
99
|
+
raise AxonConnectionError(
|
|
100
|
+
f"Cannot connect to Axon server at {self._base_url}. "
|
|
101
|
+
f"Make sure the server is running."
|
|
102
|
+
)
|
|
103
|
+
time.sleep(backoff * (2 ** attempt))
|
|
104
|
+
except httpx.TimeoutException:
|
|
105
|
+
if attempt == retries - 1:
|
|
106
|
+
raise AxonConnectionError(
|
|
107
|
+
f"Request to {url} timed out. Server may be overloaded."
|
|
108
|
+
)
|
|
109
|
+
time.sleep(backoff * (2 ** attempt))
|
|
110
|
+
|
|
111
|
+
# Success
|
|
112
|
+
if response.status_code == 200:
|
|
113
|
+
return response.json()
|
|
114
|
+
|
|
115
|
+
# Parse error detail from response body
|
|
116
|
+
try:
|
|
117
|
+
detail = response.json().get("detail", response.text)
|
|
118
|
+
except Exception:
|
|
119
|
+
detail = response.text
|
|
120
|
+
|
|
121
|
+
# Map status codes to specific exceptions
|
|
122
|
+
if response.status_code == 401:
|
|
123
|
+
raise AuthError(f"Authentication failed: {detail}", 401)
|
|
124
|
+
elif response.status_code == 403:
|
|
125
|
+
raise AxonPermissionError(f"Permission denied: {detail}", 403)
|
|
126
|
+
elif response.status_code == 404:
|
|
127
|
+
raise NotFoundError(f"Not found: {detail}", 404)
|
|
128
|
+
elif response.status_code == 409:
|
|
129
|
+
raise LockConflictError("", detail=detail)
|
|
130
|
+
elif response.status_code == 429:
|
|
131
|
+
retry_after = int(response.headers.get("Retry-After", 60))
|
|
132
|
+
raise RateLimitError(retry_after)
|
|
133
|
+
elif response.status_code >= 500:
|
|
134
|
+
raise ServerError(
|
|
135
|
+
f"Server error ({response.status_code}): {detail}",
|
|
136
|
+
response.status_code,
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
raise ServerError(
|
|
140
|
+
f"Unexpected status {response.status_code}: {detail}",
|
|
141
|
+
response.status_code,
|
|
142
|
+
)
|
axon/client.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from axon.memory import MemoryClient, SyncMemoryClient
|
|
3
|
+
from axon.coordination import CoordinationClient, SyncCoordinationClient
|
|
4
|
+
from axon.receipts import ReceiptsClient, SyncReceiptsClient
|
|
5
|
+
from axon.events import EventsClient, SyncEventsClient
|
|
6
|
+
from axon.messages import MessagesClient, SyncMessagesClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AxonClient:
|
|
10
|
+
"""
|
|
11
|
+
Async entry point for the Axon Protocol Python SDK.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
project_id: str,
|
|
18
|
+
agent_token: str = None,
|
|
19
|
+
agent_id: str = None,
|
|
20
|
+
base_url: str = "http://localhost:8000",
|
|
21
|
+
timeout: float = 30.0,
|
|
22
|
+
):
|
|
23
|
+
self.api_key = api_key
|
|
24
|
+
self.project_id = project_id
|
|
25
|
+
self.agent_token = agent_token
|
|
26
|
+
self.agent_id = agent_id
|
|
27
|
+
self.base_url = base_url.rstrip("/")
|
|
28
|
+
|
|
29
|
+
headers = {}
|
|
30
|
+
if api_key:
|
|
31
|
+
headers["X-API-Key"] = api_key
|
|
32
|
+
if agent_token:
|
|
33
|
+
headers["Authorization"] = f"Bearer {agent_token}"
|
|
34
|
+
|
|
35
|
+
self._http = httpx.AsyncClient(
|
|
36
|
+
headers=headers,
|
|
37
|
+
timeout=httpx.Timeout(timeout),
|
|
38
|
+
limits=httpx.Limits(
|
|
39
|
+
max_connections=20,
|
|
40
|
+
max_keepalive_connections=10,
|
|
41
|
+
keepalive_expiry=30,
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.memory = MemoryClient(self._http, self.base_url)
|
|
46
|
+
self.lock = CoordinationClient(self._http, self.base_url)
|
|
47
|
+
self.receipts = ReceiptsClient(self._http, self.base_url)
|
|
48
|
+
self.events = EventsClient(self.base_url, api_key, project_id)
|
|
49
|
+
self.messages = MessagesClient(self._http, self.base_url)
|
|
50
|
+
|
|
51
|
+
async def ping(self) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Check if the Axon server is reachable (async).
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
response = await self._http.get(f"{self.base_url}/v1/health")
|
|
57
|
+
return response.status_code == 200
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
async def close(self):
|
|
62
|
+
"""Close the underlying HTTP connection pool."""
|
|
63
|
+
await self._http.aclose()
|
|
64
|
+
|
|
65
|
+
async def __aenter__(self):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
69
|
+
await self.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AxonSyncClient:
|
|
73
|
+
"""
|
|
74
|
+
Sync entry point for the Axon Protocol Python SDK.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
api_key: str,
|
|
80
|
+
project_id: str,
|
|
81
|
+
agent_token: str = None,
|
|
82
|
+
agent_id: str = None,
|
|
83
|
+
base_url: str = "http://localhost:8000",
|
|
84
|
+
timeout: float = 30.0,
|
|
85
|
+
):
|
|
86
|
+
self.api_key = api_key
|
|
87
|
+
self.project_id = project_id
|
|
88
|
+
self.agent_token = agent_token
|
|
89
|
+
self.agent_id = agent_id
|
|
90
|
+
self.base_url = base_url.rstrip("/")
|
|
91
|
+
|
|
92
|
+
headers = {}
|
|
93
|
+
if api_key:
|
|
94
|
+
headers["X-API-Key"] = api_key
|
|
95
|
+
if agent_token:
|
|
96
|
+
headers["Authorization"] = f"Bearer {agent_token}"
|
|
97
|
+
|
|
98
|
+
self._http = httpx.Client(
|
|
99
|
+
headers=headers,
|
|
100
|
+
timeout=httpx.Timeout(timeout),
|
|
101
|
+
limits=httpx.Limits(
|
|
102
|
+
max_connections=20,
|
|
103
|
+
max_keepalive_connections=10,
|
|
104
|
+
keepalive_expiry=30,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.memory = SyncMemoryClient(self._http, self.base_url)
|
|
109
|
+
self.lock = SyncCoordinationClient(self._http, self.base_url)
|
|
110
|
+
self.receipts = SyncReceiptsClient(self._http, self.base_url)
|
|
111
|
+
self.events = SyncEventsClient(self.base_url, api_key, project_id)
|
|
112
|
+
self.messages = SyncMessagesClient(self._http, self.base_url)
|
|
113
|
+
|
|
114
|
+
def ping(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if the Axon server is reachable (sync).
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
response = self._http.get(f"{self.base_url}/v1/health")
|
|
120
|
+
return response.status_code == 200
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def close(self):
|
|
125
|
+
"""Close the underlying HTTP connection pool."""
|
|
126
|
+
self._http.close()
|
|
127
|
+
|
|
128
|
+
def __enter__(self):
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
132
|
+
self.close()
|
axon/coordination.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
2
|
+
from axon._base import _BaseClient, _BaseSyncClient
|
|
3
|
+
from axon.types import LockInfo, LockStatus
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CoordinationClient(_BaseClient):
|
|
7
|
+
|
|
8
|
+
async def acquire(
|
|
9
|
+
self,
|
|
10
|
+
resource_id: str,
|
|
11
|
+
timeout: int = 300,
|
|
12
|
+
metadata: dict = None,
|
|
13
|
+
) -> LockInfo:
|
|
14
|
+
"""
|
|
15
|
+
Acquire an exclusive lock on a resource (async).
|
|
16
|
+
"""
|
|
17
|
+
body = {"resource_id": resource_id, "timeout": timeout}
|
|
18
|
+
if metadata is not None:
|
|
19
|
+
body["metadata"] = metadata
|
|
20
|
+
|
|
21
|
+
data = await self._request("POST", "/v1/lock/acquire", json=body)
|
|
22
|
+
return LockInfo(
|
|
23
|
+
lock_id=data["lock_id"],
|
|
24
|
+
resource_id=data["resource_id"],
|
|
25
|
+
expires_at=data["expires_at"],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def release(self, resource_id: str) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Release a lock that you hold (async).
|
|
31
|
+
"""
|
|
32
|
+
data = await self._request(
|
|
33
|
+
"POST",
|
|
34
|
+
"/v1/lock/release",
|
|
35
|
+
params={"resource_id": resource_id},
|
|
36
|
+
)
|
|
37
|
+
return data.get("released", False)
|
|
38
|
+
|
|
39
|
+
async def status(self, resource_id: str) -> LockStatus:
|
|
40
|
+
"""
|
|
41
|
+
Check lock status of a resource (async).
|
|
42
|
+
"""
|
|
43
|
+
data = await self._request("GET", f"/v1/lock/status/{resource_id}")
|
|
44
|
+
return LockStatus(
|
|
45
|
+
locked=data["locked"],
|
|
46
|
+
resource_id=data["resource_id"],
|
|
47
|
+
holder_agent_id=data.get("holder_agent_id"),
|
|
48
|
+
locked_at=data.get("locked_at"),
|
|
49
|
+
expires_at=data.get("expires_at"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def list_active(self) -> list[dict]:
|
|
53
|
+
"""
|
|
54
|
+
List all active locks in the project (async).
|
|
55
|
+
"""
|
|
56
|
+
data = await self._request("GET", "/v1/lock/list")
|
|
57
|
+
return data.get("locks", [])
|
|
58
|
+
|
|
59
|
+
@asynccontextmanager
|
|
60
|
+
async def hold(
|
|
61
|
+
self,
|
|
62
|
+
resource_id: str,
|
|
63
|
+
timeout: int = 300,
|
|
64
|
+
metadata: dict = None,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Context manager that acquires a lock and automatically releases it on exit (async).
|
|
68
|
+
"""
|
|
69
|
+
lock_info = await self.acquire(resource_id, timeout=timeout, metadata=metadata)
|
|
70
|
+
try:
|
|
71
|
+
yield lock_info
|
|
72
|
+
finally:
|
|
73
|
+
try:
|
|
74
|
+
await self.release(resource_id)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def __call__(self, resource_id: str, timeout: int = 300, metadata: dict = None):
|
|
79
|
+
"""
|
|
80
|
+
Redirect axon.lock("resource_id") to hold(resource_id)
|
|
81
|
+
"""
|
|
82
|
+
return self.hold(resource_id, timeout=timeout, metadata=metadata)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SyncCoordinationClient(_BaseSyncClient):
|
|
86
|
+
|
|
87
|
+
def acquire(
|
|
88
|
+
self,
|
|
89
|
+
resource_id: str,
|
|
90
|
+
timeout: int = 300,
|
|
91
|
+
metadata: dict = None,
|
|
92
|
+
) -> LockInfo:
|
|
93
|
+
"""
|
|
94
|
+
Acquire an exclusive lock on a resource (sync).
|
|
95
|
+
"""
|
|
96
|
+
body = {"resource_id": resource_id, "timeout": timeout}
|
|
97
|
+
if metadata is not None:
|
|
98
|
+
body["metadata"] = metadata
|
|
99
|
+
|
|
100
|
+
data = self._request("POST", "/v1/lock/acquire", json=body)
|
|
101
|
+
return LockInfo(
|
|
102
|
+
lock_id=data["lock_id"],
|
|
103
|
+
resource_id=data["resource_id"],
|
|
104
|
+
expires_at=data["expires_at"],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def release(self, resource_id: str) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Release a lock that you hold (sync).
|
|
110
|
+
"""
|
|
111
|
+
data = self._request(
|
|
112
|
+
"POST",
|
|
113
|
+
"/v1/lock/release",
|
|
114
|
+
params={"resource_id": resource_id},
|
|
115
|
+
)
|
|
116
|
+
return data.get("released", False)
|
|
117
|
+
|
|
118
|
+
def status(self, resource_id: str) -> LockStatus:
|
|
119
|
+
"""
|
|
120
|
+
Check lock status of a resource (sync).
|
|
121
|
+
"""
|
|
122
|
+
data = self._request("GET", f"/v1/lock/status/{resource_id}")
|
|
123
|
+
return LockStatus(
|
|
124
|
+
locked=data["locked"],
|
|
125
|
+
resource_id=data["resource_id"],
|
|
126
|
+
holder_agent_id=data.get("holder_agent_id"),
|
|
127
|
+
locked_at=data.get("locked_at"),
|
|
128
|
+
expires_at=data.get("expires_at"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def list_active(self) -> list[dict]:
|
|
132
|
+
"""
|
|
133
|
+
List all active locks in the project (sync).
|
|
134
|
+
"""
|
|
135
|
+
data = self._request("GET", "/v1/lock/list")
|
|
136
|
+
return data.get("locks", [])
|
|
137
|
+
|
|
138
|
+
@contextmanager
|
|
139
|
+
def hold(
|
|
140
|
+
self,
|
|
141
|
+
resource_id: str,
|
|
142
|
+
timeout: int = 300,
|
|
143
|
+
metadata: dict = None,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Context manager that acquires a lock and automatically releases it on exit (sync).
|
|
147
|
+
"""
|
|
148
|
+
lock_info = self.acquire(resource_id, timeout=timeout, metadata=metadata)
|
|
149
|
+
try:
|
|
150
|
+
yield lock_info
|
|
151
|
+
finally:
|
|
152
|
+
try:
|
|
153
|
+
self.release(resource_id)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
def __call__(self, resource_id: str, timeout: int = 300, metadata: dict = None):
|
|
158
|
+
"""
|
|
159
|
+
Redirect axon.lock("resource_id") to hold(resource_id)
|
|
160
|
+
"""
|
|
161
|
+
return self.hold(resource_id, timeout=timeout, metadata=metadata)
|
axon/events.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import AsyncIterator, Iterator
|
|
5
|
+
import websockets
|
|
6
|
+
from websockets.sync.client import connect as ws_sync_connect
|
|
7
|
+
from axon.exceptions import AxonConnectionError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventsClient:
|
|
11
|
+
"""
|
|
12
|
+
Connects to the Axon server via WebSocket and streams real-time events (async).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, base_url: str, api_key: str, project_id: str):
|
|
16
|
+
ws_base = base_url.replace("https://", "wss://").replace("http://", "ws://")
|
|
17
|
+
self._ws_url = ws_base.rstrip("/")
|
|
18
|
+
self._api_key = api_key
|
|
19
|
+
self._project_id = project_id
|
|
20
|
+
|
|
21
|
+
async def listen(self, reconnect: bool = True) -> AsyncIterator[dict]:
|
|
22
|
+
url = f"{self._ws_url}/v1/events/{self._project_id}"
|
|
23
|
+
|
|
24
|
+
while True:
|
|
25
|
+
try:
|
|
26
|
+
async with websockets.connect(
|
|
27
|
+
url,
|
|
28
|
+
additional_headers={"X-API-Key": self._api_key},
|
|
29
|
+
ping_interval=30,
|
|
30
|
+
ping_timeout=10,
|
|
31
|
+
) as ws:
|
|
32
|
+
async for raw_message in ws:
|
|
33
|
+
try:
|
|
34
|
+
event = json.loads(raw_message)
|
|
35
|
+
|
|
36
|
+
# Skip heartbeat pings
|
|
37
|
+
if event.get("type") == "ping":
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
yield event
|
|
41
|
+
|
|
42
|
+
except json.JSONDecodeError:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
except websockets.exceptions.ConnectionClosed:
|
|
46
|
+
if not reconnect:
|
|
47
|
+
return
|
|
48
|
+
await asyncio.sleep(2)
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
if not reconnect:
|
|
52
|
+
raise AxonConnectionError(
|
|
53
|
+
f"WebSocket connection failed: {str(e)}"
|
|
54
|
+
)
|
|
55
|
+
await asyncio.sleep(2)
|
|
56
|
+
|
|
57
|
+
async def listen_once(self, timeout: float = 10.0) -> dict | None:
|
|
58
|
+
try:
|
|
59
|
+
async with asyncio.timeout(timeout):
|
|
60
|
+
async for event in self.listen(reconnect=False):
|
|
61
|
+
return event
|
|
62
|
+
except (asyncio.TimeoutError, StopAsyncIteration):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SyncEventsClient:
|
|
67
|
+
"""
|
|
68
|
+
Connects to the Axon server via WebSocket and streams real-time events (sync).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, base_url: str, api_key: str, project_id: str):
|
|
72
|
+
ws_base = base_url.replace("https://", "wss://").replace("http://", "ws://")
|
|
73
|
+
self._ws_url = ws_base.rstrip("/")
|
|
74
|
+
self._api_key = api_key
|
|
75
|
+
self._project_id = project_id
|
|
76
|
+
|
|
77
|
+
def listen(self, reconnect: bool = True) -> Iterator[dict]:
|
|
78
|
+
url = f"{self._ws_url}/v1/events/{self._project_id}"
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
try:
|
|
82
|
+
with ws_sync_connect(
|
|
83
|
+
url,
|
|
84
|
+
additional_headers={"X-API-Key": self._api_key},
|
|
85
|
+
) as ws:
|
|
86
|
+
for raw_message in ws:
|
|
87
|
+
try:
|
|
88
|
+
event = json.loads(raw_message)
|
|
89
|
+
|
|
90
|
+
# Skip heartbeat pings
|
|
91
|
+
if event.get("type") == "ping":
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
yield event
|
|
95
|
+
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
if not reconnect:
|
|
101
|
+
raise AxonConnectionError(
|
|
102
|
+
f"WebSocket connection failed: {str(e)}"
|
|
103
|
+
)
|
|
104
|
+
time.sleep(2)
|
|
105
|
+
|
|
106
|
+
def listen_once(self, timeout: float = 10.0) -> dict | None:
|
|
107
|
+
iterator = self.listen(reconnect=False)
|
|
108
|
+
start_time = time.time()
|
|
109
|
+
while time.time() - start_time < timeout:
|
|
110
|
+
try:
|
|
111
|
+
return next(iterator)
|
|
112
|
+
except StopIteration:
|
|
113
|
+
return None
|
|
114
|
+
except Exception:
|
|
115
|
+
raise
|
|
116
|
+
return None
|
axon/exceptions.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class AxonError(Exception):
|
|
2
|
+
"""Base exception for all Axon errors"""
|
|
3
|
+
def __init__(self, message: str, status_code: int = None):
|
|
4
|
+
super().__init__(message)
|
|
5
|
+
self.message = message
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
|
|
8
|
+
def __repr__(self):
|
|
9
|
+
return f"{self.__class__.__name__}(message={self.message!r}, status_code={self.status_code})"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthError(AxonError):
|
|
13
|
+
"""Raised when authentication fails. Wrong or missing API key or token."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LockConflictError(AxonError):
|
|
18
|
+
"""Raised when you try to acquire a lock that another agent already holds."""
|
|
19
|
+
def __init__(self, resource_id: str = "", detail: str = None):
|
|
20
|
+
msg = detail or f"Resource '{resource_id}' is already locked by another agent"
|
|
21
|
+
super().__init__(msg, 409)
|
|
22
|
+
self.resource_id = resource_id
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotFoundError(AxonError):
|
|
26
|
+
"""Raised when the requested resource does not exist."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AxonPermissionError(AxonError):
|
|
31
|
+
"""Raised when the agent does not have permission to perform the action."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RateLimitError(AxonError):
|
|
36
|
+
"""Raised when the rate limit is exceeded."""
|
|
37
|
+
def __init__(self, retry_after: int = 60):
|
|
38
|
+
super().__init__(
|
|
39
|
+
f"Rate limit exceeded. Retry after {retry_after} seconds.", 429
|
|
40
|
+
)
|
|
41
|
+
self.retry_after = retry_after
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServerError(AxonError):
|
|
45
|
+
"""Raised when the Axon server returns a 5xx error."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AxonConnectionError(AxonError):
|
|
50
|
+
"""Raised when the SDK cannot connect to the Axon server at all."""
|
|
51
|
+
pass
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from axon.integrations.langchain import AxonMemoryTool, AxonLockTool, AxonReceiptCallbackHandler
|
|
2
|
+
from axon.integrations.crewai import (
|
|
3
|
+
AxonMemoryTool as CrewAxonMemoryTool,
|
|
4
|
+
AxonLockTool as CrewAxonLockTool,
|
|
5
|
+
AxonReceiptCallbackHandler as CrewAxonReceiptCallbackHandler,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AxonMemoryTool",
|
|
10
|
+
"AxonLockTool",
|
|
11
|
+
"AxonReceiptCallbackHandler",
|
|
12
|
+
"CrewAxonMemoryTool",
|
|
13
|
+
"CrewAxonLockTool",
|
|
14
|
+
"CrewAxonReceiptCallbackHandler",
|
|
15
|
+
]
|