agentrx-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.
- agentrx_sdk-0.1.0/PKG-INFO +57 -0
- agentrx_sdk-0.1.0/README.md +30 -0
- agentrx_sdk-0.1.0/pyproject.toml +49 -0
- agentrx_sdk-0.1.0/setup.cfg +4 -0
- agentrx_sdk-0.1.0/src/agentrx/__init__.py +28 -0
- agentrx_sdk-0.1.0/src/agentrx/client.py +217 -0
- agentrx_sdk-0.1.0/src/agentrx/decorator.py +259 -0
- agentrx_sdk-0.1.0/src/agentrx/models.py +136 -0
- agentrx_sdk-0.1.0/src/agentrx_sdk.egg-info/PKG-INFO +57 -0
- agentrx_sdk-0.1.0/src/agentrx_sdk.egg-info/SOURCES.txt +12 -0
- agentrx_sdk-0.1.0/src/agentrx_sdk.egg-info/dependency_links.txt +1 -0
- agentrx_sdk-0.1.0/src/agentrx_sdk.egg-info/requires.txt +7 -0
- agentrx_sdk-0.1.0/src/agentrx_sdk.egg-info/top_level.txt +1 -0
- agentrx_sdk-0.1.0/tests/test_sdk.py +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentrx-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in recovery and resilience layer for AI agents
|
|
5
|
+
Author: Chain Assets LLC
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://chainassetslab.com
|
|
8
|
+
Project-URL: Repository, https://github.com/chainassetslab/agentrx-python
|
|
9
|
+
Keywords: ai,agents,llm,recovery,mcp,langchain,crewai
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
26
|
+
Requires-Dist: respx>=0.21.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# agentrx-python
|
|
29
|
+
|
|
30
|
+
Make your AI agents bulletproof in two lines.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
pip install agentrx-sdk
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
from agentrx import with_recovery
|
|
39
|
+
|
|
40
|
+
@with_recovery(api_key="your_key", agent_id="my_agent")
|
|
41
|
+
async def call_my_tool(payload: dict) -> dict:
|
|
42
|
+
return await some_api.call(payload)
|
|
43
|
+
|
|
44
|
+
When call_my_tool raises an exception, AgentRx diagnoses it and
|
|
45
|
+
automatically retries, corrects the payload, or tells you exactly
|
|
46
|
+
what went wrong.
|
|
47
|
+
|
|
48
|
+
## Environment Variables
|
|
49
|
+
|
|
50
|
+
AGENTRX_API_KEY — Your API key (required)
|
|
51
|
+
AGENTRX_BASE_URL — AgentRx server URL (default: http://localhost:8000)
|
|
52
|
+
OTEL_TRACE_ID — OpenTelemetry trace ID (optional)
|
|
53
|
+
LANGSMITH_RUN_ID — LangSmith run ID (optional)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# agentrx-python
|
|
2
|
+
|
|
3
|
+
Make your AI agents bulletproof in two lines.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
pip install agentrx-sdk
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
from agentrx import with_recovery
|
|
12
|
+
|
|
13
|
+
@with_recovery(api_key="your_key", agent_id="my_agent")
|
|
14
|
+
async def call_my_tool(payload: dict) -> dict:
|
|
15
|
+
return await some_api.call(payload)
|
|
16
|
+
|
|
17
|
+
When call_my_tool raises an exception, AgentRx diagnoses it and
|
|
18
|
+
automatically retries, corrects the payload, or tells you exactly
|
|
19
|
+
what went wrong.
|
|
20
|
+
|
|
21
|
+
## Environment Variables
|
|
22
|
+
|
|
23
|
+
AGENTRX_API_KEY — Your API key (required)
|
|
24
|
+
AGENTRX_BASE_URL — AgentRx server URL (default: http://localhost:8000)
|
|
25
|
+
OTEL_TRACE_ID — OpenTelemetry trace ID (optional)
|
|
26
|
+
LANGSMITH_RUN_ID — LangSmith run ID (optional)
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentrx-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Drop-in recovery and resilience layer for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Chain Assets LLC" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "agents", "llm", "recovery", "mcp", "langchain", "crewai"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"pydantic>=2.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0.0",
|
|
36
|
+
"pytest-asyncio>=0.23.0",
|
|
37
|
+
"respx>=0.21.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://chainassetslab.com"
|
|
42
|
+
Repository = "https://github.com/chainassetslab/agentrx-python"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
asyncio_mode = "auto"
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentrx — Python SDK for the AgentRx Metacognitive Recovery API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import AgentRxClient
|
|
6
|
+
from .decorator import with_recovery
|
|
7
|
+
from .models import (
|
|
8
|
+
ActionType,
|
|
9
|
+
AgentRxError,
|
|
10
|
+
FailureSignature,
|
|
11
|
+
HumanHandoffRequired,
|
|
12
|
+
PreflightResult,
|
|
13
|
+
RecoveryAction,
|
|
14
|
+
RecoveryException,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"with_recovery",
|
|
20
|
+
"AgentRxClient",
|
|
21
|
+
"HumanHandoffRequired",
|
|
22
|
+
"RecoveryException",
|
|
23
|
+
"AgentRxError",
|
|
24
|
+
"RecoveryAction",
|
|
25
|
+
"PreflightResult",
|
|
26
|
+
"ActionType",
|
|
27
|
+
"FailureSignature",
|
|
28
|
+
]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentrx.client
|
|
3
|
+
==============
|
|
4
|
+
Async HTTP client for the AgentRx API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from .models import (
|
|
18
|
+
AgentRxError,
|
|
19
|
+
PreflightResult,
|
|
20
|
+
RecoveryAction,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_DEFAULT_BASE_URL = "http://localhost:8000"
|
|
24
|
+
|
|
25
|
+
# Global schema cache — persists across client instances for the
|
|
26
|
+
# lifetime of the Python process. Prevents re-uploading the same
|
|
27
|
+
# schema on every tool failure when the decorator creates a new
|
|
28
|
+
# client instance each call.
|
|
29
|
+
_GLOBAL_SCHEMA_CACHE: Dict[str, bool] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentRxClient:
|
|
33
|
+
"""
|
|
34
|
+
Async HTTP client for the AgentRx API.
|
|
35
|
+
|
|
36
|
+
Usage as async context manager (recommended):
|
|
37
|
+
async with AgentRxClient(api_key="...") as rx:
|
|
38
|
+
action = await rx.diagnose(...)
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
api_key: Your AgentRx API key. Falls back to AGENTRX_API_KEY env var.
|
|
42
|
+
base_url: AgentRx API base URL. Falls back to AGENTRX_BASE_URL env var.
|
|
43
|
+
timeout: HTTP timeout in seconds. Default 10.
|
|
44
|
+
trace_id: Optional trace ID to propagate to AgentRx logs.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
api_key: Optional[str] = None,
|
|
50
|
+
base_url: Optional[str] = None,
|
|
51
|
+
timeout: float = 10.0,
|
|
52
|
+
trace_id: Optional[str] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._api_key = api_key or os.environ.get("AGENTRX_API_KEY", "")
|
|
55
|
+
self._base_url = (base_url or os.environ.get("AGENTRX_BASE_URL", _DEFAULT_BASE_URL)).rstrip("/")
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._trace_id = trace_id
|
|
58
|
+
self._schema_cache = _GLOBAL_SCHEMA_CACHE
|
|
59
|
+
self._http: Optional[httpx.AsyncClient] = None
|
|
60
|
+
|
|
61
|
+
async def open(self) -> None:
|
|
62
|
+
if not self._api_key:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"No API key provided. Pass api_key= or set AGENTRX_API_KEY env var."
|
|
65
|
+
)
|
|
66
|
+
self._http = httpx.AsyncClient(
|
|
67
|
+
base_url=self._base_url,
|
|
68
|
+
timeout=self._timeout,
|
|
69
|
+
headers=self._build_headers(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def close(self) -> None:
|
|
73
|
+
if self._http:
|
|
74
|
+
await self._http.aclose()
|
|
75
|
+
self._http = None
|
|
76
|
+
|
|
77
|
+
async def __aenter__(self) -> "AgentRxClient":
|
|
78
|
+
await self.open()
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
82
|
+
await self.close()
|
|
83
|
+
|
|
84
|
+
async def diagnose(
|
|
85
|
+
self,
|
|
86
|
+
agent_id: str,
|
|
87
|
+
goal: str,
|
|
88
|
+
tool_name: str,
|
|
89
|
+
payload: Dict[str, Any],
|
|
90
|
+
error_response: Dict[str, Any],
|
|
91
|
+
latency_ms: int = 0,
|
|
92
|
+
execution_history: List[Dict[str, Any]] = None,
|
|
93
|
+
active_plan: List[str] = None,
|
|
94
|
+
tool_schema: Optional[Dict[str, Any]] = None,
|
|
95
|
+
) -> RecoveryAction:
|
|
96
|
+
self._ensure_open()
|
|
97
|
+
|
|
98
|
+
schema_hash = None
|
|
99
|
+
if tool_schema is not None:
|
|
100
|
+
schema_hash = await self._ensure_schema_cached(tool_schema)
|
|
101
|
+
|
|
102
|
+
body = {
|
|
103
|
+
"state": {
|
|
104
|
+
"agent_id": agent_id,
|
|
105
|
+
"goal": goal,
|
|
106
|
+
"active_plan": active_plan or [],
|
|
107
|
+
"execution_history": execution_history or [],
|
|
108
|
+
},
|
|
109
|
+
"failure": {
|
|
110
|
+
"mcp_tool_name": tool_name,
|
|
111
|
+
"attempted_payload": payload,
|
|
112
|
+
"error_response": error_response,
|
|
113
|
+
"latency_ms": latency_ms,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
if schema_hash:
|
|
117
|
+
body["schema_hash"] = schema_hash
|
|
118
|
+
|
|
119
|
+
response = await self._post("/v1/diagnose_and_recover", body)
|
|
120
|
+
return RecoveryAction.from_dict(response)
|
|
121
|
+
|
|
122
|
+
async def preflight(
|
|
123
|
+
self,
|
|
124
|
+
agent_id: str,
|
|
125
|
+
tool_name: str,
|
|
126
|
+
payload: Dict[str, Any],
|
|
127
|
+
tool_schema: Dict[str, Any],
|
|
128
|
+
) -> PreflightResult:
|
|
129
|
+
self._ensure_open()
|
|
130
|
+
|
|
131
|
+
schema_hash = await self._ensure_schema_cached(tool_schema)
|
|
132
|
+
|
|
133
|
+
body = {
|
|
134
|
+
"agent_id": agent_id,
|
|
135
|
+
"mcp_tool_name": tool_name,
|
|
136
|
+
"intended_payload": payload,
|
|
137
|
+
"schema_hash": schema_hash,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
response = await self._post("/v1/preflight", body)
|
|
141
|
+
return PreflightResult.from_dict(response)
|
|
142
|
+
|
|
143
|
+
async def register_schema(self, tool_schema: Dict[str, Any]) -> str:
|
|
144
|
+
self._ensure_open()
|
|
145
|
+
response = await self._post("/v1/schema/register", {"tool_schema": tool_schema})
|
|
146
|
+
schema_hash = response["schema_hash"]
|
|
147
|
+
_GLOBAL_SCHEMA_CACHE[schema_hash] = True
|
|
148
|
+
return schema_hash
|
|
149
|
+
|
|
150
|
+
async def clear_agent_state(self, agent_id: str) -> Dict[str, Any]:
|
|
151
|
+
self._ensure_open()
|
|
152
|
+
resp = await self._http.delete(f"/v1/state/{agent_id}")
|
|
153
|
+
return self._handle_response(resp)
|
|
154
|
+
|
|
155
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
156
|
+
headers: Dict[str, str] = {
|
|
157
|
+
"X-API-Key": self._api_key,
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"User-Agent": "agentrx-python/0.1.0",
|
|
160
|
+
}
|
|
161
|
+
trace_id = (
|
|
162
|
+
self._trace_id
|
|
163
|
+
or os.environ.get("OTEL_TRACE_ID")
|
|
164
|
+
or os.environ.get("LANGSMITH_RUN_ID")
|
|
165
|
+
)
|
|
166
|
+
if trace_id:
|
|
167
|
+
headers["X-Trace-Id"] = trace_id
|
|
168
|
+
return headers
|
|
169
|
+
|
|
170
|
+
async def _ensure_schema_cached(self, tool_schema: Dict[str, Any]) -> str:
|
|
171
|
+
serialized = json.dumps(tool_schema, sort_keys=True, separators=(",", ":"))
|
|
172
|
+
schema_hash = hashlib.sha256(serialized.encode()).hexdigest()
|
|
173
|
+
|
|
174
|
+
if not _GLOBAL_SCHEMA_CACHE.get(schema_hash):
|
|
175
|
+
await self.register_schema(tool_schema)
|
|
176
|
+
|
|
177
|
+
return schema_hash
|
|
178
|
+
|
|
179
|
+
async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
180
|
+
resp = await self._http.post(path, json=body)
|
|
181
|
+
return self._handle_response(resp)
|
|
182
|
+
|
|
183
|
+
def _handle_response(self, resp: httpx.Response) -> Dict[str, Any]:
|
|
184
|
+
try:
|
|
185
|
+
data = resp.json()
|
|
186
|
+
except Exception:
|
|
187
|
+
raise AgentRxError(
|
|
188
|
+
status_code=resp.status_code,
|
|
189
|
+
detail=f"Non-JSON response from AgentRx: {resp.text[:200]}",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if resp.status_code in (200, 503):
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
if resp.status_code == 428:
|
|
196
|
+
raise AgentRxError(
|
|
197
|
+
status_code=428,
|
|
198
|
+
detail=(
|
|
199
|
+
"Schema not cached on AgentRx server. "
|
|
200
|
+
f"Detail: {data.get('detail', data)}"
|
|
201
|
+
),
|
|
202
|
+
trace_id=data.get("trace_id"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
detail = data.get("detail") or data.get("message") or str(data)
|
|
206
|
+
raise AgentRxError(
|
|
207
|
+
status_code=resp.status_code,
|
|
208
|
+
detail=str(detail),
|
|
209
|
+
trace_id=data.get("trace_id"),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _ensure_open(self) -> None:
|
|
213
|
+
if self._http is None:
|
|
214
|
+
raise RuntimeError(
|
|
215
|
+
"AgentRxClient is not open. "
|
|
216
|
+
"Use 'async with AgentRxClient(...) as rx:' or call await rx.open() first."
|
|
217
|
+
)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentrx.decorator
|
|
3
|
+
=================
|
|
4
|
+
The @with_recovery decorator — the primary entry point for the SDK.
|
|
5
|
+
|
|
6
|
+
from agentrx import with_recovery
|
|
7
|
+
|
|
8
|
+
@with_recovery(api_key="your_key", agent_id="my_agent")
|
|
9
|
+
async def call_my_tool(payload: dict) -> dict:
|
|
10
|
+
return await some_api.call(payload)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import functools
|
|
17
|
+
import inspect
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Callable, Dict, Optional
|
|
20
|
+
|
|
21
|
+
from .client import AgentRxClient
|
|
22
|
+
from .models import (
|
|
23
|
+
ActionType,
|
|
24
|
+
AgentRxError,
|
|
25
|
+
HumanHandoffRequired,
|
|
26
|
+
RecoveryAction,
|
|
27
|
+
RecoveryException,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def with_recovery(
|
|
32
|
+
api_key: Optional[str] = None,
|
|
33
|
+
agent_id: str = "default_agent",
|
|
34
|
+
goal: str = "Complete the current task",
|
|
35
|
+
tool_schema: Optional[Dict] = None,
|
|
36
|
+
max_retries: int = 2,
|
|
37
|
+
base_url: Optional[str] = None,
|
|
38
|
+
on_handoff: Optional[Callable] = None,
|
|
39
|
+
on_recovery: Optional[Callable] = None,
|
|
40
|
+
ignore_exceptions: tuple = (),
|
|
41
|
+
) -> Callable:
|
|
42
|
+
"""
|
|
43
|
+
Decorator that wraps an agent tool function with AgentRx recovery.
|
|
44
|
+
Works on both async and sync functions.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
api_key: Your AgentRx API key.
|
|
48
|
+
agent_id: Stable identifier for this agent instance.
|
|
49
|
+
goal: The agent's current high-level goal.
|
|
50
|
+
tool_schema: JSON Schema dict for the wrapped tool.
|
|
51
|
+
max_retries: Maximum automatic retry attempts. Default 2.
|
|
52
|
+
base_url: AgentRx API URL.
|
|
53
|
+
on_handoff: Callback fired on HUMAN_HANDOFF.
|
|
54
|
+
on_recovery: Callback fired after every diagnosis.
|
|
55
|
+
ignore_exceptions: Exception types to pass through without diagnosis.
|
|
56
|
+
"""
|
|
57
|
+
def decorator(func: Callable) -> Callable:
|
|
58
|
+
is_async = asyncio.iscoroutinefunction(func)
|
|
59
|
+
|
|
60
|
+
@functools.wraps(func)
|
|
61
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
62
|
+
return await _execute(
|
|
63
|
+
func, args, kwargs, is_async=True,
|
|
64
|
+
api_key=api_key, agent_id=agent_id, goal=goal,
|
|
65
|
+
tool_schema=tool_schema, max_retries=max_retries,
|
|
66
|
+
base_url=base_url, on_handoff=on_handoff,
|
|
67
|
+
on_recovery=on_recovery, ignore_exceptions=ignore_exceptions,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
72
|
+
import concurrent.futures
|
|
73
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
74
|
+
future = pool.submit(
|
|
75
|
+
asyncio.run,
|
|
76
|
+
_execute(
|
|
77
|
+
func, args, kwargs, is_async=False,
|
|
78
|
+
api_key=api_key, agent_id=agent_id, goal=goal,
|
|
79
|
+
tool_schema=tool_schema, max_retries=max_retries,
|
|
80
|
+
base_url=base_url, on_handoff=on_handoff,
|
|
81
|
+
on_recovery=on_recovery, ignore_exceptions=ignore_exceptions,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return future.result()
|
|
85
|
+
|
|
86
|
+
return async_wrapper if is_async else sync_wrapper
|
|
87
|
+
|
|
88
|
+
return decorator
|
|
89
|
+
|
|
90
|
+
async def _execute(
|
|
91
|
+
func: Callable,
|
|
92
|
+
args: tuple,
|
|
93
|
+
kwargs: dict,
|
|
94
|
+
is_async: bool,
|
|
95
|
+
api_key: Optional[str],
|
|
96
|
+
agent_id: str,
|
|
97
|
+
goal: str,
|
|
98
|
+
tool_schema: Optional[Dict],
|
|
99
|
+
max_retries: int,
|
|
100
|
+
base_url: Optional[str],
|
|
101
|
+
on_handoff: Optional[Callable],
|
|
102
|
+
on_recovery: Optional[Callable],
|
|
103
|
+
ignore_exceptions: tuple,
|
|
104
|
+
) -> Any:
|
|
105
|
+
current_payload = _extract_payload(func, args, kwargs)
|
|
106
|
+
|
|
107
|
+
async with AgentRxClient(api_key=api_key, base_url=base_url) as rx:
|
|
108
|
+
attempt = 0
|
|
109
|
+
|
|
110
|
+
while attempt <= max_retries:
|
|
111
|
+
start_ms = int(time.monotonic() * 1000)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
if is_async:
|
|
115
|
+
return await func(*args, **kwargs)
|
|
116
|
+
else:
|
|
117
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
118
|
+
None, functools.partial(func, *args, **kwargs)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
except ignore_exceptions:
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
except (KeyboardInterrupt, SystemExit):
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
latency_ms = int(time.monotonic() * 1000) - start_ms
|
|
129
|
+
error_response = _extract_error_response(exc)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
action = await rx.diagnose(
|
|
133
|
+
agent_id=agent_id,
|
|
134
|
+
goal=goal,
|
|
135
|
+
tool_name=func.__name__,
|
|
136
|
+
payload=current_payload or {},
|
|
137
|
+
error_response=error_response,
|
|
138
|
+
latency_ms=latency_ms,
|
|
139
|
+
tool_schema=tool_schema,
|
|
140
|
+
)
|
|
141
|
+
except AgentRxError as rx_err:
|
|
142
|
+
raise type(exc)(
|
|
143
|
+
f"{exc} [AgentRx unavailable: {rx_err}]"
|
|
144
|
+
) from exc
|
|
145
|
+
|
|
146
|
+
if on_recovery:
|
|
147
|
+
await _call_maybe_async(on_recovery, action)
|
|
148
|
+
|
|
149
|
+
if action.action_type == ActionType.RETRY_WITH_BACKOFF:
|
|
150
|
+
if attempt >= max_retries:
|
|
151
|
+
raise exc
|
|
152
|
+
wait_s = (action.retry_after_ms or 2000) / 1000
|
|
153
|
+
await asyncio.sleep(wait_s)
|
|
154
|
+
attempt += 1
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
elif action.action_type == ActionType.RELAX_SCHEMA:
|
|
158
|
+
if attempt >= max_retries:
|
|
159
|
+
raise exc
|
|
160
|
+
if action.corrected_payload:
|
|
161
|
+
args, kwargs = _inject_payload(
|
|
162
|
+
func, args, kwargs, action.corrected_payload
|
|
163
|
+
)
|
|
164
|
+
current_payload = action.corrected_payload
|
|
165
|
+
attempt += 1
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
elif action.action_type == ActionType.HUMAN_HANDOFF:
|
|
169
|
+
if on_handoff:
|
|
170
|
+
await _call_maybe_async(on_handoff, action)
|
|
171
|
+
return None
|
|
172
|
+
raise HumanHandoffRequired(
|
|
173
|
+
action=action,
|
|
174
|
+
agent_id=agent_id,
|
|
175
|
+
tool_name=func.__name__,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
elif action.action_type == ActionType.SKIP_AND_CONTINUE:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
elif action.action_type == ActionType.ABORT:
|
|
182
|
+
raise exc
|
|
183
|
+
|
|
184
|
+
else:
|
|
185
|
+
raise RecoveryException(
|
|
186
|
+
action=action,
|
|
187
|
+
original_error=exc,
|
|
188
|
+
agent_id=agent_id,
|
|
189
|
+
tool_name=func.__name__,
|
|
190
|
+
) from exc
|
|
191
|
+
|
|
192
|
+
raise RuntimeError("AgentRx decorator: retry loop exited unexpectedly")
|
|
193
|
+
|
|
194
|
+
def _extract_payload(
|
|
195
|
+
func: Callable,
|
|
196
|
+
args: tuple,
|
|
197
|
+
kwargs: dict,
|
|
198
|
+
) -> Optional[Dict[str, Any]]:
|
|
199
|
+
for name in ("payload", "params", "data", "body", "inputs"):
|
|
200
|
+
if name in kwargs and isinstance(kwargs[name], dict):
|
|
201
|
+
return kwargs[name]
|
|
202
|
+
|
|
203
|
+
sig = inspect.signature(func)
|
|
204
|
+
params = list(sig.parameters.keys())
|
|
205
|
+
offset = 1 if params and params[0] in ("self", "cls") else 0
|
|
206
|
+
|
|
207
|
+
for arg in args[offset:]:
|
|
208
|
+
if isinstance(arg, dict):
|
|
209
|
+
return arg
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _inject_payload(
|
|
215
|
+
func: Callable,
|
|
216
|
+
args: tuple,
|
|
217
|
+
kwargs: dict,
|
|
218
|
+
corrected_payload: Dict[str, Any],
|
|
219
|
+
) -> tuple[tuple, dict]:
|
|
220
|
+
for name in ("payload", "params", "data", "body", "inputs"):
|
|
221
|
+
if name in kwargs and isinstance(kwargs[name], dict):
|
|
222
|
+
return args, {**kwargs, name: corrected_payload}
|
|
223
|
+
|
|
224
|
+
sig = inspect.signature(func)
|
|
225
|
+
params = list(sig.parameters.keys())
|
|
226
|
+
offset = 1 if params and params[0] in ("self", "cls") else 0
|
|
227
|
+
|
|
228
|
+
new_args = list(args)
|
|
229
|
+
for i, arg in enumerate(args[offset:], start=offset):
|
|
230
|
+
if isinstance(arg, dict):
|
|
231
|
+
new_args[i] = corrected_payload
|
|
232
|
+
return tuple(new_args), kwargs
|
|
233
|
+
|
|
234
|
+
return args, kwargs
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _extract_error_response(exc: Exception) -> Dict[str, Any]:
|
|
238
|
+
response = getattr(exc, "response", None)
|
|
239
|
+
if response is not None:
|
|
240
|
+
status_code = getattr(response, "status_code", 0)
|
|
241
|
+
try:
|
|
242
|
+
body = response.json()
|
|
243
|
+
message = body.get("message") or body.get("detail") or str(body)
|
|
244
|
+
except Exception:
|
|
245
|
+
message = getattr(response, "text", str(exc))[:500]
|
|
246
|
+
return {"status_code": status_code, "message": message}
|
|
247
|
+
|
|
248
|
+
status_code = getattr(exc, "status_code", getattr(exc, "code", 0))
|
|
249
|
+
return {
|
|
250
|
+
"status_code": int(status_code) if status_code else 0,
|
|
251
|
+
"message": str(exc)[:500],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def _call_maybe_async(callback: Callable, *args: Any) -> None:
|
|
256
|
+
if asyncio.iscoroutinefunction(callback):
|
|
257
|
+
await callback(*args)
|
|
258
|
+
else:
|
|
259
|
+
callback(*args)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentrx.models
|
|
3
|
+
==============
|
|
4
|
+
Typed dataclasses matching the AgentRx API response shapes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ActionType(str, Enum):
|
|
15
|
+
RELAX_SCHEMA = "RELAX_SCHEMA"
|
|
16
|
+
INJECT_KNOWLEDGE = "INJECT_KNOWLEDGE"
|
|
17
|
+
RETRY_WITH_BACKOFF = "RETRY_WITH_BACKOFF"
|
|
18
|
+
HUMAN_HANDOFF = "HUMAN_HANDOFF"
|
|
19
|
+
REFRESH_AUTH = "REFRESH_AUTH"
|
|
20
|
+
SKIP_AND_CONTINUE = "SKIP_AND_CONTINUE"
|
|
21
|
+
ABORT = "ABORT"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FailureSignature(str, Enum):
|
|
25
|
+
SCHEMA_MISMATCH = "SCHEMA_MISMATCH"
|
|
26
|
+
RESOURCE_MISSING = "RESOURCE_MISSING"
|
|
27
|
+
NETWORK_LATENCY = "NETWORK_LATENCY"
|
|
28
|
+
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
|
29
|
+
AUTH_FAILURE = "AUTH_FAILURE"
|
|
30
|
+
TOOL_DEPRECATED = "TOOL_DEPRECATED"
|
|
31
|
+
HALLUCINATED_PARAM = "HALLUCINATED_PARAM"
|
|
32
|
+
HALLUCINATED_VALUE = "HALLUCINATED_VALUE"
|
|
33
|
+
AGENT_LOOP = "AGENT_LOOP"
|
|
34
|
+
UNKNOWN = "UNKNOWN"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class RecoveryAction:
|
|
39
|
+
action_type: ActionType
|
|
40
|
+
failure_signature: FailureSignature
|
|
41
|
+
confidence_score: float
|
|
42
|
+
trace_id: str
|
|
43
|
+
recovery_prompt: Optional[str] = None
|
|
44
|
+
corrected_payload: Optional[Dict[str, Any]] = None
|
|
45
|
+
retry_after_ms: Optional[int] = None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RecoveryAction":
|
|
49
|
+
return cls(
|
|
50
|
+
action_type = ActionType(data["action_type"]),
|
|
51
|
+
failure_signature = FailureSignature(data["failure_signature"]),
|
|
52
|
+
confidence_score = float(data["confidence_score"]),
|
|
53
|
+
trace_id = data["trace_id"],
|
|
54
|
+
recovery_prompt = data.get("recovery_prompt"),
|
|
55
|
+
corrected_payload = data.get("corrected_payload"),
|
|
56
|
+
retry_after_ms = data.get("retry_after_ms"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def should_retry(self) -> bool:
|
|
61
|
+
return self.action_type == ActionType.RETRY_WITH_BACKOFF
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def should_handoff(self) -> bool:
|
|
65
|
+
return self.action_type == ActionType.HUMAN_HANDOFF
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def should_skip(self) -> bool:
|
|
69
|
+
return self.action_type == ActionType.SKIP_AND_CONTINUE
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PreflightResult:
|
|
74
|
+
proceed: bool
|
|
75
|
+
risk_score: float
|
|
76
|
+
warnings: List[str]
|
|
77
|
+
trace_id: str
|
|
78
|
+
suggested_correction: Optional[Dict[str, Any]] = None
|
|
79
|
+
predicted_signature: Optional[FailureSignature] = None
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PreflightResult":
|
|
83
|
+
sig = data.get("predicted_signature")
|
|
84
|
+
return cls(
|
|
85
|
+
proceed = bool(data["proceed"]),
|
|
86
|
+
risk_score = float(data["risk_score"]),
|
|
87
|
+
warnings = data.get("warnings", []),
|
|
88
|
+
trace_id = data["trace_id"],
|
|
89
|
+
suggested_correction = data.get("suggested_correction"),
|
|
90
|
+
predicted_signature = FailureSignature(sig) if sig else None,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class AgentRxError(Exception):
|
|
95
|
+
status_code: int
|
|
96
|
+
detail: str
|
|
97
|
+
trace_id: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
tid = f" [trace_id={self.trace_id}]" if self.trace_id else ""
|
|
101
|
+
return f"AgentRxError {self.status_code}: {self.detail}{tid}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class HumanHandoffRequired(Exception):
|
|
106
|
+
action: RecoveryAction
|
|
107
|
+
agent_id: str
|
|
108
|
+
tool_name: str
|
|
109
|
+
|
|
110
|
+
def __str__(self) -> str:
|
|
111
|
+
return (
|
|
112
|
+
f"Agent '{self.agent_id}' requires human review. "
|
|
113
|
+
f"Tool: '{self.tool_name}'. "
|
|
114
|
+
f"Reason: {self.action.recovery_prompt or self.action.failure_signature}. "
|
|
115
|
+
f"trace_id={self.action.trace_id}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class RecoveryException(Exception):
|
|
121
|
+
action: RecoveryAction
|
|
122
|
+
original_error: Exception
|
|
123
|
+
agent_id: str
|
|
124
|
+
tool_name: str
|
|
125
|
+
|
|
126
|
+
def __post_init__(self) -> None:
|
|
127
|
+
self.__cause__ = self.original_error
|
|
128
|
+
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
return (
|
|
131
|
+
f"AgentRx recovery required for agent '{self.agent_id}' "
|
|
132
|
+
f"tool '{self.tool_name}': {self.action.action_type.value}. "
|
|
133
|
+
f"Recovery prompt: {self.action.recovery_prompt or 'none'}. "
|
|
134
|
+
f"Original error: {self.original_error}. "
|
|
135
|
+
f"trace_id={self.action.trace_id}"
|
|
136
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentrx-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in recovery and resilience layer for AI agents
|
|
5
|
+
Author: Chain Assets LLC
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://chainassetslab.com
|
|
8
|
+
Project-URL: Repository, https://github.com/chainassetslab/agentrx-python
|
|
9
|
+
Keywords: ai,agents,llm,recovery,mcp,langchain,crewai
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
26
|
+
Requires-Dist: respx>=0.21.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# agentrx-python
|
|
29
|
+
|
|
30
|
+
Make your AI agents bulletproof in two lines.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
pip install agentrx-sdk
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
from agentrx import with_recovery
|
|
39
|
+
|
|
40
|
+
@with_recovery(api_key="your_key", agent_id="my_agent")
|
|
41
|
+
async def call_my_tool(payload: dict) -> dict:
|
|
42
|
+
return await some_api.call(payload)
|
|
43
|
+
|
|
44
|
+
When call_my_tool raises an exception, AgentRx diagnoses it and
|
|
45
|
+
automatically retries, corrects the payload, or tells you exactly
|
|
46
|
+
what went wrong.
|
|
47
|
+
|
|
48
|
+
## Environment Variables
|
|
49
|
+
|
|
50
|
+
AGENTRX_API_KEY — Your API key (required)
|
|
51
|
+
AGENTRX_BASE_URL — AgentRx server URL (default: http://localhost:8000)
|
|
52
|
+
OTEL_TRACE_ID — OpenTelemetry trace ID (optional)
|
|
53
|
+
LANGSMITH_RUN_ID — LangSmith run ID (optional)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/agentrx/__init__.py
|
|
4
|
+
src/agentrx/client.py
|
|
5
|
+
src/agentrx/decorator.py
|
|
6
|
+
src/agentrx/models.py
|
|
7
|
+
src/agentrx_sdk.egg-info/PKG-INFO
|
|
8
|
+
src/agentrx_sdk.egg-info/SOURCES.txt
|
|
9
|
+
src/agentrx_sdk.egg-info/dependency_links.txt
|
|
10
|
+
src/agentrx_sdk.egg-info/requires.txt
|
|
11
|
+
src/agentrx_sdk.egg-info/top_level.txt
|
|
12
|
+
tests/test_sdk.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentrx
|
|
File without changes
|