agentx-python 0.4.13__py3-none-any.whl → 0.4.14__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.
@@ -11,22 +11,111 @@ Usage::
11
11
  from agents import add_trace_processor
12
12
  add_trace_processor(processor)
13
13
 
14
- Requires: ``pip install agentx[openai-agents]``
14
+ Requires: ``pip install "agentx-python[openai-agents]"``
15
15
  """
16
16
  from __future__ import annotations
17
17
 
18
18
  import time
19
- from typing import Any, Dict, Optional
19
+ from datetime import datetime, timezone
20
+ from typing import Any, Dict, List, Optional
20
21
 
21
22
  from agentx.tracing.tracer import Tracer, _safe_serialize
22
23
 
23
24
 
25
+ def _iso_to_ts(iso: Optional[str]) -> Optional[float]:
26
+ """Parse an ISO-8601 timestamp string to a Unix timestamp float."""
27
+ if not iso:
28
+ return None
29
+ try:
30
+ return datetime.fromisoformat(iso.replace("Z", "+00:00")).timestamp()
31
+ except Exception:
32
+ return None
33
+
34
+
35
+ def _span_latency_ms(span: Any) -> Optional[int]:
36
+ """Return span duration in ms from ISO started_at / ended_at strings."""
37
+ t0 = _iso_to_ts(getattr(span, "started_at", None))
38
+ t1 = _iso_to_ts(getattr(span, "ended_at", None))
39
+ if t0 is not None and t1 is not None:
40
+ return max(0, int((t1 - t0) * 1000))
41
+ return None
42
+
43
+
44
+ def _extract_text(output: Any) -> Optional[str]:
45
+ """
46
+ Best-effort extraction of a human-readable string from a GenerationSpanData
47
+ or ResponseSpanData output, which can be a list of message dicts in either
48
+ the Chat Completions or Responses API format.
49
+ """
50
+ if output is None:
51
+ return None
52
+ if isinstance(output, str):
53
+ return output
54
+ if not isinstance(output, (list, tuple)):
55
+ return _safe_serialize(output)
56
+
57
+ for item in reversed(output):
58
+ if not isinstance(item, dict):
59
+ # Could be a pydantic model — try .text or .content
60
+ text = getattr(item, "text", None) or getattr(item, "content", None)
61
+ if text and isinstance(text, str):
62
+ return text
63
+ continue
64
+
65
+ # Responses API: {"type": "message", "content": [{"type": "output_text", "text": "..."}]}
66
+ if item.get("type") == "message":
67
+ content = item.get("content", [])
68
+ if isinstance(content, list):
69
+ for part in content:
70
+ if isinstance(part, dict) and part.get("type") == "output_text":
71
+ return part.get("text")
72
+
73
+ # Chat Completions API: {"role": "assistant", "content": "..."}
74
+ role = item.get("role", "")
75
+ if role == "assistant":
76
+ content = item.get("content")
77
+ if isinstance(content, str):
78
+ return content
79
+ if isinstance(content, list):
80
+ for part in content:
81
+ if isinstance(part, dict) and part.get("type") == "text":
82
+ return part.get("text")
83
+
84
+ return _safe_serialize(output)
85
+
86
+
87
+ def _extract_input_text(input_data: Any) -> Optional[str]:
88
+ """Extract the user's input query from a generation span's input messages."""
89
+ if input_data is None:
90
+ return None
91
+ if isinstance(input_data, str):
92
+ return input_data
93
+ if not isinstance(input_data, (list, tuple)):
94
+ return _safe_serialize(input_data)
95
+
96
+ # Walk messages and grab the last user message content
97
+ user_text = None
98
+ for item in input_data:
99
+ if not isinstance(item, dict):
100
+ continue
101
+ if item.get("role") == "user":
102
+ content = item.get("content")
103
+ if isinstance(content, str):
104
+ user_text = content
105
+ elif isinstance(content, list):
106
+ for part in content:
107
+ if isinstance(part, dict) and part.get("type") in ("text", "input_text"):
108
+ user_text = part.get("text")
109
+
110
+ return user_text or _safe_serialize(input_data)
111
+
112
+
24
113
  class AgentXTracingProcessor:
25
114
  """
26
115
  Implements the ``TracingProcessor`` interface expected by the OpenAI Agents SDK
27
116
  (``agents.add_trace_processor``).
28
117
 
29
- Sends one AgentX trace per top-level agent run span.
118
+ Sends one AgentX trace per top-level agent run.
30
119
  """
31
120
 
32
121
  def __init__(
@@ -38,7 +127,7 @@ class AgentXTracingProcessor:
38
127
  self._tracer = tracer
39
128
  self._metadata = metadata
40
129
  self._session_id = session_id
41
- # span_id → state for in-flight agent spans
130
+ # trace_idaccumulated state
42
131
  self._spans: Dict[str, Dict[str, Any]] = {}
43
132
 
44
133
  # ------------------------------------------------------------------
@@ -46,18 +135,17 @@ class AgentXTracingProcessor:
46
135
  # ------------------------------------------------------------------
47
136
 
48
137
  def on_trace_start(self, trace: Any) -> None:
49
- """Called when a new trace (top-level run) begins."""
50
138
  trace_id = getattr(trace, "trace_id", None) or str(id(trace))
51
139
  self._spans[trace_id] = {
52
140
  "start": time.time(),
53
141
  "name": getattr(trace, "name", "openai-agent"),
54
142
  "input": None,
143
+ "output": None,
55
144
  "tool_calls": [],
56
145
  "model": None,
57
146
  }
58
147
 
59
148
  def on_trace_end(self, trace: Any) -> None:
60
- """Called when the trace ends (agent run complete)."""
61
149
  trace_id = getattr(trace, "trace_id", None) or str(id(trace))
62
150
  state = self._spans.pop(trace_id, None)
63
151
  if state is None:
@@ -66,7 +154,7 @@ class AgentXTracingProcessor:
66
154
  self._tracer._send(
67
155
  name=state["name"],
68
156
  input=state.get("input"),
69
- output=_safe_serialize(getattr(trace, "output", None)),
157
+ output=state.get("output"),
70
158
  latency_ms=latency_ms,
71
159
  framework="openai-agents",
72
160
  model=state.get("model"),
@@ -76,47 +164,58 @@ class AgentXTracingProcessor:
76
164
  )
77
165
 
78
166
  def on_span_start(self, span: Any) -> None:
79
- """Called for each nested span (LLM call, tool call, etc.)."""
80
- span_type = getattr(span, "span_data", None)
81
- if span_type is None:
82
- return
83
-
84
- # Capture model from LLM generation spans
85
- if hasattr(span_type, "model"):
86
- trace_id = getattr(span, "trace_id", None)
87
- if trace_id and trace_id in self._spans and not self._spans[trace_id].get("model"):
88
- self._spans[trace_id]["model"] = str(span_type.model)
89
-
90
- # Capture input on the root span
91
- if hasattr(span_type, "input") and span_type.input:
92
- trace_id = getattr(span, "trace_id", None)
93
- if trace_id and trace_id in self._spans and not self._spans[trace_id].get("input"):
94
- self._spans[trace_id]["input"] = _safe_serialize(span_type.input)
167
+ pass # All data is captured in on_span_end when fields are fully populated
95
168
 
96
169
  def on_span_end(self, span: Any) -> None:
97
- """Called when a nested span ends; captures tool-call results."""
98
170
  span_data = getattr(span, "span_data", None)
99
171
  if span_data is None:
100
172
  return
101
173
 
102
- # Tool / function calls
103
- if hasattr(span_data, "tool_name") or hasattr(span_data, "name"):
104
- trace_id = getattr(span, "trace_id", None)
105
- if trace_id and trace_id in self._spans:
106
- tool_entry: Dict[str, Any] = {
107
- "name": getattr(span_data, "tool_name", None)
108
- or getattr(span_data, "name", "tool"),
109
- "input": _safe_serialize(getattr(span_data, "input", None)),
110
- "output": str(getattr(span_data, "output", ""))[:500],
111
- }
112
- if hasattr(span, "started_at") and hasattr(span, "ended_at"):
113
- try:
114
- tool_entry["latency_ms"] = int(
115
- (span.ended_at - span.started_at) * 1000
116
- )
117
- except Exception:
118
- pass
119
- self._spans[trace_id]["tool_calls"].append(tool_entry)
174
+ trace_id = getattr(span, "trace_id", None)
175
+ if not trace_id or trace_id not in self._spans:
176
+ return
177
+
178
+ state = self._spans[trace_id]
179
+ span_type = getattr(span_data, "type", None)
180
+
181
+ if span_type == "generation":
182
+ # Capture input from the first generation span
183
+ if state["input"] is None and span_data.input:
184
+ state["input"] = _extract_input_text(span_data.input)
185
+ # Always update output to the latest generation (last one wins = final reply)
186
+ if span_data.output:
187
+ state["output"] = _extract_text(span_data.output)
188
+ # Capture model name
189
+ if not state["model"] and span_data.model:
190
+ state["model"] = str(span_data.model)
191
+
192
+ elif span_type == "response":
193
+ # Responses API path — extract from the response object
194
+ response = getattr(span_data, "response", None)
195
+ if response is not None:
196
+ if state["input"] is None:
197
+ raw_input = getattr(span_data, "input", None)
198
+ if raw_input:
199
+ state["input"] = _extract_input_text(raw_input) if isinstance(raw_input, list) else str(raw_input)
200
+ output_items = getattr(response, "output", None)
201
+ if output_items:
202
+ state["output"] = _extract_text(output_items)
203
+ if not state["model"]:
204
+ model = getattr(response, "model", None)
205
+ if model:
206
+ state["model"] = str(model)
207
+
208
+ elif span_type == "function":
209
+ # Tool / function call
210
+ tool_entry: Dict[str, Any] = {
211
+ "name": span_data.name,
212
+ "input": span_data.input,
213
+ "output": str(span_data.output)[:500] if span_data.output is not None else None,
214
+ }
215
+ latency = _span_latency_ms(span)
216
+ if latency is not None:
217
+ tool_entry["latency_ms"] = latency
218
+ state["tool_calls"].append(tool_entry)
120
219
 
121
220
  def force_flush(self) -> None:
122
221
  self._tracer.flush()
agentx/version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "0.4.13"
1
+ VERSION = "0.4.14"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.4.13
3
+ Version: 0.4.14
4
4
  Summary: Official Python SDK for AgentX (https://www.agentx.so/)
5
5
  Home-page: https://github.com/AgentX-ai/AgentX-python
6
6
  Author: Robin Wang and AgentX Team
@@ -2,7 +2,7 @@ agentx/__init__.py,sha256=4iMiGLNU4S-I-WyX0Z7l0BFUY_krVJ3seMQPqyO-ReA,602
2
2
  agentx/agentx.py,sha256=tw3CUnkvTM1eopoO7u8TgKnAz9t0HNSxrVcjIJsytAg,3469
3
3
  agentx/exceptions.py,sha256=TKooc1_pQ2P_4DqpLfZx_ap-6cfrjFLW8JS2qaGx_1M,1237
4
4
  agentx/util.py,sha256=yOV3VVdc0KUbw5pEkSRiO2lZzEtL1vOsnB_f5tFk948,665
5
- agentx/version.py,sha256=dO83YWKEtJW3A7sv1BcvYqTJgQZCIf7sMXiONZFOY_Q,19
5
+ agentx/version.py,sha256=_pWcb0KD7RwUQ8uiQjs-gUiG1ej2mVAyGLgZjVwFiUc,19
6
6
  agentx/evaluations/__init__.py,sha256=OmdCHiBO3Qcsck0UkOyVqSvfRs6nXbSi-2FXzyRfsog,89
7
7
  agentx/evaluations/_term.py,sha256=S4blZgH66wHvkt2hR29h8JE5xnakBeD7dlZdUW9Wfoo,2065
8
8
  agentx/evaluations/client.py,sha256=3mC0uSBRk9uO4F3a7kIMM_UUsyypF99_97E5MSpYFzM,7736
@@ -21,7 +21,7 @@ agentx/integrations/__init__.py,sha256=ZKOwYsvHXo9ipJUv5rPF1DOA0H5Q5YBG0WRI1f0lv
21
21
  agentx/integrations/anthropic.py,sha256=VzRdeICYm_zfgNBy6PEu6bYSUa9iKuSoRX6HSivWQpM,4802
22
22
  agentx/integrations/crewai.py,sha256=o7tgFE1I2T7fxOc2lG7aY1nAAwihejV3y-EJII6z1j0,3488
23
23
  agentx/integrations/langchain.py,sha256=_W62CUnzqlzSgIcNim0xDGXc3WsPoWYLQSHpTD4Xp5E,7844
24
- agentx/integrations/openai_agents.py,sha256=XDN_tNfx1ztLUlwNHgFk6QSiYbUDLo-HPljrHjZoVWk,4668
24
+ agentx/integrations/openai_agents.py,sha256=JscJELxs8sTj3toi9ixY5TOpH-lCVBtpgxYgahIaezE,8162
25
25
  agentx/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  agentx/resources/agent.py,sha256=ZKpxDYzJbKgOX4-W86U__b4gko7XlbwB4q6L8Az9uo0,2011
27
27
  agentx/resources/conversation.py,sha256=HK7gEGK6qIUz0KvG-ItUip08otWZdkP5S6qHiznvgZM,4443
@@ -30,8 +30,8 @@ agentx/tracing/__init__.py,sha256=l2mIRILE-wWrCD0fekgrIOQUEvtDObzdrkgpQUtXroU,40
30
30
  agentx/tracing/ci_types.py,sha256=zFVcZGvc1qbVDdOlEPQ1i5R6terqJHxCzFF4ZjouxxI,1423
31
31
  agentx/tracing/ingest_client.py,sha256=8aLabvWecgZsn0pkajZkI1it2TOk5boP0kC3o6TO52o,11487
32
32
  agentx/tracing/tracer.py,sha256=nJILoeQg6HV4SR05xJPy6EiSiZCpkp4yFBTrN1BO_1w,16951
33
- agentx_python-0.4.13.dist-info/licenses/LICENSE,sha256=6ZbiPNFmv3xBb44LGhAa3PZYK0ROAztsd5LRFZDlGFE,1074
34
- agentx_python-0.4.13.dist-info/METADATA,sha256=nDiVYt09aeyL89w3-ro9WfjsD7abgG0DdyujwsOdC5Q,6630
35
- agentx_python-0.4.13.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
36
- agentx_python-0.4.13.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
37
- agentx_python-0.4.13.dist-info/RECORD,,
33
+ agentx_python-0.4.14.dist-info/licenses/LICENSE,sha256=6ZbiPNFmv3xBb44LGhAa3PZYK0ROAztsd5LRFZDlGFE,1074
34
+ agentx_python-0.4.14.dist-info/METADATA,sha256=b2VooejMy2en_rD9tGJhcHLdrzmuC2Tx5gjfSEKYsi8,6630
35
+ agentx_python-0.4.14.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
36
+ agentx_python-0.4.14.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
37
+ agentx_python-0.4.14.dist-info/RECORD,,