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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,7 @@
1
+ httpx>=0.27.0
2
+ pydantic>=2.0.0
3
+
4
+ [dev]
5
+ pytest>=8.0.0
6
+ pytest-asyncio>=0.23.0
7
+ respx>=0.21.0
File without changes