agentmetrics 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.
- agentmetrics-0.1.0/PKG-INFO +97 -0
- agentmetrics-0.1.0/README.md +75 -0
- agentmetrics-0.1.0/agentmetrics/__init__.py +4 -0
- agentmetrics-0.1.0/agentmetrics/http_client.py +50 -0
- agentmetrics-0.1.0/agentmetrics/sentinel.py +143 -0
- agentmetrics-0.1.0/agentmetrics.egg-info/PKG-INFO +97 -0
- agentmetrics-0.1.0/agentmetrics.egg-info/SOURCES.txt +11 -0
- agentmetrics-0.1.0/agentmetrics.egg-info/dependency_links.txt +1 -0
- agentmetrics-0.1.0/agentmetrics.egg-info/requires.txt +1 -0
- agentmetrics-0.1.0/agentmetrics.egg-info/top_level.txt +1 -0
- agentmetrics-0.1.0/pyproject.toml +33 -0
- agentmetrics-0.1.0/setup.cfg +4 -0
- agentmetrics-0.1.0/tests/test_sentinel.py +81 -0
|
@@ -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,75 @@
|
|
|
1
|
+
# AgentMetrics SDK
|
|
2
|
+
|
|
3
|
+
Real-time cost tracking for AI agents. One decorator. Full visibility.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentmetrics
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from agentmetrics import sentinel
|
|
15
|
+
|
|
16
|
+
# Configure once at startup
|
|
17
|
+
sentinel.configure(api_key="am_your_key_here")
|
|
18
|
+
|
|
19
|
+
# Decorate any agent function
|
|
20
|
+
@sentinel.track(agent_id="customer_support")
|
|
21
|
+
def my_agent(task: str) -> str:
|
|
22
|
+
# Your agent logic here
|
|
23
|
+
return result
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Async Support
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
@sentinel.track(agent_id="async_agent")
|
|
30
|
+
async def my_async_agent(task: str) -> str:
|
|
31
|
+
result = await some_llm_call(task)
|
|
32
|
+
return result
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## With LangGraph
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from agentmetrics import sentinel
|
|
39
|
+
sentinel.configure(api_key="am_your_key_here")
|
|
40
|
+
|
|
41
|
+
@sentinel.track(agent_id="langgraph_agent")
|
|
42
|
+
def run_graph(state: dict) -> dict:
|
|
43
|
+
return graph.invoke(state)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## With CrewAI
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from agentmetrics import sentinel
|
|
50
|
+
sentinel.configure(api_key="am_your_key_here")
|
|
51
|
+
|
|
52
|
+
@sentinel.track(agent_id="research_crew")
|
|
53
|
+
def run_crew(topic: str) -> str:
|
|
54
|
+
return crew.kickoff(inputs={"topic": topic})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Self-Hosted
|
|
58
|
+
|
|
59
|
+
Point the SDK at your own server:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
sentinel.configure(
|
|
63
|
+
api_key="am_your_key_here",
|
|
64
|
+
base_url="https://your-server.com/v1",
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Graceful Degradation
|
|
69
|
+
|
|
70
|
+
If the AgentMetrics server is unreachable, your agent keeps running normally.
|
|
71
|
+
The SDK never raises exceptions or blocks execution.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Apache 2.0
|
|
@@ -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()]
|
|
@@ -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,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
agentmetrics/__init__.py
|
|
4
|
+
agentmetrics/http_client.py
|
|
5
|
+
agentmetrics/sentinel.py
|
|
6
|
+
agentmetrics.egg-info/PKG-INFO
|
|
7
|
+
agentmetrics.egg-info/SOURCES.txt
|
|
8
|
+
agentmetrics.egg-info/dependency_links.txt
|
|
9
|
+
agentmetrics.egg-info/requires.txt
|
|
10
|
+
agentmetrics.egg-info/top_level.txt
|
|
11
|
+
tests/test_sentinel.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentmetrics
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentmetrics"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Real-time cost visibility & optimization for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = ["requests>=2.28"]
|
|
13
|
+
keywords = ["ai", "agents", "observability", "cost", "langchain", "crewai", "langgraph"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://agentmetrics.dev"
|
|
28
|
+
Repository = "https://github.com/agentmetrics/agentmetrics"
|
|
29
|
+
Documentation = "https://agentmetrics.dev/docs"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|
|
33
|
+
include = ["agentmetrics*"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from agentmetrics import sentinel, Sentinel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_function():
|
|
7
|
+
sentinel._api_key = None
|
|
8
|
+
sentinel._client = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_no_op_when_not_configured():
|
|
12
|
+
"""Agent runs normally when sentinel is not configured."""
|
|
13
|
+
@sentinel.track(agent_id="test")
|
|
14
|
+
def my_agent():
|
|
15
|
+
return "result"
|
|
16
|
+
|
|
17
|
+
assert my_agent() == "result"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_tracks_success():
|
|
21
|
+
sentinel.configure(api_key="test_key", base_url="http://localhost:8000/v1")
|
|
22
|
+
|
|
23
|
+
sent = []
|
|
24
|
+
sentinel._client.fire_and_forget = lambda payload: sent.append(payload)
|
|
25
|
+
|
|
26
|
+
@sentinel.track(agent_id="test_agent")
|
|
27
|
+
def my_agent():
|
|
28
|
+
return "ok"
|
|
29
|
+
|
|
30
|
+
result = my_agent()
|
|
31
|
+
assert result == "ok"
|
|
32
|
+
assert len(sent) == 1
|
|
33
|
+
assert sent[0]["status"] == "success"
|
|
34
|
+
assert sent[0]["agent_id"] == "test_agent"
|
|
35
|
+
assert sent[0]["duration_ms"] >= 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tracks_failure():
|
|
39
|
+
sentinel.configure(api_key="test_key", base_url="http://localhost:8000/v1")
|
|
40
|
+
|
|
41
|
+
sent = []
|
|
42
|
+
sentinel._client.fire_and_forget = lambda payload: sent.append(payload)
|
|
43
|
+
|
|
44
|
+
@sentinel.track(agent_id="failing_agent")
|
|
45
|
+
def my_agent():
|
|
46
|
+
raise ValueError("something broke")
|
|
47
|
+
|
|
48
|
+
with pytest.raises(ValueError):
|
|
49
|
+
my_agent()
|
|
50
|
+
|
|
51
|
+
assert len(sent) == 1
|
|
52
|
+
assert sent[0]["status"] == "failed"
|
|
53
|
+
assert "something broke" in sent[0]["error"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_async_agent():
|
|
58
|
+
sentinel.configure(api_key="test_key", base_url="http://localhost:8000/v1")
|
|
59
|
+
|
|
60
|
+
sent = []
|
|
61
|
+
sentinel._client.fire_and_forget = lambda payload: sent.append(payload)
|
|
62
|
+
|
|
63
|
+
@sentinel.track(agent_id="async_agent")
|
|
64
|
+
async def my_async_agent():
|
|
65
|
+
return "async_result"
|
|
66
|
+
|
|
67
|
+
result = await my_async_agent()
|
|
68
|
+
assert result == "async_result"
|
|
69
|
+
assert sent[0]["status"] == "success"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_never_raises_on_send_failure():
|
|
73
|
+
sentinel.configure(api_key="test_key", base_url="http://unreachable:9999/v1")
|
|
74
|
+
|
|
75
|
+
@sentinel.track(agent_id="test")
|
|
76
|
+
def my_agent():
|
|
77
|
+
return "fine"
|
|
78
|
+
|
|
79
|
+
# Should not raise even though server is unreachable
|
|
80
|
+
result = my_agent()
|
|
81
|
+
assert result == "fine"
|