agentmetrics 0.1.0__py3-none-any.whl
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.
agentmetrics/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("agentmetrics")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HttpClient:
|
|
13
|
+
def __init__(self, api_key: str, base_url: str):
|
|
14
|
+
self.api_key = api_key
|
|
15
|
+
self.base_url = base_url.rstrip("/")
|
|
16
|
+
self._pending_threads: list[threading.Thread] = []
|
|
17
|
+
self._lock = threading.Lock()
|
|
18
|
+
|
|
19
|
+
def fire_and_forget(self, payload: dict) -> None:
|
|
20
|
+
"""Post event in a background thread. Never blocks. Never raises."""
|
|
21
|
+
t = threading.Thread(target=self._post_with_retry, args=(payload,), daemon=True)
|
|
22
|
+
with self._lock:
|
|
23
|
+
self._pending_threads.append(t)
|
|
24
|
+
t.start()
|
|
25
|
+
|
|
26
|
+
def _post_with_retry(self, payload: dict, retries: int = 3) -> None:
|
|
27
|
+
url = f"{self.base_url}/events"
|
|
28
|
+
headers = {
|
|
29
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
}
|
|
32
|
+
for attempt in range(retries):
|
|
33
|
+
try:
|
|
34
|
+
resp = requests.post(url, json=payload, headers=headers, timeout=5)
|
|
35
|
+
if resp.status_code in (200, 201):
|
|
36
|
+
return
|
|
37
|
+
logger.debug("AgentMetrics: non-2xx response %s", resp.status_code)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.debug("AgentMetrics: send failed (attempt %d): %s", attempt + 1, exc)
|
|
40
|
+
if attempt < retries - 1:
|
|
41
|
+
time.sleep(1)
|
|
42
|
+
|
|
43
|
+
def flush(self, timeout: float = 10.0) -> None:
|
|
44
|
+
"""Wait for all in-flight events to complete. Useful in tests and shutdown."""
|
|
45
|
+
with self._lock:
|
|
46
|
+
threads = list(self._pending_threads)
|
|
47
|
+
for t in threads:
|
|
48
|
+
t.join(timeout=timeout)
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._pending_threads = [t for t in self._pending_threads if t.is_alive()]
|
agentmetrics/sentinel.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from agentmetrics.http_client import HttpClient
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("agentmetrics")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Sentinel:
|
|
14
|
+
"""
|
|
15
|
+
Core tracking class. Use the module-level `sentinel` singleton.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
from agentmetrics import sentinel
|
|
19
|
+
sentinel.configure(api_key="am_...")
|
|
20
|
+
|
|
21
|
+
@sentinel.track(agent_id="my_agent")
|
|
22
|
+
def my_agent(task):
|
|
23
|
+
return result
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._api_key: Optional[str] = None
|
|
28
|
+
self._client: Optional[HttpClient] = None
|
|
29
|
+
|
|
30
|
+
def configure(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str,
|
|
33
|
+
base_url: str = "https://api.agentmetrics.dev/v1",
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Call once at app startup before using @track."""
|
|
36
|
+
self._api_key = api_key
|
|
37
|
+
self._client = HttpClient(api_key=api_key, base_url=base_url)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_configured(self) -> bool:
|
|
41
|
+
return self._api_key is not None
|
|
42
|
+
|
|
43
|
+
def track(self, agent_id: str):
|
|
44
|
+
"""
|
|
45
|
+
Decorator that wraps a function (sync or async) and sends
|
|
46
|
+
execution metrics to AgentMetrics. Graceful no-op if not configured.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
agent_id: Unique identifier for this agent in the dashboard.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
@sentinel.track(agent_id="customer_support")
|
|
53
|
+
def handle_ticket(ticket):
|
|
54
|
+
...
|
|
55
|
+
"""
|
|
56
|
+
def decorator(func):
|
|
57
|
+
if asyncio.iscoroutinefunction(func):
|
|
58
|
+
@functools.wraps(func)
|
|
59
|
+
async def async_wrapper(*args, **kwargs):
|
|
60
|
+
if not self.is_configured:
|
|
61
|
+
return await func(*args, **kwargs)
|
|
62
|
+
return await self._run_async(func, agent_id, args, kwargs)
|
|
63
|
+
return async_wrapper
|
|
64
|
+
else:
|
|
65
|
+
@functools.wraps(func)
|
|
66
|
+
def sync_wrapper(*args, **kwargs):
|
|
67
|
+
if not self.is_configured:
|
|
68
|
+
return func(*args, **kwargs)
|
|
69
|
+
return self._run_sync(func, agent_id, args, kwargs)
|
|
70
|
+
return sync_wrapper
|
|
71
|
+
return decorator
|
|
72
|
+
|
|
73
|
+
def _run_sync(self, func, agent_id: str, args, kwargs):
|
|
74
|
+
trace_id = str(uuid.uuid4())
|
|
75
|
+
start = time.monotonic()
|
|
76
|
+
status = "success"
|
|
77
|
+
error_msg = None
|
|
78
|
+
try:
|
|
79
|
+
result = func(*args, **kwargs)
|
|
80
|
+
return result
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
status = "failed"
|
|
83
|
+
error_msg = str(exc)
|
|
84
|
+
raise
|
|
85
|
+
finally:
|
|
86
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
87
|
+
self._send(trace_id, agent_id, status, duration_ms, error_msg)
|
|
88
|
+
|
|
89
|
+
async def _run_async(self, func, agent_id: str, args, kwargs):
|
|
90
|
+
trace_id = str(uuid.uuid4())
|
|
91
|
+
start = time.monotonic()
|
|
92
|
+
status = "success"
|
|
93
|
+
error_msg = None
|
|
94
|
+
try:
|
|
95
|
+
result = await func(*args, **kwargs)
|
|
96
|
+
return result
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
status = "failed"
|
|
99
|
+
error_msg = str(exc)
|
|
100
|
+
raise
|
|
101
|
+
finally:
|
|
102
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
103
|
+
self._send(trace_id, agent_id, status, duration_ms, error_msg)
|
|
104
|
+
|
|
105
|
+
def _send(
|
|
106
|
+
self,
|
|
107
|
+
trace_id: str,
|
|
108
|
+
agent_id: str,
|
|
109
|
+
status: str,
|
|
110
|
+
duration_ms: float,
|
|
111
|
+
error_msg: Optional[str],
|
|
112
|
+
cost_usd: float = 0.0,
|
|
113
|
+
model: Optional[str] = None,
|
|
114
|
+
input_tokens: Optional[int] = None,
|
|
115
|
+
output_tokens: Optional[int] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
if not self._client:
|
|
118
|
+
return
|
|
119
|
+
payload = {
|
|
120
|
+
"trace_id": trace_id,
|
|
121
|
+
"agent_id": agent_id,
|
|
122
|
+
"status": status,
|
|
123
|
+
"duration_ms": duration_ms,
|
|
124
|
+
"cost_usd": cost_usd,
|
|
125
|
+
"error": error_msg,
|
|
126
|
+
}
|
|
127
|
+
if model:
|
|
128
|
+
payload["model"] = model
|
|
129
|
+
if input_tokens is not None:
|
|
130
|
+
payload["input_tokens"] = input_tokens
|
|
131
|
+
if output_tokens is not None:
|
|
132
|
+
payload["output_tokens"] = output_tokens
|
|
133
|
+
|
|
134
|
+
self._client.fire_and_forget(payload)
|
|
135
|
+
|
|
136
|
+
def flush(self, timeout: float = 10.0) -> None:
|
|
137
|
+
"""Wait for all pending events to be sent. Call before process exit."""
|
|
138
|
+
if self._client:
|
|
139
|
+
self._client.flush(timeout=timeout)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Module-level singleton
|
|
143
|
+
sentinel = Sentinel()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentmetrics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time cost visibility & optimization for AI agents.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://agentmetrics.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/agentmetrics/agentmetrics
|
|
8
|
+
Project-URL: Documentation, https://agentmetrics.dev/docs
|
|
9
|
+
Keywords: ai,agents,observability,cost,langchain,crewai,langgraph
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.28
|
|
22
|
+
|
|
23
|
+
# AgentMetrics SDK
|
|
24
|
+
|
|
25
|
+
Real-time cost tracking for AI agents. One decorator. Full visibility.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install agentmetrics
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from agentmetrics import sentinel
|
|
37
|
+
|
|
38
|
+
# Configure once at startup
|
|
39
|
+
sentinel.configure(api_key="am_your_key_here")
|
|
40
|
+
|
|
41
|
+
# Decorate any agent function
|
|
42
|
+
@sentinel.track(agent_id="customer_support")
|
|
43
|
+
def my_agent(task: str) -> str:
|
|
44
|
+
# Your agent logic here
|
|
45
|
+
return result
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Async Support
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
@sentinel.track(agent_id="async_agent")
|
|
52
|
+
async def my_async_agent(task: str) -> str:
|
|
53
|
+
result = await some_llm_call(task)
|
|
54
|
+
return result
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## With LangGraph
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from agentmetrics import sentinel
|
|
61
|
+
sentinel.configure(api_key="am_your_key_here")
|
|
62
|
+
|
|
63
|
+
@sentinel.track(agent_id="langgraph_agent")
|
|
64
|
+
def run_graph(state: dict) -> dict:
|
|
65
|
+
return graph.invoke(state)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## With CrewAI
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from agentmetrics import sentinel
|
|
72
|
+
sentinel.configure(api_key="am_your_key_here")
|
|
73
|
+
|
|
74
|
+
@sentinel.track(agent_id="research_crew")
|
|
75
|
+
def run_crew(topic: str) -> str:
|
|
76
|
+
return crew.kickoff(inputs={"topic": topic})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Self-Hosted
|
|
80
|
+
|
|
81
|
+
Point the SDK at your own server:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
sentinel.configure(
|
|
85
|
+
api_key="am_your_key_here",
|
|
86
|
+
base_url="https://your-server.com/v1",
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Graceful Degradation
|
|
91
|
+
|
|
92
|
+
If the AgentMetrics server is unreachable, your agent keeps running normally.
|
|
93
|
+
The SDK never raises exceptions or blocks execution.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
Apache 2.0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agentmetrics/__init__.py,sha256=Ixy5U1E7P53-UgRPvLKkf71Tr0BgJ2Qlt6-_jFTEI14,111
|
|
2
|
+
agentmetrics/http_client.py,sha256=3MwaTciJ0UZUCJUYZGJlkC4HDNEpUyul9U2bikScjfg,1843
|
|
3
|
+
agentmetrics/sentinel.py,sha256=WKmJnlDlBoPdBZGvjnrFN5DHTvJZMPfSCvOcQrzAP1c,4386
|
|
4
|
+
agentmetrics-0.1.0.dist-info/METADATA,sha256=Oy1ITJqkKsUCjZA0fipnIRRH3VKeN54_uQZ7M9CWlzg,2346
|
|
5
|
+
agentmetrics-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
agentmetrics-0.1.0.dist-info/top_level.txt,sha256=526eHN7Tjt7n65SfgJVFw9U07QTDvn1iSnjq80Nm50A,13
|
|
7
|
+
agentmetrics-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentmetrics
|