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.
- clevagent-0.1.0/PKG-INFO +91 -0
- clevagent-0.1.0/README.md +68 -0
- clevagent-0.1.0/clevagent/__init__.py +156 -0
- clevagent-0.1.0/clevagent/_client.py +49 -0
- clevagent-0.1.0/clevagent/_cost_tracker.py +265 -0
- clevagent-0.1.0/clevagent/_crash_handler.py +35 -0
- clevagent-0.1.0/clevagent/_heartbeat.py +80 -0
- clevagent-0.1.0/clevagent/_signals.py +32 -0
- clevagent-0.1.0/clevagent/_state.py +59 -0
- clevagent-0.1.0/clevagent.egg-info/PKG-INFO +91 -0
- clevagent-0.1.0/clevagent.egg-info/SOURCES.txt +14 -0
- clevagent-0.1.0/clevagent.egg-info/dependency_links.txt +1 -0
- clevagent-0.1.0/clevagent.egg-info/requires.txt +11 -0
- clevagent-0.1.0/clevagent.egg-info/top_level.txt +1 -0
- clevagent-0.1.0/pyproject.toml +35 -0
- clevagent-0.1.0/setup.cfg +4 -0
clevagent-0.1.0/PKG-INFO
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|