agentx-python 0.6.19__tar.gz → 0.6.21__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.
Files changed (70) hide show
  1. {agentx_python-0.6.19 → agentx_python-0.6.21}/PKG-INFO +1 -1
  2. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/__init__.py +1 -0
  3. agentx_python-0.6.21/agentx/integrations/moveworks.py +525 -0
  4. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/tracing/tracer.py +11 -6
  5. agentx_python-0.6.21/agentx/version.py +1 -0
  6. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx_python.egg-info/PKG-INFO +1 -1
  7. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx_python.egg-info/SOURCES.txt +1 -0
  8. agentx_python-0.6.21/agentx_python.egg-info/entry_points.txt +3 -0
  9. {agentx_python-0.6.19 → agentx_python-0.6.21}/setup.py +6 -1
  10. agentx_python-0.6.19/agentx/version.py +0 -1
  11. agentx_python-0.6.19/agentx_python.egg-info/entry_points.txt +0 -2
  12. {agentx_python-0.6.19 → agentx_python-0.6.21}/LICENSE +0 -0
  13. {agentx_python-0.6.19 → agentx_python-0.6.21}/README.md +0 -0
  14. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/__init__.py +0 -0
  15. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/agentx.py +0 -0
  16. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/cli.py +0 -0
  17. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/__init__.py +0 -0
  18. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/_term.py +0 -0
  19. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/adapters/__init__.py +0 -0
  20. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  21. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/adapters/precomputed.py +0 -0
  22. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/adapters/raw.py +0 -0
  23. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/client.py +0 -0
  24. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/datasets.py +0 -0
  25. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/evaluation_settings.py +0 -0
  26. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/models.py +0 -0
  27. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/prompts.py +0 -0
  28. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/redaction.py +0 -0
  29. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/reporting.py +0 -0
  30. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/results.py +0 -0
  31. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/runner.py +0 -0
  32. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/evaluations/tracing.py +0 -0
  33. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/exceptions.py +0 -0
  34. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/feedback.py +0 -0
  35. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/_traced_call.py +0 -0
  36. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/anthropic.py +0 -0
  37. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/autogen.py +0 -0
  38. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/crewai.py +0 -0
  39. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/google_adk.py +0 -0
  40. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/google_genai.py +0 -0
  41. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/langchain.py +0 -0
  42. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/litellm.py +0 -0
  43. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/llamaindex.py +0 -0
  44. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/openai.py +0 -0
  45. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/integrations/openai_agents.py +0 -0
  46. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/__init__.py +0 -0
  47. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/client.py +0 -0
  48. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/models.py +0 -0
  49. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/online_evaluators.py +0 -0
  50. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/patterns.py +0 -0
  51. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/profile.py +0 -0
  52. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/monitor/signals.py +0 -0
  53. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/outcomes.py +0 -0
  54. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/py.typed +0 -0
  55. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/resources/__init__.py +0 -0
  56. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/resources/agent.py +0 -0
  57. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/resources/conversation.py +0 -0
  58. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/resources/workforce.py +0 -0
  59. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/tracing/__init__.py +0 -0
  60. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/tracing/ci_types.py +0 -0
  61. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/tracing/ingest_client.py +0 -0
  62. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx/util.py +0 -0
  63. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx_python.egg-info/dependency_links.txt +0 -0
  64. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx_python.egg-info/not-zip-safe +0 -0
  65. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx_python.egg-info/requires.txt +0 -0
  66. {agentx_python-0.6.19 → agentx_python-0.6.21}/agentx_python.egg-info/top_level.txt +0 -0
  67. {agentx_python-0.6.19 → agentx_python-0.6.21}/setup.cfg +0 -0
  68. {agentx_python-0.6.19 → agentx_python-0.6.21}/tests/test_integration.py +0 -0
  69. {agentx_python-0.6.19 → agentx_python-0.6.21}/tests/test_integrations.py +0 -0
  70. {agentx_python-0.6.19 → agentx_python-0.6.21}/tests/test_span_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.19
3
+ Version: 0.6.21
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
@@ -7,3 +7,4 @@
7
7
  # from agentx.integrations.anthropic import patch_anthropic_client
8
8
  # from agentx.integrations.google_adk import AgentXADKPlugin
9
9
  # from agentx.integrations.google_genai import patch_genai_client
10
+ # from agentx.integrations.moveworks import MoveworksImporter # Data API pull sync, not in-process
@@ -0,0 +1,525 @@
1
+ """
2
+ Moveworks integration for AgentX production tracing.
3
+
4
+ Moveworks agents run inside the Moveworks cloud (Agent Studio + Reasoning Engine), so unlike the
5
+ LangChain/CrewAI integrations there is no in-process callback to hook: Moveworks Python Script
6
+ Actions have outbound network access disabled at the infrastructure level. The sanctioned export
7
+ surface is the read-only Moveworks **Data API** (OData, ``https://api.moveworks.ai/export/v1``),
8
+ which exposes ``conversations``, ``interactions``, ``plugin-calls``, ``plugin-resources`` and
9
+ ``users`` tables. This module pulls those records and replays them into AgentX as traces:
10
+
11
+ - conversation -> AgentX session (``session_id = mw_<conversation_id>``)
12
+ - interaction -> one AgentX trace (input/output, timestamps, latency where derivable)
13
+ - plugin call -> ``tool_calls`` entries on its interaction's trace
14
+
15
+ Usage::
16
+
17
+ from agentx.integrations.moveworks import MoveworksDataAPIClient, MoveworksImporter
18
+
19
+ client = MoveworksDataAPIClient(api_key="mw-data-api-key")
20
+ importer = MoveworksImporter(
21
+ client,
22
+ agentx_api_key="agtx_local_...",
23
+ agentx_base_url="http://localhost:4700/api/v1", # self-host engine
24
+ )
25
+ report = importer.sync(since=datetime.now(timezone.utc) - timedelta(days=1))
26
+ print(report)
27
+
28
+ Or from a shell / cron job (keeps an incremental cursor between runs)::
29
+
30
+ export MOVEWORKS_API_KEY=... AGENTX_API_KEY=... AGENTX_API_BASE_URL=http://localhost:4700/api/v1
31
+ agentx-moveworks sync --since 7d
32
+
33
+ Notes and limits:
34
+
35
+ - The Data API retains 30 days of history and follows a ~24-hour freshness SLA - this is
36
+ near-real-time observability, not live tracing.
37
+ - Re-syncing a window is safe: every trace carries a deterministic ``span_id``
38
+ (``mw:<interaction_id>``) and the engine skips spans it has already ingested.
39
+ - Moveworks does not export token counts, so these traces do not contribute to the LLM cost
40
+ chart; plugin calls carry served/used flags but no failure semantics, so no tool failures are
41
+ fabricated.
42
+ - Data API credentials are minted by a Moveworks superadmin (Setup -> Credentials). Field names
43
+ in Data API records vary between deployments/versions; the importer reads each field from a
44
+ list of known candidates and tolerates absences.
45
+ """
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ import json
50
+ import os
51
+ import sys
52
+ from datetime import datetime, timedelta, timezone
53
+ from pathlib import Path
54
+ from typing import Any, Dict, Iterator, List, Optional
55
+
56
+ import requests
57
+
58
+ from agentx.tracing.ingest_client import IngestClient
59
+ from agentx.version import VERSION
60
+
61
+ _DEFAULT_BASE_URL = "https://api.moveworks.ai/export/v1"
62
+ _DEFAULT_TIMESTAMP_FIELD = "created_time"
63
+ _DEFAULT_CURSOR_FILE = Path.home() / ".agentx" / "moveworks_cursor.json"
64
+
65
+
66
+ # ----------------------------------------------------------------------------------------------
67
+ # Defensive record readers - Data API field names vary between deployments/versions.
68
+ # ----------------------------------------------------------------------------------------------
69
+
70
+ def _first(record: Dict[str, Any], *keys: str) -> Any:
71
+ for key in keys:
72
+ value = record.get(key)
73
+ if value not in (None, ""):
74
+ return value
75
+ return None
76
+
77
+
78
+ def _parse_time(value: Any) -> Optional[datetime]:
79
+ """ISO-8601 strings and epoch seconds/millis, to aware UTC datetimes."""
80
+ if value is None:
81
+ return None
82
+ if isinstance(value, (int, float)):
83
+ # Millisecond epochs are > year 33658 when read as seconds; use magnitude to decide.
84
+ seconds = value / 1000.0 if value > 1e11 else float(value)
85
+ return datetime.fromtimestamp(seconds, tz=timezone.utc)
86
+ if isinstance(value, str):
87
+ text = value.strip().replace("Z", "+00:00")
88
+ try:
89
+ parsed = datetime.fromisoformat(text)
90
+ except ValueError:
91
+ return None
92
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
93
+ return None
94
+
95
+
96
+ def _record_time(record: Dict[str, Any]) -> Optional[datetime]:
97
+ return _parse_time(
98
+ _first(record, "created_time", "created_at", "timestamp", "event_time", "time", "start_time")
99
+ )
100
+
101
+
102
+ class MoveworksDataAPIClient:
103
+ """
104
+ Minimal client for the Moveworks Data API (OData, GET-only).
105
+
106
+ ``api_key`` is sent as ``Authorization: Bearer <key>`` by default; override ``auth_header`` /
107
+ ``auth_scheme`` if your deployment differs. Pagination follows ``odata.nextlink``.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ api_key: str,
113
+ *,
114
+ base_url: str = _DEFAULT_BASE_URL,
115
+ auth_header: str = "Authorization",
116
+ auth_scheme: str = "Bearer",
117
+ timeout: float = 30.0,
118
+ session: Optional[requests.Session] = None,
119
+ ) -> None:
120
+ if not api_key:
121
+ raise ValueError("A Moveworks Data API key is required (minted by a Moveworks superadmin)")
122
+ self._base_url = base_url.rstrip("/")
123
+ self._timeout = timeout
124
+ self._session = session or requests.Session()
125
+ value = f"{auth_scheme} {api_key}".strip() if auth_scheme else api_key
126
+ self._session.headers.update({auth_header: value, "Accept": "application/json"})
127
+
128
+ def records(
129
+ self,
130
+ table: str,
131
+ *,
132
+ since: Optional[datetime] = None,
133
+ until: Optional[datetime] = None,
134
+ timestamp_field: str = _DEFAULT_TIMESTAMP_FIELD,
135
+ extra_params: Optional[Dict[str, str]] = None,
136
+ max_pages: int = 1000,
137
+ ) -> Iterator[Dict[str, Any]]:
138
+ """Yield records from ``/records/<table>``, following OData pagination."""
139
+ filters: List[str] = []
140
+ if since is not None:
141
+ filters.append(f"{timestamp_field} ge {since.astimezone(timezone.utc).isoformat()}")
142
+ if until is not None:
143
+ filters.append(f"{timestamp_field} lt {until.astimezone(timezone.utc).isoformat()}")
144
+ params: Dict[str, str] = dict(extra_params or {})
145
+ if filters:
146
+ params["$filter"] = " and ".join(filters)
147
+
148
+ url: Optional[str] = f"{self._base_url}/records/{table}"
149
+ for _ in range(max_pages):
150
+ if not url:
151
+ return
152
+ response = self._session.get(url, params=params, timeout=self._timeout)
153
+ response.raise_for_status()
154
+ body = response.json()
155
+ for record in body.get("value", []):
156
+ if isinstance(record, dict):
157
+ yield record
158
+ # nextlink is absolute and already carries its own query string.
159
+ url = body.get("odata.nextlink") or body.get("@odata.nextLink")
160
+ params = {}
161
+
162
+
163
+ class MoveworksSyncReport:
164
+ def __init__(self) -> None:
165
+ self.conversations = 0
166
+ self.interactions = 0
167
+ self.ingested = 0
168
+ self.failed = 0
169
+ self.plugin_calls_attached = 0
170
+ self.skipped_no_time = 0
171
+ # Distinct AgentX session ids touched by this run - what judge_sessions() operates on.
172
+ self.session_ids: List[str] = []
173
+ self.sessions_judged = 0
174
+ self.sessions_judge_skipped = 0
175
+ self.sessions_judge_failed = 0
176
+
177
+ def __repr__(self) -> str: # also what the CLI prints
178
+ base = (
179
+ f"MoveworksSyncReport(conversations={self.conversations}, interactions={self.interactions}, "
180
+ f"ingested={self.ingested}, failed={self.failed}, "
181
+ f"plugin_calls_attached={self.plugin_calls_attached}, skipped_no_time={self.skipped_no_time}"
182
+ )
183
+ if self.sessions_judged or self.sessions_judge_skipped or self.sessions_judge_failed:
184
+ base += (
185
+ f", sessions_judged={self.sessions_judged}, "
186
+ f"sessions_judge_skipped={self.sessions_judge_skipped}, "
187
+ f"sessions_judge_failed={self.sessions_judge_failed}"
188
+ )
189
+ return base + ")"
190
+
191
+
192
+ class MoveworksImporter:
193
+ """
194
+ Pulls Moveworks Data API records and replays them into AgentX as traces.
195
+
196
+ ``agent_name`` overrides the agent every trace is attributed to; by default traces group per
197
+ Moveworks domain as ``moveworks-<domain>`` (falling back to ``moveworks-assistant``).
198
+ """
199
+
200
+ def __init__(
201
+ self,
202
+ client: MoveworksDataAPIClient,
203
+ *,
204
+ agentx_api_key: str,
205
+ agentx_base_url: Optional[str] = None,
206
+ agent_name: Optional[str] = None,
207
+ timestamp_field: str = _DEFAULT_TIMESTAMP_FIELD,
208
+ ) -> None:
209
+ self._client = client
210
+ self._agent_name = agent_name
211
+ self._timestamp_field = timestamp_field
212
+ self._ingest = IngestClient(agentx_api_key, sdk_version=VERSION, base_url=agentx_base_url)
213
+
214
+ # -- record -> wire mapping ----------------------------------------------------------------
215
+
216
+ def _conversation_index(self, since: datetime, until: Optional[datetime]) -> Dict[str, Dict[str, Any]]:
217
+ index: Dict[str, Dict[str, Any]] = {}
218
+ try:
219
+ for conversation in self._client.records(
220
+ "conversations", since=since, until=until, timestamp_field=self._timestamp_field
221
+ ):
222
+ conversation_id = _first(conversation, "conversation_id", "id")
223
+ if conversation_id:
224
+ index[str(conversation_id)] = conversation
225
+ except requests.RequestException:
226
+ # Conversations only enrich metadata (domain/route); interactions alone still import.
227
+ pass
228
+ return index
229
+
230
+ def _plugin_calls_index(self, since: datetime, until: Optional[datetime]) -> Dict[str, List[Dict[str, Any]]]:
231
+ index: Dict[str, List[Dict[str, Any]]] = {}
232
+ try:
233
+ for call in self._client.records(
234
+ "plugin-calls", since=since, until=until, timestamp_field=self._timestamp_field
235
+ ):
236
+ key = _first(call, "interaction_id", "conversation_id")
237
+ if key:
238
+ index.setdefault(str(key), []).append(call)
239
+ except requests.RequestException:
240
+ pass
241
+ return index
242
+
243
+ @staticmethod
244
+ def _tool_call_wire(call: Dict[str, Any]) -> Dict[str, Any]:
245
+ wire: Dict[str, Any] = {"name": str(_first(call, "plugin_name", "name") or "moveworks-plugin")}
246
+ served = _first(call, "served", "is_served")
247
+ used = _first(call, "used", "is_used")
248
+ # served/used are surfacing metrics, not success/failure - never fabricate a failure.
249
+ if served is not None:
250
+ wire["served"] = bool(served)
251
+ if used is not None:
252
+ wire["used"] = bool(used)
253
+ return wire
254
+
255
+ def _interaction_wire(
256
+ self,
257
+ interaction: Dict[str, Any],
258
+ conversations: Dict[str, Dict[str, Any]],
259
+ plugin_calls: Dict[str, List[Dict[str, Any]]],
260
+ ) -> Optional[Dict[str, Any]]:
261
+ started = _record_time(interaction)
262
+ if started is None:
263
+ return None
264
+
265
+ interaction_id = str(_first(interaction, "interaction_id", "id") or "")
266
+ conversation_id = _first(interaction, "conversation_id")
267
+ conversation = conversations.get(str(conversation_id)) if conversation_id else None
268
+
269
+ domain = _first(interaction, "domain") or (conversation and _first(conversation, "domain"))
270
+ route = _first(interaction, "route") or (conversation and _first(conversation, "route"))
271
+ interaction_type = _first(interaction, "interaction_type", "type")
272
+
273
+ user_text = _first(interaction, "utterance", "user_input", "text", "message", "query", "content")
274
+ response_text = _first(interaction, "response", "bot_response", "reply", "answer")
275
+
276
+ calls: List[Dict[str, Any]] = []
277
+ if interaction_id and interaction_id in plugin_calls:
278
+ calls = plugin_calls[interaction_id]
279
+ elif conversation_id and str(conversation_id) in plugin_calls:
280
+ calls = plugin_calls[str(conversation_id)]
281
+
282
+ metadata: Dict[str, Any] = {"source": "moveworks"}
283
+ for key, value in (
284
+ ("conversationId", conversation_id),
285
+ ("interactionId", interaction_id or None),
286
+ ("interactionType", interaction_type),
287
+ ("domain", domain),
288
+ ("route", route),
289
+ ("userId", _first(interaction, "user_id")),
290
+ ):
291
+ if value:
292
+ metadata[key] = value
293
+
294
+ latency_ms: Optional[int] = None
295
+ ended = _parse_time(_first(interaction, "resolved_time", "completed_time", "end_time", "updated_time"))
296
+ if ended is not None and ended >= started:
297
+ latency_ms = int((ended - started).total_seconds() * 1000)
298
+
299
+ name = self._agent_name or (f"moveworks-{domain}" if domain else "moveworks-assistant")
300
+
301
+ wire: Dict[str, Any] = {
302
+ "name": name,
303
+ "framework": "moveworks",
304
+ "metadata": metadata,
305
+ "started_at_unix_nano": str(int(started.timestamp() * 1_000_000_000)),
306
+ }
307
+ if interaction_id:
308
+ wire["span_id"] = f"mw:{interaction_id}"
309
+ if conversation_id:
310
+ wire["session_id"] = f"mw_{conversation_id}"
311
+ if user_text is not None:
312
+ wire["input"] = user_text
313
+ else:
314
+ wire["input"] = json.dumps({"interaction_type": interaction_type or "unknown"})
315
+ if response_text is not None:
316
+ wire["output"] = response_text
317
+ if latency_ms is not None:
318
+ wire["latency_ms"] = latency_ms
319
+ if calls:
320
+ wire["tool_calls"] = [self._tool_call_wire(call) for call in calls]
321
+ return wire
322
+
323
+ # -- public --------------------------------------------------------------------------------
324
+
325
+ def sync(
326
+ self,
327
+ since: datetime,
328
+ until: Optional[datetime] = None,
329
+ *,
330
+ monitor: bool = False,
331
+ judge_sessions: bool = False,
332
+ dry_run: bool = False,
333
+ on_payload: Optional[Any] = None,
334
+ ) -> MoveworksSyncReport:
335
+ """
336
+ Import every interaction in ``[since, until)``. Safe to re-run over the same window - the
337
+ engine deduplicates on the deterministic ``span_id`` (and skips re-judging deduped spans).
338
+
339
+ ``monitor=True`` sets ``monitor: true`` on every trace - the engine's explicit opt-in that
340
+ runs pattern/built-in checks (PII, empty response, tool failure, active patterns) on each
341
+ imported trace. ``judge_sessions=True`` additionally asks the engine to judge every
342
+ imported session with each enabled session-scoped evaluator after the sync; the request
343
+ carries ``ifStale=true`` so a session already scored (e.g. by the engine's own 24h sweep)
344
+ is never judged twice. Returns a summary report.
345
+ """
346
+ report = MoveworksSyncReport()
347
+ conversations = self._conversation_index(since, until)
348
+ report.conversations = len(conversations)
349
+ plugin_calls = self._plugin_calls_index(since, until)
350
+ session_ids: List[str] = []
351
+
352
+ for interaction in self._client.records(
353
+ "interactions", since=since, until=until, timestamp_field=self._timestamp_field
354
+ ):
355
+ report.interactions += 1
356
+ wire = self._interaction_wire(interaction, conversations, plugin_calls)
357
+ if wire is None:
358
+ report.skipped_no_time += 1
359
+ continue
360
+ if monitor:
361
+ wire["monitor"] = True
362
+ report.plugin_calls_attached += len(wire.get("tool_calls", []))
363
+ session_id = wire.get("session_id")
364
+ if session_id and session_id not in session_ids:
365
+ session_ids.append(session_id)
366
+ if on_payload is not None:
367
+ on_payload(wire)
368
+ if dry_run:
369
+ continue
370
+ if self._ingest.send_trace_sync(wire) is not None:
371
+ report.ingested += 1
372
+ else:
373
+ report.failed += 1
374
+
375
+ report.session_ids = session_ids
376
+ if judge_sessions and not dry_run:
377
+ self.judge_sessions(session_ids, report=report)
378
+ return report
379
+
380
+ def judge_sessions(
381
+ self, session_ids: List[str], *, report: Optional[MoveworksSyncReport] = None
382
+ ) -> MoveworksSyncReport:
383
+ """
384
+ Judge each session with every enabled session-scoped evaluator via the engine's on-demand
385
+ judge route, with ``ifStale=true`` so an up-to-date verdict (from the sweep or a previous
386
+ run) is left alone instead of judged again.
387
+ """
388
+ report = report or MoveworksSyncReport()
389
+ # Same base/key the ingest client already resolved - the judge routes live under
390
+ # /agent-monitoring on the same engine.
391
+ base = self._ingest._base_url
392
+ http = self._ingest._session
393
+ try:
394
+ response = http.get(f"{base}/agent-monitoring/online-evaluators", timeout=30)
395
+ response.raise_for_status()
396
+ evaluators = [
397
+ e for e in response.json().get("evaluators", [])
398
+ if e.get("enabled") and e.get("scope") == "session"
399
+ ]
400
+ except requests.RequestException:
401
+ report.sessions_judge_failed += len(session_ids)
402
+ return report
403
+ for session_id in session_ids:
404
+ for evaluator in evaluators:
405
+ try:
406
+ result = http.post(
407
+ f"{base}/agent-monitoring/sessions/{session_id}/judge/{evaluator['_id']}",
408
+ params={"ifStale": "true"},
409
+ timeout=120,
410
+ )
411
+ if result.status_code == 200 and result.json().get("skipped"):
412
+ report.sessions_judge_skipped += 1
413
+ elif result.ok:
414
+ report.sessions_judged += 1
415
+ else:
416
+ report.sessions_judge_failed += 1
417
+ except requests.RequestException:
418
+ report.sessions_judge_failed += 1
419
+ return report
420
+
421
+
422
+ # ----------------------------------------------------------------------------------------------
423
+ # CLI: agentx-moveworks sync
424
+ # ----------------------------------------------------------------------------------------------
425
+
426
+ def _parse_since(value: str) -> datetime:
427
+ """Either an ISO timestamp or a relative shorthand like ``7d`` / ``24h`` / ``30m``."""
428
+ text = value.strip().lower()
429
+ if text and text[-1] in ("d", "h", "m") and text[:-1].isdigit():
430
+ amount = int(text[:-1])
431
+ delta = {"d": timedelta(days=amount), "h": timedelta(hours=amount), "m": timedelta(minutes=amount)}[text[-1]]
432
+ return datetime.now(timezone.utc) - delta
433
+ parsed = _parse_time(value)
434
+ if parsed is None:
435
+ raise SystemExit(f"agentx-moveworks: could not parse --since {value!r} (use ISO-8601 or e.g. 7d)")
436
+ return parsed
437
+
438
+
439
+ def _read_cursor(path: Path) -> Optional[datetime]:
440
+ try:
441
+ return _parse_time(json.loads(path.read_text()).get("last_synced"))
442
+ except (OSError, ValueError, json.JSONDecodeError):
443
+ return None
444
+
445
+
446
+ def _write_cursor(path: Path, value: datetime) -> None:
447
+ path.parent.mkdir(parents=True, exist_ok=True)
448
+ path.write_text(json.dumps({"last_synced": value.isoformat()}))
449
+
450
+
451
+ def cli_main(argv: Optional[List[str]] = None) -> None:
452
+ parser = argparse.ArgumentParser(
453
+ prog="agentx-moveworks",
454
+ description="Sync Moveworks Data API activity into AgentX as traces/sessions.",
455
+ )
456
+ sub = parser.add_subparsers(dest="command", required=True)
457
+ sync = sub.add_parser("sync", help="Import a time window of Moveworks activity")
458
+ sync.add_argument("--since", help="ISO timestamp or relative (7d, 24h). Default: cursor file, else 24h")
459
+ sync.add_argument("--until", help="ISO timestamp (default: now)")
460
+ sync.add_argument("--cursor-file", default=str(_DEFAULT_CURSOR_FILE), help="Incremental cursor path")
461
+ sync.add_argument("--no-cursor", action="store_true", help="Ignore and don't update the cursor file")
462
+ sync.add_argument("--agent-name", help="Attribute every trace to this agent (default: per Moveworks domain)")
463
+ sync.add_argument("--timestamp-field", default=_DEFAULT_TIMESTAMP_FIELD, help="Data API record timestamp field")
464
+ sync.add_argument("--moveworks-base-url", default=os.getenv("MOVEWORKS_BASE_URL", _DEFAULT_BASE_URL))
465
+ sync.add_argument(
466
+ "--monitor",
467
+ action="store_true",
468
+ help="Run pattern/built-in checks (PII, empty response, tool failure, active patterns) on each imported trace",
469
+ )
470
+ sync.add_argument(
471
+ "--judge-sessions",
472
+ action="store_true",
473
+ help="After the sync, judge each imported session with every enabled session-scoped evaluator "
474
+ "(skips sessions that already have an up-to-date verdict, so the engine's own sweep is never duplicated)",
475
+ )
476
+ sync.add_argument("--dry-run", action="store_true", help="Map and print payloads without ingesting")
477
+
478
+ args = parser.parse_args(argv)
479
+
480
+ moveworks_key = os.getenv("MOVEWORKS_API_KEY", "")
481
+ agentx_key = os.getenv("AGENTX_API_KEY", "")
482
+ if not moveworks_key:
483
+ raise SystemExit("agentx-moveworks: set MOVEWORKS_API_KEY (Data API credential from Moveworks Setup)")
484
+ if not agentx_key and not args.dry_run:
485
+ raise SystemExit("agentx-moveworks: set AGENTX_API_KEY (your AgentX project API key)")
486
+
487
+ cursor_path = Path(args.cursor_file)
488
+ since = (
489
+ _parse_since(args.since)
490
+ if args.since
491
+ else ((None if args.no_cursor else _read_cursor(cursor_path)) or (datetime.now(timezone.utc) - timedelta(hours=24)))
492
+ )
493
+ until = _parse_time(args.until) if args.until else datetime.now(timezone.utc)
494
+
495
+ client = MoveworksDataAPIClient(moveworks_key, base_url=args.moveworks_base_url)
496
+ importer = MoveworksImporter(
497
+ client,
498
+ agentx_api_key=agentx_key or "dry-run",
499
+ agentx_base_url=os.getenv("AGENTX_API_BASE_URL"),
500
+ agent_name=args.agent_name,
501
+ timestamp_field=args.timestamp_field,
502
+ )
503
+
504
+ print(f"agentx-moveworks: syncing {since.isoformat()} -> {until.isoformat()}")
505
+ on_payload = (lambda wire: print(json.dumps(wire))) if args.dry_run else None
506
+ report = importer.sync(
507
+ since,
508
+ until,
509
+ monitor=args.monitor,
510
+ judge_sessions=args.judge_sessions,
511
+ dry_run=args.dry_run,
512
+ on_payload=on_payload,
513
+ )
514
+ print(report)
515
+
516
+ if not args.dry_run and not args.no_cursor and report.failed == 0:
517
+ _write_cursor(cursor_path, until)
518
+ print(f"agentx-moveworks: cursor -> {cursor_path}")
519
+ elif report.failed:
520
+ print("agentx-moveworks: failures occurred - cursor NOT advanced, re-run to retry", file=sys.stderr)
521
+ sys.exit(1)
522
+
523
+
524
+ if __name__ == "__main__":
525
+ cli_main()
@@ -99,9 +99,12 @@ class _TraceSpan:
99
99
  self._parent_span_id: Optional[str] = None
100
100
  # Numbers auto-named "LLM Call N"/"Retrieval N" child spans - see _merge_child_run.
101
101
  self._child_span_count = 0
102
- # Monitor: check this trace against patterns immediately on ingest, no dashboard profile
103
- # required. pattern_ids (if given) fully defines what's checked - only those patterns run,
104
- # the built-in checks are skipped. See Tracer.trace()'s monitor/pattern_ids params.
102
+ # Monitor: True checks this trace against patterns immediately on ingest, no dashboard
103
+ # profile required; False explicitly OPTS OUT of every ingest-time check (patterns,
104
+ # built-ins, online/custom evaluators, topics) - what eval-run traces send, since the
105
+ # run's own evaluator already judges each case. None (default) leaves the server's
106
+ # standard behavior. pattern_ids (if given) fully defines what's checked. See
107
+ # Tracer.trace()'s monitor/pattern_ids params.
105
108
  self._monitor = monitor
106
109
  self._pattern_ids = pattern_ids
107
110
 
@@ -152,7 +155,7 @@ class _TraceSpan:
152
155
 
153
156
  self._trace_id = self._tracer._send(
154
157
  sync=self._sync,
155
- monitor=self._monitor or None,
158
+ monitor=self._monitor,
156
159
  pattern_ids=self._pattern_ids,
157
160
  name=self.name,
158
161
  agent_id=self._agent_id,
@@ -769,7 +772,7 @@ class Tracer:
769
772
  model: Optional[str] = None,
770
773
  session_id: Optional[str] = None,
771
774
  sync: bool = False,
772
- monitor: bool = False,
775
+ monitor: Optional[bool] = None,
773
776
  pattern_ids: Optional[List[str]] = None,
774
777
  agent_id: Optional[str] = None,
775
778
  ) -> _TraceSpan:
@@ -799,7 +802,9 @@ class Tracer:
799
802
  return {"output": resp, "trace_id": span.trace_id}
800
803
 
801
804
  Pass ``monitor=True`` to check this trace against Monitor patterns immediately, with no
802
- dashboard profile required. ``pattern_ids`` (ids from ``client.monitor.patterns.builder(
805
+ dashboard profile required. Pass ``monitor=False`` to explicitly skip EVERY ingest-time
806
+ check (patterns, built-ins, online/custom evaluators, topics) - use this for traces
807
+ created inside an evaluation run, which the run's own evaluator already judges. ``pattern_ids`` (ids from ``client.monitor.patterns.builder(
803
808
  ...).publish()``) restricts detection to exactly those patterns; omit it to run the full
804
809
  default sweep (built-in checks plus every enabled workspace pattern)::
805
810
 
@@ -0,0 +1 @@
1
+ VERSION = "0.6.21"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.19
3
+ Version: 0.6.21
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
@@ -36,6 +36,7 @@ agentx/integrations/google_genai.py
36
36
  agentx/integrations/langchain.py
37
37
  agentx/integrations/litellm.py
38
38
  agentx/integrations/llamaindex.py
39
+ agentx/integrations/moveworks.py
39
40
  agentx/integrations/openai.py
40
41
  agentx/integrations/openai_agents.py
41
42
  agentx/monitor/__init__.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ agentx-moveworks = agentx.integrations.moveworks:cli_main
3
+ agentx-trace-eval = agentx.cli:main
@@ -30,7 +30,12 @@ setup(
30
30
  # Thin launcher for the self-hostable governance engine (AgentX-ai/AgentX-trace-eval),
31
31
  # not this SDK itself, see agentx/cli.py's module docstring for why it lazily downloads
32
32
  # rather than bundling that engine in this package.
33
- "console_scripts": ["agentx-trace-eval=agentx.cli:main"],
33
+ "console_scripts": [
34
+ "agentx-trace-eval=agentx.cli:main",
35
+ # Moveworks Data API -> AgentX traces sync (see agentx/integrations/moveworks.py) -
36
+ # a pull importer, since Moveworks agents run in their cloud with no in-process hook.
37
+ "agentx-moveworks=agentx.integrations.moveworks:cli_main",
38
+ ],
34
39
  },
35
40
  install_requires=[
36
41
  "urllib3>=1.26.11",
@@ -1 +0,0 @@
1
- VERSION = "0.6.19"
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- agentx-trace-eval = agentx.cli:main
File without changes
File without changes
File without changes