agentpulse-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,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentpulse-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight monitoring for indie AI agents
|
|
5
|
+
Project-URL: Homepage, https://agentpulse.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/marcusbuildsthings-droid/agentpulse
|
|
7
|
+
Project-URL: Documentation, https://docs.agentpulse.dev
|
|
8
|
+
Author-email: Marcus <marcus.builds.things@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agent,ai,llm,monitoring,observability
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# AgentPulse Python SDK
|
|
21
|
+
|
|
22
|
+
Lightweight monitoring for indie AI agents. Zero dependencies. Fire-and-forget.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install agentpulse
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from agentpulse import pulse, init
|
|
34
|
+
|
|
35
|
+
# Initialize with your API key
|
|
36
|
+
init(api_key="ap_...")
|
|
37
|
+
|
|
38
|
+
# Track sessions
|
|
39
|
+
pulse.session_start("main")
|
|
40
|
+
pulse.session_event("main", "tool_call", {"tool": "web_search"})
|
|
41
|
+
pulse.session_end("main")
|
|
42
|
+
|
|
43
|
+
# Report cron jobs
|
|
44
|
+
pulse.cron_report("email-check", status="ok", duration_ms=1200)
|
|
45
|
+
pulse.cron_report("backup", status="error", summary="disk full")
|
|
46
|
+
|
|
47
|
+
# Track costs
|
|
48
|
+
pulse.cost_event(model="claude-opus-4", input_tokens=5000, output_tokens=1000, cost=0.15)
|
|
49
|
+
|
|
50
|
+
# Monitor memory
|
|
51
|
+
pulse.memory_report("MEMORY.md", size_bytes=45000, lines=800)
|
|
52
|
+
|
|
53
|
+
# Custom metrics
|
|
54
|
+
pulse.metric("response_time_ms", 342)
|
|
55
|
+
|
|
56
|
+
# Alerts
|
|
57
|
+
pulse.alert("Cost spike detected", severity="warning", details="$5.20 in last hour")
|
|
58
|
+
|
|
59
|
+
# Heartbeat
|
|
60
|
+
pulse.heartbeat()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
Events are queued locally and flushed to the AgentPulse API in batches every 10 seconds. Zero blocking. If the API is unreachable, events are retried on the next flush cycle.
|
|
66
|
+
|
|
67
|
+
## Environment Variables
|
|
68
|
+
|
|
69
|
+
| Variable | Description |
|
|
70
|
+
|----------|-------------|
|
|
71
|
+
| `AGENTPULSE_API_KEY` | Your API key |
|
|
72
|
+
| `AGENTPULSE_ENDPOINT` | Custom API endpoint |
|
|
73
|
+
| `AGENTPULSE_AGENT` | Agent name (defaults to hostname) |
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# AgentPulse Python SDK
|
|
2
|
+
|
|
3
|
+
Lightweight monitoring for indie AI agents. Zero dependencies. Fire-and-forget.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentpulse
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from agentpulse import pulse, init
|
|
15
|
+
|
|
16
|
+
# Initialize with your API key
|
|
17
|
+
init(api_key="ap_...")
|
|
18
|
+
|
|
19
|
+
# Track sessions
|
|
20
|
+
pulse.session_start("main")
|
|
21
|
+
pulse.session_event("main", "tool_call", {"tool": "web_search"})
|
|
22
|
+
pulse.session_end("main")
|
|
23
|
+
|
|
24
|
+
# Report cron jobs
|
|
25
|
+
pulse.cron_report("email-check", status="ok", duration_ms=1200)
|
|
26
|
+
pulse.cron_report("backup", status="error", summary="disk full")
|
|
27
|
+
|
|
28
|
+
# Track costs
|
|
29
|
+
pulse.cost_event(model="claude-opus-4", input_tokens=5000, output_tokens=1000, cost=0.15)
|
|
30
|
+
|
|
31
|
+
# Monitor memory
|
|
32
|
+
pulse.memory_report("MEMORY.md", size_bytes=45000, lines=800)
|
|
33
|
+
|
|
34
|
+
# Custom metrics
|
|
35
|
+
pulse.metric("response_time_ms", 342)
|
|
36
|
+
|
|
37
|
+
# Alerts
|
|
38
|
+
pulse.alert("Cost spike detected", severity="warning", details="$5.20 in last hour")
|
|
39
|
+
|
|
40
|
+
# Heartbeat
|
|
41
|
+
pulse.heartbeat()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
Events are queued locally and flushed to the AgentPulse API in batches every 10 seconds. Zero blocking. If the API is unreachable, events are retried on the next flush cycle.
|
|
47
|
+
|
|
48
|
+
## Environment Variables
|
|
49
|
+
|
|
50
|
+
| Variable | Description |
|
|
51
|
+
|----------|-------------|
|
|
52
|
+
| `AGENTPULSE_API_KEY` | Your API key |
|
|
53
|
+
| `AGENTPULSE_ENDPOINT` | Custom API endpoint |
|
|
54
|
+
| `AGENTPULSE_AGENT` | Agent name (defaults to hostname) |
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""AgentPulse SDK client — fire-and-forget event reporting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass, field, asdict
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
_DEFAULT_ENDPOINT = "https://api.agentpulse.dev"
|
|
17
|
+
_FLUSH_INTERVAL = 10.0 # seconds
|
|
18
|
+
_BATCH_SIZE = 50
|
|
19
|
+
_QUEUE_MAX = 5000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Event:
|
|
24
|
+
kind: str # session, cron, cost, heartbeat, metric, alert
|
|
25
|
+
ts: float = field(default_factory=time.time)
|
|
26
|
+
data: dict = field(default_factory=dict)
|
|
27
|
+
agent: str = ""
|
|
28
|
+
session: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentPulse:
|
|
32
|
+
"""Non-blocking event reporter with background flush."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: Optional[str] = None,
|
|
37
|
+
endpoint: Optional[str] = None,
|
|
38
|
+
agent_name: Optional[str] = None,
|
|
39
|
+
flush_interval: float = _FLUSH_INTERVAL,
|
|
40
|
+
enabled: bool = True,
|
|
41
|
+
debug: bool = False,
|
|
42
|
+
):
|
|
43
|
+
self.api_key = api_key or os.environ.get("AGENTPULSE_API_KEY", "")
|
|
44
|
+
self.endpoint = (endpoint or os.environ.get("AGENTPULSE_ENDPOINT", _DEFAULT_ENDPOINT)).rstrip("/")
|
|
45
|
+
self.agent_name = agent_name or os.environ.get("AGENTPULSE_AGENT", platform.node())
|
|
46
|
+
self.enabled = enabled and bool(self.api_key)
|
|
47
|
+
self.debug = debug
|
|
48
|
+
|
|
49
|
+
self._queue: queue.Queue[Event] = queue.Queue(maxsize=_QUEUE_MAX)
|
|
50
|
+
self._active_sessions: dict[str, float] = {} # session_key -> start_ts
|
|
51
|
+
self._lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
if self.enabled:
|
|
54
|
+
self._worker = threading.Thread(target=self._flush_loop, args=(flush_interval,), daemon=True)
|
|
55
|
+
self._worker.start()
|
|
56
|
+
atexit.register(self.flush)
|
|
57
|
+
|
|
58
|
+
# ── Public API ───────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def session_start(self, key: str, metadata: Optional[dict] = None) -> None:
|
|
61
|
+
"""Mark a session as started."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._active_sessions[key] = time.time()
|
|
64
|
+
self._enqueue("session", data={"action": "start", **(metadata or {})}, session=key)
|
|
65
|
+
|
|
66
|
+
def session_end(self, key: str, metadata: Optional[dict] = None) -> None:
|
|
67
|
+
"""Mark a session as ended."""
|
|
68
|
+
start = self._active_sessions.pop(key, None)
|
|
69
|
+
duration_ms = int((time.time() - start) * 1000) if start else None
|
|
70
|
+
self._enqueue("session", data={"action": "end", "duration_ms": duration_ms, **(metadata or {})}, session=key)
|
|
71
|
+
|
|
72
|
+
def session_event(self, key: str, event_type: str, data: Optional[dict] = None) -> None:
|
|
73
|
+
"""Log an arbitrary session event."""
|
|
74
|
+
self._enqueue("session", data={"action": event_type, **(data or {})}, session=key)
|
|
75
|
+
|
|
76
|
+
def cron_report(
|
|
77
|
+
self,
|
|
78
|
+
job_name: str,
|
|
79
|
+
status: str = "ok",
|
|
80
|
+
duration_ms: Optional[int] = None,
|
|
81
|
+
summary: Optional[str] = None,
|
|
82
|
+
metadata: Optional[dict] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Report a cron job run."""
|
|
85
|
+
self._enqueue("cron", data={
|
|
86
|
+
"job": job_name,
|
|
87
|
+
"status": status,
|
|
88
|
+
"duration_ms": duration_ms,
|
|
89
|
+
"summary": summary,
|
|
90
|
+
**(metadata or {}),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
def cost_event(
|
|
94
|
+
self,
|
|
95
|
+
model: str,
|
|
96
|
+
input_tokens: int = 0,
|
|
97
|
+
output_tokens: int = 0,
|
|
98
|
+
cost: Optional[float] = None,
|
|
99
|
+
session: Optional[str] = None,
|
|
100
|
+
metadata: Optional[dict] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Report an API cost event."""
|
|
103
|
+
self._enqueue("cost", data={
|
|
104
|
+
"model": model,
|
|
105
|
+
"input_tokens": input_tokens,
|
|
106
|
+
"output_tokens": output_tokens,
|
|
107
|
+
"cost_usd": cost,
|
|
108
|
+
**(metadata or {}),
|
|
109
|
+
}, session=session or "")
|
|
110
|
+
|
|
111
|
+
def heartbeat(self, metadata: Optional[dict] = None) -> None:
|
|
112
|
+
"""Send a heartbeat ping."""
|
|
113
|
+
self._enqueue("heartbeat", data={
|
|
114
|
+
"active_sessions": len(self._active_sessions),
|
|
115
|
+
**(metadata or {}),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
def metric(self, name: str, value: float, tags: Optional[dict] = None) -> None:
|
|
119
|
+
"""Report a custom metric."""
|
|
120
|
+
self._enqueue("metric", data={"name": name, "value": value, "tags": tags or {}})
|
|
121
|
+
|
|
122
|
+
def alert(self, title: str, severity: str = "warning", details: Optional[str] = None) -> None:
|
|
123
|
+
"""Send an alert."""
|
|
124
|
+
self._enqueue("alert", data={"title": title, "severity": severity, "details": details})
|
|
125
|
+
|
|
126
|
+
def memory_report(
|
|
127
|
+
self,
|
|
128
|
+
file: str,
|
|
129
|
+
size_bytes: int,
|
|
130
|
+
lines: Optional[int] = None,
|
|
131
|
+
metadata: Optional[dict] = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Report memory file status."""
|
|
134
|
+
self._enqueue("memory", data={
|
|
135
|
+
"file": file,
|
|
136
|
+
"size_bytes": size_bytes,
|
|
137
|
+
"lines": lines,
|
|
138
|
+
**(metadata or {}),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
# ── Flush / Transport ────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
def flush(self) -> int:
|
|
144
|
+
"""Flush pending events. Returns count sent."""
|
|
145
|
+
batch: list[Event] = []
|
|
146
|
+
while not self._queue.empty() and len(batch) < _BATCH_SIZE * 10:
|
|
147
|
+
try:
|
|
148
|
+
batch.append(self._queue.get_nowait())
|
|
149
|
+
except queue.Empty:
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
if not batch:
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
payload = {
|
|
156
|
+
"agent": self.agent_name,
|
|
157
|
+
"events": [asdict(e) for e in batch],
|
|
158
|
+
}
|
|
159
|
+
try:
|
|
160
|
+
self._post("/v1/ingest", payload)
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
if self.debug:
|
|
163
|
+
print(f"[agentpulse] flush error: {exc}")
|
|
164
|
+
# Re-queue on failure (best effort, may lose some)
|
|
165
|
+
for e in batch[:_QUEUE_MAX // 2]:
|
|
166
|
+
try:
|
|
167
|
+
self._queue.put_nowait(e)
|
|
168
|
+
except queue.Full:
|
|
169
|
+
break
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
return len(batch)
|
|
173
|
+
|
|
174
|
+
def _post(self, path: str, payload: dict) -> dict:
|
|
175
|
+
"""HTTP POST with urllib (no dependencies)."""
|
|
176
|
+
url = self.endpoint + path
|
|
177
|
+
data = json.dumps(payload).encode()
|
|
178
|
+
req = urllib.request.Request(
|
|
179
|
+
url,
|
|
180
|
+
data=data,
|
|
181
|
+
headers={
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
184
|
+
"User-Agent": f"agentpulse-python/{__import__('agentpulse').__version__}",
|
|
185
|
+
},
|
|
186
|
+
method="POST",
|
|
187
|
+
)
|
|
188
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
189
|
+
return json.loads(resp.read())
|
|
190
|
+
|
|
191
|
+
def _enqueue(self, kind: str, data: dict, session: str = "") -> None:
|
|
192
|
+
if not self.enabled:
|
|
193
|
+
return
|
|
194
|
+
event = Event(kind=kind, data=data, agent=self.agent_name, session=session)
|
|
195
|
+
try:
|
|
196
|
+
self._queue.put_nowait(event)
|
|
197
|
+
except queue.Full:
|
|
198
|
+
if self.debug:
|
|
199
|
+
print("[agentpulse] queue full, dropping event")
|
|
200
|
+
|
|
201
|
+
def _flush_loop(self, interval: float) -> None:
|
|
202
|
+
while True:
|
|
203
|
+
time.sleep(interval)
|
|
204
|
+
self.flush()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ── Module-level singleton ───────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
pulse = AgentPulse()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def init(
|
|
213
|
+
api_key: Optional[str] = None,
|
|
214
|
+
endpoint: Optional[str] = None,
|
|
215
|
+
agent_name: Optional[str] = None,
|
|
216
|
+
**kwargs: Any,
|
|
217
|
+
) -> AgentPulse:
|
|
218
|
+
"""Initialize the global pulse instance."""
|
|
219
|
+
global pulse
|
|
220
|
+
pulse = AgentPulse(api_key=api_key, endpoint=endpoint, agent_name=agent_name, **kwargs)
|
|
221
|
+
return pulse
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentpulse-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight monitoring for indie AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Marcus", email = "marcus.builds.things@gmail.com" }]
|
|
13
|
+
keywords = ["ai", "agent", "monitoring", "observability", "llm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"Topic :: System :: Monitoring",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://agentpulse.dev"
|
|
25
|
+
Repository = "https://github.com/marcusbuildsthings-droid/agentpulse"
|
|
26
|
+
Documentation = "https://docs.agentpulse.dev"
|