clevagent 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,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: clevagent
3
+ Version: 0.1.0
4
+ Summary: Monitor your AI agents. Heartbeat watchdog, loop detection, cost tracking.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://clevagent.io
7
+ Project-URL: Documentation, https://clevagent.io/docs
8
+ Project-URL: Repository, https://github.com/clevagent/clevagent-python
9
+ Keywords: ai,agent,monitoring,heartbeat,observability
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: System :: Monitoring
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: requests>=2.25.0
16
+ Provides-Extra: openai
17
+ Requires-Dist: openai>=1.0.0; extra == "openai"
18
+ Provides-Extra: anthropic
19
+ Requires-Dist: anthropic>=0.26.0; extra == "anthropic"
20
+ Provides-Extra: all
21
+ Requires-Dist: openai>=1.0.0; extra == "all"
22
+ Requires-Dist: anthropic>=0.26.0; extra == "all"
23
+
24
+ # ClevAgent SDK
25
+
26
+ Monitor your AI agents in 2 lines of code.
27
+
28
+ ```python
29
+ import clevagent
30
+ clevagent.init(api_key=os.environ["CLEVAGENT_API_KEY"], agent="my-agent")
31
+ ```
32
+
33
+ That's it. ClevAgent sends heartbeats every 60 seconds. If your agent goes silent, you get an alert.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install clevagent
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - **Heartbeat watchdog** — detect dead agents automatically
44
+ - **Loop detection** — catch runaway tool-call loops
45
+ - **Cost tracking** — auto-capture for OpenAI/Anthropic SDKs; manual `log_cost()` for others
46
+ - **Auto-restart** — restart Docker containers when agents die (requires `container_id`)
47
+ - **Crash capture** — send last error as an emergency heartbeat on unhandled exceptions
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ import os
53
+ import clevagent
54
+
55
+ clevagent.init(
56
+ api_key=os.environ["CLEVAGENT_API_KEY"],
57
+ agent="my-trading-bot", # agent name (auto-created on first ping)
58
+ interval=60, # heartbeat interval in seconds
59
+ auto_cost=True, # auto-capture OpenAI/Anthropic usage
60
+ )
61
+
62
+ # --- your agent code runs here ---
63
+
64
+ # Optional: manual ping with context
65
+ clevagent.ping(
66
+ status="ok",
67
+ message="Processed 42 signals",
68
+ iteration_count=42,
69
+ )
70
+
71
+ # Optional: manual cost logging (for other SDKs)
72
+ clevagent.log_cost(tokens=1500, cost_usd=0.0023)
73
+ ```
74
+
75
+ ## Cost Tracking
76
+
77
+ | Method | SDKs |
78
+ |--------|------|
79
+ | Auto ✅ | OpenAI (`gpt-4o`, `gpt-4o-mini`, etc.), Anthropic (`claude-3`, `claude-4`) |
80
+ | Manual 📝 | Any other SDK — use `clevagent.log_cost(tokens=N, cost_usd=X)` |
81
+ | Not supported ❌ | Streaming responses (use manual for those) |
82
+
83
+ ## Auto-Restart
84
+
85
+ Auto-restart requires Docker and a `container_id` configured in the ClevAgent dashboard. Non-Docker deployments are not supported.
86
+
87
+ ## Links
88
+
89
+ - [Dashboard](https://clevagent.io)
90
+ - [Documentation](https://clevagent.io/docs)
91
+ - [Support](mailto:support@clevagent.io)
@@ -0,0 +1,68 @@
1
+ # ClevAgent SDK
2
+
3
+ Monitor your AI agents in 2 lines of code.
4
+
5
+ ```python
6
+ import clevagent
7
+ clevagent.init(api_key=os.environ["CLEVAGENT_API_KEY"], agent="my-agent")
8
+ ```
9
+
10
+ That's it. ClevAgent sends heartbeats every 60 seconds. If your agent goes silent, you get an alert.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install clevagent
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - **Heartbeat watchdog** — detect dead agents automatically
21
+ - **Loop detection** — catch runaway tool-call loops
22
+ - **Cost tracking** — auto-capture for OpenAI/Anthropic SDKs; manual `log_cost()` for others
23
+ - **Auto-restart** — restart Docker containers when agents die (requires `container_id`)
24
+ - **Crash capture** — send last error as an emergency heartbeat on unhandled exceptions
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ import os
30
+ import clevagent
31
+
32
+ clevagent.init(
33
+ api_key=os.environ["CLEVAGENT_API_KEY"],
34
+ agent="my-trading-bot", # agent name (auto-created on first ping)
35
+ interval=60, # heartbeat interval in seconds
36
+ auto_cost=True, # auto-capture OpenAI/Anthropic usage
37
+ )
38
+
39
+ # --- your agent code runs here ---
40
+
41
+ # Optional: manual ping with context
42
+ clevagent.ping(
43
+ status="ok",
44
+ message="Processed 42 signals",
45
+ iteration_count=42,
46
+ )
47
+
48
+ # Optional: manual cost logging (for other SDKs)
49
+ clevagent.log_cost(tokens=1500, cost_usd=0.0023)
50
+ ```
51
+
52
+ ## Cost Tracking
53
+
54
+ | Method | SDKs |
55
+ |--------|------|
56
+ | Auto ✅ | OpenAI (`gpt-4o`, `gpt-4o-mini`, etc.), Anthropic (`claude-3`, `claude-4`) |
57
+ | Manual 📝 | Any other SDK — use `clevagent.log_cost(tokens=N, cost_usd=X)` |
58
+ | Not supported ❌ | Streaming responses (use manual for those) |
59
+
60
+ ## Auto-Restart
61
+
62
+ Auto-restart requires Docker and a `container_id` configured in the ClevAgent dashboard. Non-Docker deployments are not supported.
63
+
64
+ ## Links
65
+
66
+ - [Dashboard](https://clevagent.io)
67
+ - [Documentation](https://clevagent.io/docs)
68
+ - [Support](mailto:support@clevagent.io)
@@ -0,0 +1,156 @@
1
+ """
2
+ ClevAgent SDK — Monitor your AI agents in 2 lines of code.
3
+
4
+ import clevagent
5
+ clevagent.init(api_key="cv_xxx", agent="my-agent")
6
+
7
+ The SDK starts a background daemon thread that sends heartbeats to the
8
+ ClevAgent API every `interval` seconds. If the process dies, pings stop,
9
+ and the server detects the silence and fires an alert.
10
+ """
11
+
12
+ import logging
13
+ from typing import Optional
14
+
15
+ from ._state import _state
16
+ from ._heartbeat import HeartbeatThread
17
+ from ._cost_tracker import install_auto_cost
18
+ from ._signals import install_signal_handlers
19
+ from ._crash_handler import install as _install_crash_handler
20
+
21
+ logger = logging.getLogger("clevagent")
22
+
23
+ # Module-level thread reference — replaced on each init() call
24
+ _thread: Optional[HeartbeatThread] = None
25
+
26
+
27
+ def init(
28
+ api_key: str,
29
+ agent: str,
30
+ interval: int = 60,
31
+ endpoint: str = "https://api.clevagent.io",
32
+ auto_cost: bool = True,
33
+ ) -> None:
34
+ """
35
+ Initialize ClevAgent monitoring. Call once at agent startup.
36
+
37
+ Args:
38
+ api_key: Project API key from the ClevAgent dashboard (cv_xxx).
39
+ agent: Unique agent name within the project. Auto-created on first ping.
40
+ interval: Heartbeat interval in seconds (default: 60).
41
+ endpoint: API base URL (override for self-hosted or local dev).
42
+ auto_cost: If True, monkey-patches OpenAI/Anthropic SDKs to capture usage.
43
+ Falls back gracefully if SDKs are not installed or have changed.
44
+ """
45
+ global _thread
46
+
47
+ # Stop existing thread if re-initializing
48
+ if _thread is not None and _thread.is_alive():
49
+ _thread.stop(send_final=False)
50
+
51
+ _state.api_key = api_key
52
+ _state.agent = agent
53
+ _state.interval = interval
54
+ _state.endpoint = endpoint.rstrip("/")
55
+ _state.initialized = True
56
+
57
+ if auto_cost:
58
+ install_auto_cost()
59
+
60
+ _install_crash_handler()
61
+
62
+ _thread = HeartbeatThread()
63
+ _thread.start()
64
+
65
+ install_signal_handlers()
66
+
67
+ logger.info(
68
+ "ClevAgent initialized — agent=%s endpoint=%s interval=%ds auto_cost=%s",
69
+ agent, _state.endpoint, interval, auto_cost,
70
+ )
71
+
72
+
73
+ def ping(
74
+ status: str = "ok",
75
+ message: Optional[str] = None,
76
+ tokens_used: Optional[int] = None,
77
+ cost_usd: Optional[float] = None,
78
+ tool_calls: Optional[int] = None,
79
+ iteration_count: Optional[int] = None,
80
+ memory_mb: Optional[float] = None,
81
+ custom: Optional[dict] = None,
82
+ ) -> None:
83
+ """
84
+ Send a manual heartbeat ping.
85
+
86
+ Use for granular control — e.g., after completing a task loop, or to
87
+ report a warning/error state before the regular interval fires.
88
+
89
+ Args:
90
+ status: "ok" | "warning" | "error"
91
+ message: Optional free-text status message (used for loop detection).
92
+ tokens_used: Token count for this ping cycle.
93
+ cost_usd: Cost for this ping cycle in USD.
94
+ tool_calls: Number of tool calls made since last ping.
95
+ iteration_count: Current loop iteration count (for loop detection).
96
+ memory_mb: Current memory usage in MB.
97
+ custom: Arbitrary JSON-serializable dict stored in Heartbeat.custom_data.
98
+ """
99
+ if _thread is None:
100
+ raise RuntimeError(
101
+ "clevagent.init() must be called before ping(). "
102
+ "Example: clevagent.init(api_key='cv_xxx', agent='my-agent')"
103
+ )
104
+ extra: dict = {}
105
+ if custom is not None:
106
+ import json
107
+ extra["custom_data"] = json.dumps(custom)
108
+
109
+ _thread.send_now(
110
+ status=status,
111
+ message=message,
112
+ tokens_used=tokens_used,
113
+ cost_usd=cost_usd,
114
+ tool_calls=tool_calls,
115
+ iteration_count=iteration_count,
116
+ memory_mb=memory_mb,
117
+ **extra,
118
+ )
119
+
120
+
121
+ def log_cost(tokens: int, cost_usd: float, model: Optional[str] = None) -> None: # noqa: ARG001
122
+ """
123
+ Explicitly log cost data. Use this as a fallback when auto_cost is
124
+ unavailable or when you want precise cost accounting.
125
+
126
+ Example:
127
+ response = client.messages.create(...)
128
+ clevagent.log_cost(
129
+ tokens=response.usage.input_tokens + response.usage.output_tokens,
130
+ cost_usd=0.0045,
131
+ )
132
+ """
133
+ _state.accumulate_cost(tokens=tokens, cost_usd=cost_usd)
134
+
135
+
136
+ def log_iteration(count: int) -> None:
137
+ """
138
+ Log the current iteration count. Used by the loop detector to identify
139
+ runaway agents that keep incrementing without progress.
140
+ """
141
+ _state._iteration_count = count
142
+
143
+
144
+ def shutdown() -> None:
145
+ """
146
+ Stop the heartbeat thread gracefully.
147
+
148
+ Sends a final "shutdown" heartbeat so the server knows the agent
149
+ stopped intentionally (not crashed). Auto-called on SIGTERM/SIGINT.
150
+ """
151
+ global _thread
152
+ if _thread is not None:
153
+ _thread.stop(send_final=True)
154
+ _thread = None
155
+ _state.initialized = False
156
+ logger.info("ClevAgent shutdown complete")
@@ -0,0 +1,49 @@
1
+ """HTTP client for ClevAgent API.
2
+
3
+ Uses `requests` (only external dependency) with a 5-second timeout and 1 retry.
4
+ X-API-Key auth header is set automatically from _state.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Optional
9
+
10
+ import requests
11
+
12
+ logger = logging.getLogger("clevagent")
13
+
14
+ _TIMEOUT = 5 # seconds per request
15
+ _RETRIES = 1 # retry once on network error
16
+
17
+
18
+ def send_heartbeat(
19
+ endpoint: str,
20
+ api_key: str,
21
+ agent: str,
22
+ **payload: Any,
23
+ ) -> Optional[dict]:
24
+ """
25
+ POST /api/v1/heartbeat.
26
+
27
+ Returns the parsed JSON response on success, or None on failure.
28
+ Never raises — all errors are logged as warnings.
29
+ """
30
+ url = f"{endpoint}/api/v1/heartbeat"
31
+ headers = {"X-API-Key": api_key, "Content-Type": "application/json"}
32
+ body = {"agent": agent, **{k: v for k, v in payload.items() if v is not None}}
33
+
34
+ last_err: Optional[Exception] = None
35
+ for attempt in range(_RETRIES + 1):
36
+ try:
37
+ resp = requests.post(url, json=body, headers=headers, timeout=_TIMEOUT)
38
+ resp.raise_for_status()
39
+ return resp.json()
40
+ except requests.exceptions.RequestException as exc:
41
+ last_err = exc
42
+ if attempt < _RETRIES:
43
+ logger.debug("Heartbeat attempt %d failed, retrying: %s", attempt + 1, exc)
44
+
45
+ logger.warning(
46
+ "Heartbeat failed after %d attempt(s) — agent=%s error=%s",
47
+ _RETRIES + 1, agent, last_err,
48
+ )
49
+ return None
@@ -0,0 +1,265 @@
1
+ """Auto cost tracking — monkey-patches OpenAI and Anthropic SDK clients.
2
+
3
+ ⚠️ SDK versioning risk: internal class paths may change between library versions.
4
+ All patches are wrapped in try/except. On failure: logs a warning, leaves auto_cost
5
+ inactive, and directs the user to clevagent.log_cost() for manual tracking.
6
+
7
+ OpenAI v1.x: patches openai.resources.chat.completions.Completions.create
8
+ patches openai.resources.chat.completions.AsyncCompletions.create
9
+ Anthropic: patches anthropic.resources.messages.Messages.create
10
+ patches anthropic.resources.messages.AsyncMessages.create
11
+
12
+ ⚠️ Streaming (stream=True) is NOT auto-tracked in v0.1.0.
13
+ For streaming calls, use clevagent.log_cost(tokens=N, cost_usd=X.XX) manually.
14
+ Streaming auto-cost is planned for v0.2.0.
15
+ """
16
+
17
+ import logging
18
+ from typing import Optional
19
+
20
+ logger = logging.getLogger("clevagent")
21
+
22
+
23
+ # ── Pricing tables ─────────────────────────────────────────────────────────────
24
+
25
+ # USD per 1,000 input/output tokens
26
+ _OPENAI_PRICING: dict[str, dict[str, float]] = {
27
+ "gpt-4o-mini": {"input": 0.000150, "output": 0.000600},
28
+ "gpt-4o": {"input": 0.005, "output": 0.015},
29
+ "gpt-4-turbo": {"input": 0.01, "output": 0.03},
30
+ "gpt-4": {"input": 0.03, "output": 0.06},
31
+ "gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
32
+ }
33
+
34
+ # USD per 1,000,000 input/output tokens
35
+ _ANTHROPIC_PRICING: dict[str, dict[str, float]] = {
36
+ "claude-haiku-4": {"input": 0.80, "output": 4.0},
37
+ "claude-sonnet-4": {"input": 3.0, "output": 15.0},
38
+ "claude-opus-4": {"input": 15.0, "output": 75.0},
39
+ "claude-3-haiku": {"input": 0.25, "output": 1.25},
40
+ "claude-3-5-sonnet": {"input": 3.0, "output": 15.0},
41
+ "claude-3-opus": {"input": 15.0, "output": 75.0},
42
+ }
43
+
44
+
45
+ def _match_pricing(model: str, table: dict) -> Optional[dict]:
46
+ """Find the best pricing entry for a model name (substring match)."""
47
+ model_lower = model.lower()
48
+ for key, pricing in table.items():
49
+ if key in model_lower:
50
+ return pricing
51
+ return None
52
+
53
+
54
+ def _calc_openai_cost(model: str, usage) -> float:
55
+ pricing = _match_pricing(model, _OPENAI_PRICING)
56
+ if not pricing:
57
+ return 0.0
58
+ return (
59
+ (getattr(usage, "prompt_tokens", 0) / 1000) * pricing["input"] +
60
+ (getattr(usage, "completion_tokens", 0) / 1000) * pricing["output"]
61
+ )
62
+
63
+
64
+ def _calc_anthropic_cost(model: str, usage) -> float:
65
+ pricing = _match_pricing(model, _ANTHROPIC_PRICING)
66
+ if not pricing:
67
+ return 0.0
68
+ return (
69
+ (getattr(usage, "input_tokens", 0) / 1_000_000) * pricing["input"] +
70
+ (getattr(usage, "output_tokens", 0) / 1_000_000) * pricing["output"]
71
+ )
72
+
73
+
74
+ # ── Patches ────────────────────────────────────────────────────────────────────
75
+
76
+ def _patch_openai() -> bool:
77
+ """
78
+ Patch OpenAI SDK v1.x (openai.resources.chat.completions.Completions.create).
79
+ Returns True if patch was applied successfully.
80
+ """
81
+ try:
82
+ from openai.resources.chat.completions import Completions # type: ignore
83
+ from ._state import _state
84
+
85
+ _original = Completions.create
86
+
87
+ def _patched(self_inner, *args, **kwargs):
88
+ if kwargs.get("stream"):
89
+ logger.warning(
90
+ "clevagent auto_cost: stream=True detected — streaming is not auto-tracked "
91
+ "in v0.1.0. Use clevagent.log_cost(tokens=N, cost_usd=X) manually."
92
+ )
93
+ return _original(self_inner, *args, **kwargs)
94
+ resp = _original(self_inner, *args, **kwargs)
95
+ try:
96
+ usage = getattr(resp, "usage", None)
97
+ if usage:
98
+ model = getattr(resp, "model", "")
99
+ tokens = getattr(usage, "total_tokens", 0)
100
+ cost = _calc_openai_cost(model, usage)
101
+ # Count tool calls from response choices
102
+ tool_calls = 0
103
+ choices = getattr(resp, "choices", [])
104
+ if choices:
105
+ tc = getattr(getattr(choices[0], "message", None), "tool_calls", None)
106
+ tool_calls = len(tc) if tc else 0
107
+ _state.accumulate_cost(tokens=tokens, cost_usd=cost, tool_calls=tool_calls)
108
+ except Exception:
109
+ pass # Never let tracking errors affect the actual API call
110
+ return resp
111
+
112
+ Completions.create = _patched
113
+ logger.debug("OpenAI SDK patched (Completions.create)")
114
+ return True
115
+
116
+ except (ImportError, AttributeError) as exc:
117
+ logger.debug("OpenAI patch skipped: %s", exc)
118
+ return False
119
+
120
+
121
+ def _patch_openai_async() -> bool:
122
+ """
123
+ Patch OpenAI SDK v1.x async client (AsyncCompletions.create).
124
+ Returns True if patch was applied successfully.
125
+ """
126
+ try:
127
+ from openai.resources.chat.completions import AsyncCompletions # type: ignore
128
+ from ._state import _state
129
+
130
+ _original = AsyncCompletions.create
131
+
132
+ async def _patched_async(self_inner, *args, **kwargs):
133
+ if kwargs.get("stream"):
134
+ logger.warning(
135
+ "clevagent auto_cost: stream=True detected — streaming is not auto-tracked "
136
+ "in v0.1.0. Use clevagent.log_cost(tokens=N, cost_usd=X) manually."
137
+ )
138
+ return await _original(self_inner, *args, **kwargs)
139
+ resp = await _original(self_inner, *args, **kwargs)
140
+ try:
141
+ usage = getattr(resp, "usage", None)
142
+ if usage:
143
+ model = getattr(resp, "model", "")
144
+ tokens = getattr(usage, "total_tokens", 0)
145
+ cost = _calc_openai_cost(model, usage)
146
+ tool_calls = 0
147
+ choices = getattr(resp, "choices", [])
148
+ if choices:
149
+ tc = getattr(getattr(choices[0], "message", None), "tool_calls", None)
150
+ tool_calls = len(tc) if tc else 0
151
+ _state.accumulate_cost(tokens=tokens, cost_usd=cost, tool_calls=tool_calls)
152
+ except Exception:
153
+ pass
154
+ return resp
155
+
156
+ AsyncCompletions.create = _patched_async
157
+ logger.debug("OpenAI SDK patched (AsyncCompletions.create)")
158
+ return True
159
+
160
+ except (ImportError, AttributeError) as exc:
161
+ logger.debug("OpenAI async patch skipped: %s", exc)
162
+ return False
163
+
164
+
165
+ def _patch_anthropic() -> bool:
166
+ """
167
+ Patch Anthropic SDK (anthropic.resources.messages.Messages.create).
168
+ Returns True if patch was applied successfully.
169
+ """
170
+ try:
171
+ from anthropic.resources.messages import Messages # type: ignore
172
+ from ._state import _state
173
+
174
+ _original = Messages.create
175
+
176
+ def _patched(self_inner, *args, **kwargs):
177
+ if kwargs.get("stream"):
178
+ logger.warning(
179
+ "clevagent auto_cost: stream=True detected — streaming is not auto-tracked "
180
+ "in v0.1.0. Use clevagent.log_cost(tokens=N, cost_usd=X) manually."
181
+ )
182
+ return _original(self_inner, *args, **kwargs)
183
+ resp = _original(self_inner, *args, **kwargs)
184
+ try:
185
+ usage = getattr(resp, "usage", None)
186
+ if usage:
187
+ model = getattr(resp, "model", "")
188
+ tokens = (
189
+ getattr(usage, "input_tokens", 0) +
190
+ getattr(usage, "output_tokens", 0)
191
+ )
192
+ cost = _calc_anthropic_cost(model, usage)
193
+ _state.accumulate_cost(tokens=tokens, cost_usd=cost)
194
+ except Exception:
195
+ pass
196
+ return resp
197
+
198
+ Messages.create = _patched
199
+ logger.debug("Anthropic SDK patched (Messages.create)")
200
+ return True
201
+
202
+ except (ImportError, AttributeError) as exc:
203
+ logger.debug("Anthropic patch skipped: %s", exc)
204
+ return False
205
+
206
+
207
+ def _patch_anthropic_async() -> bool:
208
+ """
209
+ Patch Anthropic SDK async client (AsyncMessages.create).
210
+ Returns True if patch was applied successfully.
211
+ """
212
+ try:
213
+ from anthropic.resources.messages import AsyncMessages # type: ignore
214
+ from ._state import _state
215
+
216
+ _original = AsyncMessages.create
217
+
218
+ async def _patched_async(self_inner, *args, **kwargs):
219
+ if kwargs.get("stream"):
220
+ logger.warning(
221
+ "clevagent auto_cost: stream=True detected — streaming is not auto-tracked "
222
+ "in v0.1.0. Use clevagent.log_cost(tokens=N, cost_usd=X) manually."
223
+ )
224
+ return await _original(self_inner, *args, **kwargs)
225
+ resp = await _original(self_inner, *args, **kwargs)
226
+ try:
227
+ usage = getattr(resp, "usage", None)
228
+ if usage:
229
+ model = getattr(resp, "model", "")
230
+ tokens = (
231
+ getattr(usage, "input_tokens", 0) +
232
+ getattr(usage, "output_tokens", 0)
233
+ )
234
+ cost = _calc_anthropic_cost(model, usage)
235
+ _state.accumulate_cost(tokens=tokens, cost_usd=cost)
236
+ except Exception:
237
+ pass
238
+ return resp
239
+
240
+ AsyncMessages.create = _patched_async
241
+ logger.debug("Anthropic SDK patched (AsyncMessages.create)")
242
+ return True
243
+
244
+ except (ImportError, AttributeError) as exc:
245
+ logger.debug("Anthropic async patch skipped: %s", exc)
246
+ return False
247
+
248
+
249
+ def install_auto_cost() -> None:
250
+ """
251
+ Activate auto cost tracking. Patches available AI SDKs; skips missing ones.
252
+ If no SDK is found, logs guidance for manual tracking via clevagent.log_cost().
253
+ """
254
+ from ._state import _state
255
+
256
+ patched = _patch_openai() | _patch_openai_async() | _patch_anthropic() | _patch_anthropic_async()
257
+
258
+ if patched:
259
+ _state.auto_cost_active = True
260
+ logger.info("Auto cost tracking enabled")
261
+ else:
262
+ logger.info(
263
+ "Auto cost tracking inactive — openai/anthropic not installed. "
264
+ "Use clevagent.log_cost(tokens=N, cost_usd=X.XX) for manual tracking."
265
+ )
@@ -0,0 +1,35 @@
1
+ """
2
+ Crash handler — captures unhandled exceptions and sends an emergency heartbeat.
3
+
4
+ Chains the existing sys.excepthook so other libraries (e.g., IPython, pytest)
5
+ are not broken. Only fires if clevagent.init() has been called.
6
+ """
7
+
8
+ import sys
9
+ import traceback
10
+
11
+ _original_excepthook = sys.excepthook
12
+
13
+
14
+ def _crash_handler(exc_type, exc_value, exc_tb):
15
+ error_msg = f"{exc_type.__name__}: {exc_value}"
16
+ tb_short = "".join(traceback.format_tb(exc_tb)[-3:])
17
+
18
+ try:
19
+ from clevagent._state import _state
20
+ if _state.initialized:
21
+ from clevagent._client import send_heartbeat
22
+ send_heartbeat(
23
+ status="error",
24
+ message=f"CRASH: {error_msg}",
25
+ custom={"traceback": tb_short, "crash": True},
26
+ )
27
+ except Exception:
28
+ pass # Never let crash-capture errors suppress the real traceback
29
+
30
+ _original_excepthook(exc_type, exc_value, exc_tb)
31
+
32
+
33
+ def install():
34
+ """Install the crash handler as sys.excepthook."""
35
+ sys.excepthook = _crash_handler
@@ -0,0 +1,80 @@
1
+ """Background heartbeat thread.
2
+
3
+ A daemon thread sends POST /api/v1/heartbeat every `_state.interval` seconds.
4
+ On shutdown(), a final "shutdown" heartbeat is sent before the thread exits.
5
+ The stop event allows immediate wake-up when shutdown() is called mid-sleep.
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+ from typing import Any, Optional
11
+
12
+ from ._client import send_heartbeat
13
+ from ._state import _state
14
+
15
+ logger = logging.getLogger("clevagent")
16
+
17
+
18
+ class HeartbeatThread(threading.Thread):
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__(daemon=True, name="clevagent-heartbeat")
22
+ self._stop_event = threading.Event()
23
+
24
+ def run(self) -> None:
25
+ logger.debug(
26
+ "Heartbeat thread started (agent=%s interval=%ds endpoint=%s)",
27
+ _state.agent, _state.interval, _state.endpoint,
28
+ )
29
+ # Send initial heartbeat immediately so the server knows the agent is alive.
30
+ self._send_heartbeat()
31
+
32
+ # wait(timeout) returns True if stop was requested, False on timeout.
33
+ # Loop continues as long as the event is NOT set (i.e., normal operation).
34
+ while not self._stop_event.wait(timeout=_state.interval):
35
+ self._send_heartbeat()
36
+
37
+ def _send_heartbeat(
38
+ self,
39
+ status: str = "ok",
40
+ message: Optional[str] = None,
41
+ extra: Optional[dict] = None,
42
+ ) -> None:
43
+ """Send one heartbeat, including accumulated cost/usage data."""
44
+ data = _state.flush_and_reset()
45
+ if extra:
46
+ data.update({k: v for k, v in extra.items() if v is not None})
47
+
48
+ resp = send_heartbeat(
49
+ endpoint=_state.endpoint,
50
+ api_key=_state.api_key,
51
+ agent=_state.agent,
52
+ status=status,
53
+ message=message,
54
+ **data,
55
+ )
56
+
57
+ if resp and resp.get("agent_id"):
58
+ _state.agent_id = resp["agent_id"]
59
+ logger.debug("Heartbeat OK — agent_id=%s server_status=%s",
60
+ _state.agent_id, resp.get("status"))
61
+
62
+ def send_now(
63
+ self,
64
+ status: str = "ok",
65
+ message: Optional[str] = None,
66
+ **extra: Any,
67
+ ) -> None:
68
+ """Trigger an immediate out-of-band heartbeat (used by ping())."""
69
+ self._send_heartbeat(status=status, message=message, extra=extra)
70
+
71
+ def stop(self, send_final: bool = True) -> None:
72
+ """Signal the thread to stop. If send_final=True, sends a shutdown heartbeat."""
73
+ self._stop_event.set()
74
+ if send_final:
75
+ try:
76
+ self._send_heartbeat(status="shutdown", message="Agent shutting down gracefully")
77
+ except Exception as exc:
78
+ logger.debug("Final heartbeat skipped: %s", exc)
79
+ self.join(timeout=10)
80
+ logger.debug("Heartbeat thread stopped")
@@ -0,0 +1,32 @@
1
+ """SIGTERM/SIGINT signal handler — calls clevagent.shutdown() on process termination.
2
+
3
+ Must be installed from the main thread; safely skips if called from a non-main thread
4
+ (e.g., when the user embeds clevagent in a thread-based framework).
5
+ """
6
+
7
+ import logging
8
+ import signal
9
+
10
+ logger = logging.getLogger("clevagent")
11
+
12
+
13
+ def install_signal_handlers() -> None:
14
+ """Register SIGTERM and SIGINT handlers to call clevagent.shutdown()."""
15
+
16
+ def _handle(signum: int, frame) -> None: # noqa: ARG001
17
+ logger.info("clevagent: signal %s received — shutting down gracefully", signum)
18
+ # Import at call-time to avoid circular import at module load
19
+ import clevagent
20
+ clevagent.shutdown()
21
+
22
+ try:
23
+ signal.signal(signal.SIGTERM, _handle)
24
+ signal.signal(signal.SIGINT, _handle)
25
+ logger.debug("Signal handlers registered (SIGTERM, SIGINT)")
26
+ except (OSError, ValueError):
27
+ # signal.signal() raises ValueError if called from a non-main thread,
28
+ # and OSError on some restricted environments.
29
+ logger.debug(
30
+ "clevagent: signal handler registration skipped "
31
+ "(not in main thread or restricted environment)"
32
+ )
@@ -0,0 +1,59 @@
1
+ """Shared SDK state — single mutable object shared across all SDK modules."""
2
+
3
+ import threading
4
+ from typing import Optional
5
+
6
+
7
+ class _SDKState:
8
+ """Thread-safe container for SDK runtime state."""
9
+
10
+ def __init__(self) -> None:
11
+ self._lock = threading.Lock()
12
+
13
+ # Configuration (set by init())
14
+ self.api_key: str = ""
15
+ self.agent: str = ""
16
+ self.interval: int = 60
17
+ self.endpoint: str = "https://api.clevagent.io"
18
+
19
+ # Runtime
20
+ self.agent_id: Optional[int] = None
21
+ self.auto_cost_active: bool = False
22
+ self.initialized: bool = False
23
+
24
+ # Accumulated cost/usage between heartbeats (reset after each send)
25
+ self._tokens: int = 0
26
+ self._cost_usd: float = 0.0
27
+ self._tool_calls: int = 0
28
+ self._iteration_count: Optional[int] = None
29
+
30
+ def accumulate_cost(
31
+ self,
32
+ tokens: int = 0,
33
+ cost_usd: float = 0.0,
34
+ tool_calls: int = 0,
35
+ ) -> None:
36
+ """Thread-safe accumulation of usage data."""
37
+ with self._lock:
38
+ self._tokens += tokens
39
+ self._cost_usd += cost_usd
40
+ self._tool_calls += tool_calls
41
+
42
+ def flush_and_reset(self) -> dict:
43
+ """Return accumulated data as a dict and reset counters. Thread-safe."""
44
+ with self._lock:
45
+ data = {
46
+ "tokens_used": self._tokens if self._tokens else None,
47
+ "cost_usd": self._cost_usd if self._cost_usd else None,
48
+ "tool_calls": self._tool_calls if self._tool_calls else None,
49
+ "iteration_count": self._iteration_count,
50
+ }
51
+ self._tokens = 0
52
+ self._cost_usd = 0.0
53
+ self._tool_calls = 0
54
+ # iteration_count is set externally by log_iteration() — don't reset
55
+ return data
56
+
57
+
58
+ # Module-level singleton
59
+ _state = _SDKState()
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: clevagent
3
+ Version: 0.1.0
4
+ Summary: Monitor your AI agents. Heartbeat watchdog, loop detection, cost tracking.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://clevagent.io
7
+ Project-URL: Documentation, https://clevagent.io/docs
8
+ Project-URL: Repository, https://github.com/clevagent/clevagent-python
9
+ Keywords: ai,agent,monitoring,heartbeat,observability
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: System :: Monitoring
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: requests>=2.25.0
16
+ Provides-Extra: openai
17
+ Requires-Dist: openai>=1.0.0; extra == "openai"
18
+ Provides-Extra: anthropic
19
+ Requires-Dist: anthropic>=0.26.0; extra == "anthropic"
20
+ Provides-Extra: all
21
+ Requires-Dist: openai>=1.0.0; extra == "all"
22
+ Requires-Dist: anthropic>=0.26.0; extra == "all"
23
+
24
+ # ClevAgent SDK
25
+
26
+ Monitor your AI agents in 2 lines of code.
27
+
28
+ ```python
29
+ import clevagent
30
+ clevagent.init(api_key=os.environ["CLEVAGENT_API_KEY"], agent="my-agent")
31
+ ```
32
+
33
+ That's it. ClevAgent sends heartbeats every 60 seconds. If your agent goes silent, you get an alert.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install clevagent
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - **Heartbeat watchdog** — detect dead agents automatically
44
+ - **Loop detection** — catch runaway tool-call loops
45
+ - **Cost tracking** — auto-capture for OpenAI/Anthropic SDKs; manual `log_cost()` for others
46
+ - **Auto-restart** — restart Docker containers when agents die (requires `container_id`)
47
+ - **Crash capture** — send last error as an emergency heartbeat on unhandled exceptions
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ import os
53
+ import clevagent
54
+
55
+ clevagent.init(
56
+ api_key=os.environ["CLEVAGENT_API_KEY"],
57
+ agent="my-trading-bot", # agent name (auto-created on first ping)
58
+ interval=60, # heartbeat interval in seconds
59
+ auto_cost=True, # auto-capture OpenAI/Anthropic usage
60
+ )
61
+
62
+ # --- your agent code runs here ---
63
+
64
+ # Optional: manual ping with context
65
+ clevagent.ping(
66
+ status="ok",
67
+ message="Processed 42 signals",
68
+ iteration_count=42,
69
+ )
70
+
71
+ # Optional: manual cost logging (for other SDKs)
72
+ clevagent.log_cost(tokens=1500, cost_usd=0.0023)
73
+ ```
74
+
75
+ ## Cost Tracking
76
+
77
+ | Method | SDKs |
78
+ |--------|------|
79
+ | Auto ✅ | OpenAI (`gpt-4o`, `gpt-4o-mini`, etc.), Anthropic (`claude-3`, `claude-4`) |
80
+ | Manual 📝 | Any other SDK — use `clevagent.log_cost(tokens=N, cost_usd=X)` |
81
+ | Not supported ❌ | Streaming responses (use manual for those) |
82
+
83
+ ## Auto-Restart
84
+
85
+ Auto-restart requires Docker and a `container_id` configured in the ClevAgent dashboard. Non-Docker deployments are not supported.
86
+
87
+ ## Links
88
+
89
+ - [Dashboard](https://clevagent.io)
90
+ - [Documentation](https://clevagent.io/docs)
91
+ - [Support](mailto:support@clevagent.io)
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ clevagent/__init__.py
4
+ clevagent/_client.py
5
+ clevagent/_cost_tracker.py
6
+ clevagent/_crash_handler.py
7
+ clevagent/_heartbeat.py
8
+ clevagent/_signals.py
9
+ clevagent/_state.py
10
+ clevagent.egg-info/PKG-INFO
11
+ clevagent.egg-info/SOURCES.txt
12
+ clevagent.egg-info/dependency_links.txt
13
+ clevagent.egg-info/requires.txt
14
+ clevagent.egg-info/top_level.txt
@@ -0,0 +1,11 @@
1
+ requests>=2.25.0
2
+
3
+ [all]
4
+ openai>=1.0.0
5
+ anthropic>=0.26.0
6
+
7
+ [anthropic]
8
+ anthropic>=0.26.0
9
+
10
+ [openai]
11
+ openai>=1.0.0
@@ -0,0 +1 @@
1
+ clevagent
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "clevagent"
7
+ version = "0.1.0"
8
+ description = "Monitor your AI agents. Heartbeat watchdog, loop detection, cost tracking."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ keywords = ["ai", "agent", "monitoring", "heartbeat", "observability"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ "Topic :: System :: Monitoring",
17
+ ]
18
+ dependencies = [
19
+ "requests>=2.25.0",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ # Auto-cost tracking works if these are installed — not required
24
+ openai = ["openai>=1.0.0"]
25
+ anthropic = ["anthropic>=0.26.0"]
26
+ all = ["openai>=1.0.0", "anthropic>=0.26.0"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://clevagent.io"
30
+ Documentation = "https://clevagent.io/docs"
31
+ Repository = "https://github.com/clevagent/clevagent-python"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["."]
35
+ include = ["clevagent*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+