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,7 @@
1
+ """AgentPulse — Lightweight monitoring for indie AI agents."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from agentpulse.client import AgentPulse, pulse
6
+
7
+ __all__ = ["AgentPulse", "pulse"]
@@ -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"