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.
@@ -0,0 +1,4 @@
1
+ from agentmetrics.sentinel import Sentinel, sentinel
2
+
3
+ __all__ = ["sentinel", "Sentinel"]
4
+ __version__ = "0.1.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,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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ agentmetrics