agentmetrics-openai-agents 0.2.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,62 @@
1
+ # Generated — dashboard SPA copied here during server build
2
+ api/app/static/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.pyo
8
+ .venv/
9
+ .env
10
+ *.egg-info/
11
+ dist/
12
+ build/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .pytest_cache/
16
+ htmlcov/
17
+ .coverage
18
+ coverage.xml
19
+
20
+ # Node / JS
21
+ node_modules/
22
+ .next/
23
+ .turbo/
24
+ dist/
25
+ build/
26
+ *.tsbuildinfo
27
+ .pnpm-store/
28
+
29
+ # Env files
30
+ .env
31
+ .env.local
32
+ .env.production
33
+ .env.*.local
34
+ api/.env.local
35
+ dashboard/.env.local
36
+
37
+ # Build artifacts inside packages
38
+ packages/python/dist/
39
+ packages/python/*.egg-info/
40
+ packages/js/dist/
41
+ packages/js/node_modules/
42
+
43
+ # Internal docs — never public
44
+ .internal/
45
+ PLAN.md
46
+ CODE.md
47
+
48
+ # OS
49
+ .DS_Store
50
+ Thumbs.db
51
+
52
+ # IDE
53
+ .vscode/
54
+ .idea/
55
+ *.swp
56
+
57
+ # Docker
58
+ *.log
59
+ .internal
60
+
61
+ # Local data (SQLite DB when running without Docker)
62
+ data/
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentmetrics-openai-agents
3
+ Version: 0.2.0
4
+ Summary: AgentMetrics observability integration for OpenAI Agents SDK
5
+ Project-URL: Homepage, https://github.com/andalabx/agentmetrics
6
+ Project-URL: Repository, https://github.com/andalabx/agentmetrics
7
+ License: MIT
8
+ Keywords: agentmetrics,ai-agents,observability,openai,openai-agents
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: agentmetrics-shared>=0.2.0
11
+ Requires-Dist: agentmetrics>=0.2.0
12
+ Requires-Dist: openai-agents>=0.0.5
13
+ Description-Content-Type: text/markdown
14
+
15
+ # agentmetrics-openai-agents
16
+
17
+ [![PyPI](https://img.shields.io/pypi/v/agentmetrics-openai-agents?color=6366f1&label=pypi&logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-openai-agents)
18
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../../LICENSE)
19
+
20
+ AgentMetrics integration for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/). Register one trace processor at startup and every agent run reports back to your dashboard showing latency, cost, token counts, tool calls, and errors, with zero changes to your agent code.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install agentmetrics-openai-agents
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Quickstart
33
+
34
+ ```python
35
+ from agents.tracing import add_trace_processor
36
+ from agentmetrics_openai_agents import AgentMetricsProcessor
37
+
38
+ # register once at startup, covers all agents in the process
39
+ add_trace_processor(AgentMetricsProcessor(
40
+ agent_id="my-openai-agent",
41
+ base_url="http://localhost:8099",
42
+ ))
43
+
44
+ # Run your agents as normal
45
+ result = await Runner.run(my_agent, "Summarize this document")
46
+ ```
47
+
48
+ ---
49
+
50
+ ## API
51
+
52
+ ### `AgentMetricsProcessor(agent_id, base_url)`
53
+
54
+ | Parameter | Default | Description |
55
+ |---|---|---|
56
+ | `agent_id` | `"openai-agent"` | Fallback label if the trace has no `name` attribute |
57
+ | `base_url` | `"http://localhost:8099"` | AgentMetrics server address |
58
+
59
+ Implements `TracingProcessor` from `agents.tracing`. Pass to `add_trace_processor()` before running any agents.
60
+
61
+ ### `.force_flush()`
62
+
63
+ Blocks until all in-flight HTTP requests complete.
64
+
65
+ ### `.shutdown()`
66
+
67
+ Calls `force_flush()`. Called automatically when the process exits cleanly.
68
+
69
+ ---
70
+
71
+ ## What gets tracked
72
+
73
+ Each agent trace emits one event to `/v1/events` when `on_trace_end` fires:
74
+
75
+ | Field | Description |
76
+ |---|---|
77
+ | `status` | `success` or `failed` |
78
+ | `duration_ms` | Wall-clock trace duration |
79
+ | `input_tokens` / `output_tokens` | Aggregated from all `LLMSpanData` spans |
80
+ | `cache_read_tokens` / `cache_write_tokens` | Cache token counts |
81
+ | `llm_calls` | Number of LLM spans in the trace |
82
+ | `tool_calls` / `tool_errors` | Counts from `FunctionSpanData` spans |
83
+ | `tool_names` | Set of function/tool names |
84
+ | `model` | Model name from the first LLM span output |
85
+ | `estimated_cost_usd` | Computed from token counts and model pricing |
86
+ | `error` | First 500 chars of the trace error on failure |
87
+
88
+ ---
89
+
90
+ ## License
91
+
92
+ [MIT](../../LICENSE)
@@ -0,0 +1,78 @@
1
+ # agentmetrics-openai-agents
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/agentmetrics-openai-agents?color=6366f1&label=pypi&logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-openai-agents)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../../LICENSE)
5
+
6
+ AgentMetrics integration for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/). Register one trace processor at startup and every agent run reports back to your dashboard showing latency, cost, token counts, tool calls, and errors, with zero changes to your agent code.
7
+
8
+ ---
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install agentmetrics-openai-agents
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from agents.tracing import add_trace_processor
22
+ from agentmetrics_openai_agents import AgentMetricsProcessor
23
+
24
+ # register once at startup, covers all agents in the process
25
+ add_trace_processor(AgentMetricsProcessor(
26
+ agent_id="my-openai-agent",
27
+ base_url="http://localhost:8099",
28
+ ))
29
+
30
+ # Run your agents as normal
31
+ result = await Runner.run(my_agent, "Summarize this document")
32
+ ```
33
+
34
+ ---
35
+
36
+ ## API
37
+
38
+ ### `AgentMetricsProcessor(agent_id, base_url)`
39
+
40
+ | Parameter | Default | Description |
41
+ |---|---|---|
42
+ | `agent_id` | `"openai-agent"` | Fallback label if the trace has no `name` attribute |
43
+ | `base_url` | `"http://localhost:8099"` | AgentMetrics server address |
44
+
45
+ Implements `TracingProcessor` from `agents.tracing`. Pass to `add_trace_processor()` before running any agents.
46
+
47
+ ### `.force_flush()`
48
+
49
+ Blocks until all in-flight HTTP requests complete.
50
+
51
+ ### `.shutdown()`
52
+
53
+ Calls `force_flush()`. Called automatically when the process exits cleanly.
54
+
55
+ ---
56
+
57
+ ## What gets tracked
58
+
59
+ Each agent trace emits one event to `/v1/events` when `on_trace_end` fires:
60
+
61
+ | Field | Description |
62
+ |---|---|
63
+ | `status` | `success` or `failed` |
64
+ | `duration_ms` | Wall-clock trace duration |
65
+ | `input_tokens` / `output_tokens` | Aggregated from all `LLMSpanData` spans |
66
+ | `cache_read_tokens` / `cache_write_tokens` | Cache token counts |
67
+ | `llm_calls` | Number of LLM spans in the trace |
68
+ | `tool_calls` / `tool_errors` | Counts from `FunctionSpanData` spans |
69
+ | `tool_names` | Set of function/tool names |
70
+ | `model` | Model name from the first LLM span output |
71
+ | `estimated_cost_usd` | Computed from token counts and model pricing |
72
+ | `error` | First 500 chars of the trace error on failure |
73
+
74
+ ---
75
+
76
+ ## License
77
+
78
+ [MIT](../../LICENSE)
@@ -0,0 +1,27 @@
1
+ from agentmetrics_openai_agents.processor import AgentMetricsProcessor
2
+
3
+
4
+ def instrument(
5
+ api_key: str,
6
+ agent_id: str = "openai-agent",
7
+ base_url: str = "http://localhost:8099",
8
+ ) -> AgentMetricsProcessor:
9
+ """
10
+ Register AgentMetrics as a tracing processor for all OpenAI Agents SDK runs.
11
+
12
+ Usage::
13
+
14
+ from agentmetrics_openai_agents import instrument
15
+
16
+ instrument(api_key="am_...")
17
+ # All agent runs are now tracked automatically.
18
+ """
19
+ from agents.tracing import add_trace_processor
20
+
21
+ processor = AgentMetricsProcessor(api_key=api_key, agent_id=agent_id, base_url=base_url)
22
+ add_trace_processor(processor)
23
+ return processor
24
+
25
+
26
+ __version__ = "0.1.0"
27
+ __all__ = ["AgentMetricsProcessor", "instrument"]
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from typing import Any
6
+
7
+ from agentmetrics.http_client import HttpClient
8
+ from agentmetrics_shared import AgentEndEvent, estimate_cost
9
+ from agents.tracing import Span, Trace, TracingProcessor
10
+ from agents.tracing.spans import (
11
+ AgentSpanData,
12
+ FunctionSpanData,
13
+ HandoffSpanData,
14
+ LLMSpanData,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # INT-22: singleton guard — register the processor only once per process
20
+ _PROCESSOR_REGISTERED = False
21
+ _PROCESSOR_LOCK = __import__("threading").Lock()
22
+
23
+
24
+ # INT-20: helper to try both known cache token attribute names
25
+ def _get_cached_tokens(details: Any) -> int:
26
+ """Return cached token count, trying both known attribute/key names."""
27
+ if details is None:
28
+ return 0
29
+ if isinstance(details, dict):
30
+ return (
31
+ details.get("cached_tokens")
32
+ or details.get("cached_input_tokens")
33
+ or 0
34
+ )
35
+ return (
36
+ getattr(details, "cached_tokens", None)
37
+ or getattr(details, "cached_input_tokens", None)
38
+ or 0
39
+ )
40
+
41
+
42
+ class _TraceState:
43
+ __slots__ = (
44
+ "agent_id",
45
+ "cache_read_tokens",
46
+ "cache_write_tokens",
47
+ "error",
48
+ "input_tokens",
49
+ "llm_calls",
50
+ "model",
51
+ "output_tokens",
52
+ "start_ms",
53
+ "status",
54
+ "tool_calls",
55
+ "tool_errors",
56
+ "tool_names",
57
+ )
58
+
59
+ def __init__(self, agent_id: str) -> None:
60
+ self.agent_id = agent_id
61
+ self.start_ms = time.monotonic()
62
+ self.input_tokens = 0
63
+ self.output_tokens = 0
64
+ self.cache_read_tokens = 0
65
+ self.cache_write_tokens = 0
66
+ self.llm_calls = 0
67
+ self.tool_calls = 0
68
+ self.tool_errors = 0
69
+ self.tool_names: set[str] = set()
70
+ self.model: str | None = None
71
+ self.status = "success"
72
+ self.error: str | None = None
73
+
74
+
75
+ class AgentMetricsProcessor(TracingProcessor):
76
+ """
77
+ OpenAI Agents SDK tracing processor that sends run summaries to AgentMetrics.
78
+
79
+ Add once globally - covers all agents in the process.
80
+
81
+ Usage::
82
+
83
+ from agents.tracing import add_trace_processor
84
+ from agentmetrics_openai_agents import AgentMetricsProcessor
85
+
86
+ add_trace_processor(AgentMetricsProcessor(api_key="am_..."))
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ api_key: str,
92
+ agent_id: str = "openai-agent",
93
+ base_url: str = "http://localhost:8099",
94
+ ) -> None:
95
+ self._client = HttpClient(api_key=api_key, base_url=base_url)
96
+ self._agent_id = agent_id
97
+ # trace_id (str) → _TraceState
98
+ self._traces: dict[str, _TraceState] = {}
99
+
100
+
101
+ def on_trace_start(self, trace: Trace) -> None:
102
+ agent_name = getattr(trace, "name", None) or self._agent_id
103
+ self._traces[trace.trace_id] = _TraceState(agent_name)
104
+
105
+ def on_trace_end(self, trace: Trace) -> None:
106
+ state = self._traces.pop(trace.trace_id, None)
107
+ if state is None:
108
+ return
109
+ if getattr(trace, "error", None):
110
+ state.status = "failed"
111
+ state.error = str(trace.error)[:500]
112
+ self._emit(state, trace.trace_id)
113
+
114
+
115
+ def on_span_start(self, span: Span[Any]) -> None:
116
+ pass
117
+
118
+ def on_span_end(self, span: Span[Any]) -> None:
119
+ state = self._traces.get(span.trace_id)
120
+ if state is None:
121
+ return
122
+
123
+ data = span.span_data
124
+
125
+ if isinstance(data, LLMSpanData):
126
+ state.llm_calls += 1
127
+ usage = getattr(data, "usage", None) or {}
128
+ if hasattr(usage, "input_tokens"):
129
+ state.input_tokens += getattr(usage, "input_tokens", 0) or 0
130
+ state.output_tokens += getattr(usage, "output_tokens", 0) or 0
131
+ # INT-20: use helper to try both cached_tokens and cached_input_tokens
132
+ details = getattr(usage, "input_tokens_details", None) or {}
133
+ state.cache_read_tokens += _get_cached_tokens(details)
134
+ elif isinstance(usage, dict):
135
+ state.input_tokens += usage.get("input_tokens", 0) or usage.get("prompt_tokens", 0) or 0
136
+ state.output_tokens += usage.get("output_tokens", 0) or usage.get("completion_tokens", 0) or 0
137
+ # INT-20: also check dict path for cached tokens
138
+ details = usage.get("input_tokens_details") or {}
139
+ state.cache_read_tokens += _get_cached_tokens(details)
140
+ if not state.model:
141
+ resp = getattr(data, "output", None)
142
+ if resp:
143
+ state.model = getattr(resp, "model", None)
144
+
145
+ elif isinstance(data, FunctionSpanData):
146
+ state.tool_calls += 1
147
+ name = getattr(data, "name", None)
148
+ if name:
149
+ state.tool_names.add(name)
150
+ if getattr(span, "error", None):
151
+ state.tool_errors += 1
152
+
153
+ elif isinstance(data, HandoffSpanData):
154
+ state.tool_calls += 1
155
+
156
+ elif isinstance(data, AgentSpanData):
157
+ # agent-level errors bubble up to trace; nothing extra here
158
+ pass
159
+
160
+
161
+ def force_flush(self) -> None:
162
+ self._client.flush(timeout=10.0)
163
+
164
+ def shutdown(self) -> None:
165
+ self.force_flush()
166
+
167
+
168
+ def _emit(self, state: _TraceState, trace_id: str) -> None:
169
+ duration_ms = (time.monotonic() - state.start_ms) * 1000
170
+ ev = AgentEndEvent(agent_id=state.agent_id, platform="openai-agents")
171
+ ev.trace_id = trace_id
172
+ ev.input_tokens = state.input_tokens
173
+ ev.output_tokens = state.output_tokens
174
+ ev.cache_read_tokens = state.cache_read_tokens
175
+ ev.cache_write_tokens = state.cache_write_tokens
176
+ ev.llm_calls = state.llm_calls
177
+ ev.tool_calls = state.tool_calls
178
+ ev.tool_errors = state.tool_errors
179
+ ev.tool_names = list(state.tool_names)
180
+ ev.status = state.status
181
+ ev.duration_ms = round(duration_ms, 2)
182
+ ev.error = state.error
183
+ ev.model = state.model
184
+ ev.estimated_cost_usd = estimate_cost(
185
+ state.model or "", state.input_tokens, state.output_tokens,
186
+ state.cache_read_tokens, state.cache_write_tokens,
187
+ ) or None
188
+ self._client.fire_and_forget(ev.to_payload())
189
+
190
+
191
+ def register(
192
+ api_key: str,
193
+ agent_id: str = "openai-agent",
194
+ base_url: str = "http://localhost:8099",
195
+ ) -> None:
196
+ """
197
+ INT-22: Register the AgentMetrics processor with a singleton guard so it is
198
+ added at most once per process, even if called multiple times.
199
+ """
200
+ global _PROCESSOR_REGISTERED
201
+ with _PROCESSOR_LOCK:
202
+ if _PROCESSOR_REGISTERED:
203
+ logger.debug(
204
+ "agentmetrics: OpenAI Agents processor already registered, skipping"
205
+ )
206
+ return
207
+ try:
208
+ from agents import add_trace_processor
209
+ add_trace_processor(AgentMetricsProcessor(
210
+ api_key=api_key, agent_id=agent_id, base_url=base_url
211
+ ))
212
+ _PROCESSOR_REGISTERED = True
213
+ except (ImportError, AttributeError) as exc:
214
+ logger.debug(
215
+ "agentmetrics: could not register OpenAI Agents processor: %s", exc
216
+ )
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentmetrics-openai-agents"
7
+ version = "0.2.0"
8
+ description = "AgentMetrics observability integration for OpenAI Agents SDK"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["openai", "openai-agents", "agentmetrics", "observability", "ai-agents"]
13
+ dependencies = [
14
+ "agentmetrics>=0.2.0",
15
+ "agentmetrics-shared>=0.2.0",
16
+ "openai-agents>=0.0.5",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/andalabx/agentmetrics"
21
+ Repository = "https://github.com/andalabx/agentmetrics"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["agentmetrics_openai_agents"]
25
+
26
+ [tool.uv.sources]
27
+ agentmetrics = { workspace = true }
28
+ agentmetrics-shared = { workspace = true }