agentx-python 0.6.32__tar.gz → 0.6.34__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 (77) hide show
  1. {agentx_python-0.6.32 → agentx_python-0.6.34}/PKG-INFO +1 -1
  2. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/agentx.py +16 -0
  3. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/_term.py +6 -1
  4. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/datasets.py +16 -0
  5. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/models.py +59 -0
  6. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/runner.py +41 -25
  7. agentx_python-0.6.34/agentx/export.py +97 -0
  8. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/client.py +15 -0
  9. agentx_python-0.6.34/agentx/monitor/scorers.py +170 -0
  10. agentx_python-0.6.34/agentx/projects.py +58 -0
  11. agentx_python-0.6.34/agentx/traces.py +62 -0
  12. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/tracing/tracer.py +13 -1
  13. agentx_python-0.6.34/agentx/version.py +1 -0
  14. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/PKG-INFO +1 -1
  15. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/SOURCES.txt +4 -0
  16. agentx_python-0.6.32/agentx/version.py +0 -1
  17. {agentx_python-0.6.32 → agentx_python-0.6.34}/LICENSE +0 -0
  18. {agentx_python-0.6.32 → agentx_python-0.6.34}/README.md +0 -0
  19. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/__init__.py +0 -0
  20. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/cli.py +0 -0
  21. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/__init__.py +0 -0
  22. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/adapters/__init__.py +0 -0
  23. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  24. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/adapters/precomputed.py +0 -0
  25. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/adapters/raw.py +0 -0
  26. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/client.py +0 -0
  27. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/evaluation_settings.py +0 -0
  28. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/prompts.py +0 -0
  29. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/reporting.py +0 -0
  30. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/results.py +0 -0
  31. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/tool_schemas.py +0 -0
  32. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/evaluations/tracing.py +0 -0
  33. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/exceptions.py +0 -0
  34. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/feedback.py +0 -0
  35. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/__init__.py +0 -0
  36. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/_traced_call.py +0 -0
  37. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/anthropic.py +0 -0
  38. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/autogen.py +0 -0
  39. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/crewai.py +0 -0
  40. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/databricks.py +0 -0
  41. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/google_adk.py +0 -0
  42. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/google_genai.py +0 -0
  43. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/langchain.py +0 -0
  44. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/litellm.py +0 -0
  45. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/llamaindex.py +0 -0
  46. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/moveworks.py +0 -0
  47. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/openai.py +0 -0
  48. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/integrations/openai_agents.py +0 -0
  49. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/__init__.py +0 -0
  50. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/agents.py +0 -0
  51. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/models.py +0 -0
  52. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/online_evaluators.py +0 -0
  53. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/patterns.py +0 -0
  54. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/profile.py +0 -0
  55. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/sessions.py +0 -0
  56. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/monitor/signals.py +0 -0
  57. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/outcomes.py +0 -0
  58. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/py.typed +0 -0
  59. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/resources/__init__.py +0 -0
  60. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/resources/agent.py +0 -0
  61. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/resources/conversation.py +0 -0
  62. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/resources/workforce.py +0 -0
  63. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/tracing/__init__.py +0 -0
  64. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/tracing/ci_types.py +0 -0
  65. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/tracing/ingest_client.py +0 -0
  66. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx/util.py +0 -0
  67. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/dependency_links.txt +0 -0
  68. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/entry_points.txt +0 -0
  69. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/not-zip-safe +0 -0
  70. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/requires.txt +0 -0
  71. {agentx_python-0.6.32 → agentx_python-0.6.34}/agentx_python.egg-info/top_level.txt +0 -0
  72. {agentx_python-0.6.32 → agentx_python-0.6.34}/setup.cfg +0 -0
  73. {agentx_python-0.6.32 → agentx_python-0.6.34}/setup.py +0 -0
  74. {agentx_python-0.6.32 → agentx_python-0.6.34}/tests/test_integration.py +0 -0
  75. {agentx_python-0.6.32 → agentx_python-0.6.34}/tests/test_integrations.py +0 -0
  76. {agentx_python-0.6.32 → agentx_python-0.6.34}/tests/test_selfhost_analysis_fallback.py +0 -0
  77. {agentx_python-0.6.32 → agentx_python-0.6.34}/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.32
3
+ Version: 0.6.34
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
@@ -60,6 +60,22 @@ class AgentX:
60
60
  # ground truth behind the dashboard's Judge Calibration card. Self-host only.
61
61
  self.outcomes = OutcomesClient(api_key=self.api_key)
62
62
 
63
+ from agentx.projects import ProjectsClient
64
+
65
+ # Project CRUD (self-host): isolated tenants with their own API keys (P1.1).
66
+ self.projects = ProjectsClient(api_key=self.api_key)
67
+
68
+ from agentx.traces import TracesClient
69
+
70
+ # The read side of tracing: trace-by-id detail and paginated listing (P1.2).
71
+ self.traces = TracesClient(api_key=self.api_key)
72
+
73
+ from agentx.export import ExportClient
74
+
75
+ # Bulk NDJSON egress for backup/migration (P2.1): manifest, per-entity streaming, and
76
+ # directory dumps. Self-host only.
77
+ self.export = ExportClient(api_key=self.api_key)
78
+
63
79
  from agentx.feedback import FeedbackClient
64
80
 
65
81
  # Forward end-user votes ("up"/"down") on traced responses - a "down" raises a signal
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import itertools
6
6
  import sys
7
+ import os
7
8
  import threading
8
9
  import time
9
10
 
@@ -61,8 +62,12 @@ class Spinner:
61
62
  self._message = message
62
63
  self._stop = threading.Event()
63
64
  self._thread: threading.Thread | None = None
65
+ # AGENTX_EVAL_QUIET=1: no spinner thread at all - CI logs stay clean.
66
+ self._quiet = os.getenv("AGENTX_EVAL_QUIET", "").lower() in ("1", "true", "yes")
64
67
 
65
68
  def __enter__(self) -> "Spinner":
69
+ if self._quiet:
70
+ return self
66
71
  if not _IS_TTY:
67
72
  print(f" {self._message}...", flush=True)
68
73
  return self
@@ -78,7 +83,7 @@ class Spinner:
78
83
  print(f" {message}...", flush=True)
79
84
 
80
85
  def __exit__(self, *_) -> None:
81
- if not _IS_TTY:
86
+ if self._quiet or not _IS_TTY:
82
87
  return
83
88
  self._stop.set()
84
89
  if self._thread:
@@ -35,6 +35,7 @@ class DatasetBuilder:
35
35
  rouge_score: bool = False,
36
36
  similarity_model: Optional[str] = None,
37
37
  sovereignty_models: Optional[List[str]] = None,
38
+ code_scorers: Optional[List[Dict[str, Any]]] = None,
38
39
  ):
39
40
  self._client = client
40
41
  self._payload: Dict[str, Any] = {
@@ -63,6 +64,21 @@ class DatasetBuilder:
63
64
  self._payload["vectorSimilarity"] = vs
64
65
  if jaccard_similarity:
65
66
  self._payload["jaccardSimilarity"] = {"enabled": True}
67
+ # Offline code scorers, versioned in the repo next to the dataset they guard (P1.4):
68
+ # each entry is {"name", "code"} (a JS function body invoked as
69
+ # score({input, output, expected, toolCalls})), optional "enabled" (default True).
70
+ if code_scorers:
71
+ import uuid as _uuid
72
+
73
+ self._payload["codeScorers"] = [
74
+ {
75
+ "id": scorer.get("id") or _uuid.uuid4().hex[:12],
76
+ "name": scorer["name"],
77
+ "code": scorer["code"],
78
+ "enabled": scorer.get("enabled", True),
79
+ }
80
+ for scorer in code_scorers
81
+ ]
66
82
  if bleu_score:
67
83
  self._payload["bleuScore"] = {"enabled": True}
68
84
  if rouge_score:
@@ -399,6 +399,65 @@ class BatchAppendResponse(BaseModel):
399
399
  extra = "ignore"
400
400
 
401
401
 
402
+ # ---------------------------------------------------------------------------
403
+ # Typed run-result rows (P1.5)
404
+ # ---------------------------------------------------------------------------
405
+
406
+
407
+ class RunResultRow(BaseModel):
408
+ """One row of a finished run's results - typed and snake_case, replacing the raw wire dicts
409
+ ``run.results()`` used to return. ``.raw`` keeps the full wire dict for anything not yet
410
+ modeled. Dict-style access (``row["rating"]``, ``row.get(...)``) still works for one
411
+ deprecation cycle and warns; prefer the attributes."""
412
+
413
+ rating: Optional[float] = None
414
+ justification: Optional[str] = None
415
+ question_text: Optional[str] = Field(default=None, alias="questionText")
416
+ response: Optional[str] = None
417
+ trace_id: Optional[str] = Field(default=None, alias="traceId")
418
+ latency_ms: Optional[float] = Field(default=None, alias="latencyMs")
419
+ input_tokens: Optional[int] = Field(default=None, alias="inputTokens")
420
+ output_tokens: Optional[int] = Field(default=None, alias="outputTokens")
421
+ cosine_similarity: Optional[float] = Field(default=None, alias="cosineSimilarity")
422
+ jaccard_similarity: Optional[float] = Field(default=None, alias="jaccardSimilarity")
423
+ bleu_score: Optional[float] = Field(default=None, alias="bleuScore")
424
+ rouge_score: Optional[float] = Field(default=None, alias="rougeScore")
425
+ code_scorer_results: Optional[List[Dict[str, Any]]] = Field(default=None, alias="codeScorerResults")
426
+ raw: Dict[str, Any] = Field(default_factory=dict)
427
+
428
+ class Config:
429
+ populate_by_name = True
430
+ extra = "ignore"
431
+
432
+ @classmethod
433
+ def from_wire(cls, wire: Dict[str, Any]) -> "RunResultRow":
434
+ row = cls.model_validate(wire)
435
+ row.raw = wire
436
+ return row
437
+
438
+ def __getitem__(self, key: str) -> Any:
439
+ import warnings
440
+
441
+ warnings.warn(
442
+ "Dict-style access on run results is deprecated - use typed attributes "
443
+ '(row.rating, row.jaccard_similarity) or row.raw["..."] for unmodeled fields.',
444
+ DeprecationWarning,
445
+ stacklevel=2,
446
+ )
447
+ return self.raw[key]
448
+
449
+ def get(self, key: str, default: Any = None) -> Any:
450
+ import warnings
451
+
452
+ warnings.warn(
453
+ "Dict-style access on run results is deprecated - use typed attributes "
454
+ 'or row.raw.get("...") for unmodeled fields.',
455
+ DeprecationWarning,
456
+ stacklevel=2,
457
+ )
458
+ return self.raw.get(key, default)
459
+
460
+
402
461
  # ---------------------------------------------------------------------------
403
462
  # Analysis / report
404
463
  # ---------------------------------------------------------------------------
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import os
4
5
  import time
5
6
  import uuid
6
7
  from typing import Any, Callable, Dict, List, Optional, Set, Union
@@ -37,6 +38,17 @@ from agentx.evaluations._term import (
37
38
 
38
39
  logger = logging.getLogger(__name__)
39
40
 
41
+
42
+ def _eval_quiet() -> bool:
43
+ """AGENTX_EVAL_QUIET=1 silences the interactive progress UI (spinners, per-case lines) for
44
+ CI logs - results, gate verdicts, and errors still print. Read per call so tests can toggle."""
45
+ return os.getenv("AGENTX_EVAL_QUIET", "").lower() in ("1", "true", "yes")
46
+
47
+
48
+ def _say(*args, **kwargs) -> None:
49
+ if not _eval_quiet():
50
+ print(*args, **kwargs)
51
+
40
52
  AdapterLike = Union[
41
53
  Callable[[EvaluationCase], Any],
42
54
  RawCallableAdapter,
@@ -126,18 +138,18 @@ class EvaluationRunContext:
126
138
  )
127
139
  n_smoke = sum(1 for c in cases if c.is_smoke_test_variant)
128
140
 
129
- print(cyan(sep))
130
- print(f" {bold('AgentX Evaluation')} {dim(' - ')} {name}")
131
- print(cyan(sep))
132
- print(f" {dim('Run :')} {dim(self._run.run_id)}")
141
+ _say(cyan(sep))
142
+ _say(f" {bold('AgentX Evaluation')} {dim(' - ')} {name}")
143
+ _say(cyan(sep))
144
+ _say(f" {dim('Run :')} {dim(self._run.run_id)}")
133
145
  if display:
134
- print(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
135
- print()
146
+ _say(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
147
+ _say()
136
148
  exec_line = f"{bold('Executing')} {n_q} question{'s' if n_q != 1 else ''} × {n_r} run{'s' if n_r != 1 else ''}"
137
149
  if n_smoke:
138
150
  variant_word = "variant" if n_smoke == 1 else "variants"
139
151
  exec_line += f" {dim(f'(+{n_smoke} smoke-test {variant_word})')}"
140
- print(exec_line)
152
+ _say(exec_line)
141
153
 
142
154
  # Resume: skip already-submitted keys
143
155
  already_done = self._fetch_submitted_keys()
@@ -185,7 +197,7 @@ class EvaluationRunContext:
185
197
  resp = self._client.append_results(self._run.run_id, batch_id, batch)
186
198
  if resp.live_statistics is not None:
187
199
  self._live_stats = resp.live_statistics
188
- print(
200
+ _say(
189
201
  f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
190
202
  )
191
203
  logger.info(
@@ -196,7 +208,7 @@ class EvaluationRunContext:
196
208
  resp.failed_validation,
197
209
  )
198
210
  except Exception as exc:
199
- print(f" {red('✗')} Scoring failed: {dim(str(exc))}")
211
+ _say(f" {red('✗')} Scoring failed: {dim(str(exc))}")
200
212
  logger.error("Failed to submit batch %s: %s", batch_id[:8], exc)
201
213
 
202
214
  def _fetch_submitted_keys(self) -> Set[str]:
@@ -213,16 +225,16 @@ class EvaluationRunContext:
213
225
  # ------------------------------------------------------------------
214
226
 
215
227
  def finalize(self) -> "EvaluationRunContext":
216
- print()
228
+ _say()
217
229
  with Spinner("Finalizing - submitting results"):
218
230
  try:
219
231
  data = self._client.finalize_run(self._run.run_id)
220
232
  if isinstance(data, dict) and data.get("liveStatistics") is not None:
221
233
  self._live_stats = LiveStatistics(**data["liveStatistics"])
222
- print(f" {green('✓')} Finalized")
234
+ _say(f" {green('✓')} Finalized")
223
235
  logger.info("Run %s finalized", self._run.run_id)
224
236
  except Exception as exc:
225
- print(f" {red('✗')} Finalize failed: {dim(str(exc))}")
237
+ _say(f" {red('✗')} Finalize failed: {dim(str(exc))}")
226
238
  logger.error("Finalize failed: %s", exc)
227
239
  return self
228
240
 
@@ -255,11 +267,11 @@ class EvaluationRunContext:
255
267
  caller=caller,
256
268
  )
257
269
  result = GateResult(data)
258
- print()
270
+ _say()
259
271
  for check in result.checks:
260
272
  mark = green("✓") if check.get("passed") else red("✗")
261
- print(f" {mark} [{check.get('check')}] {check.get('detail')}")
262
- print(f" {green('✓ GATE PASSED') if result.passed else red('✗ GATE FAILED')}")
273
+ _say(f" {mark} [{check.get('check')}] {check.get('detail')}")
274
+ _say(f" {green('✓ GATE PASSED') if result.passed else red('✗ GATE FAILED')}")
263
275
  return result
264
276
 
265
277
  # ------------------------------------------------------------------
@@ -273,9 +285,13 @@ class EvaluationRunContext:
273
285
  def results(self) -> list:
274
286
  """Per-result rows for this run (rating, justification, code scorer rows, trace ids,
275
287
  latency/tokens, similarity metrics) - what the dashboard's run detail table shows,
276
- fetched fresh from the engine."""
288
+ fetched fresh from the engine. Returns typed ``RunResultRow`` objects (snake_case
289
+ attributes; ``.raw`` is the wire dict; dict-style access warns for one cycle - P1.5)."""
290
+ from agentx.evaluations.models import RunResultRow
291
+
277
292
  detail = self._client.get_run(self.run_id)
278
- return detail.get("results", []) if isinstance(detail, dict) else []
293
+ rows = detail.get("results", []) if isinstance(detail, dict) else []
294
+ return [RunResultRow.from_wire(r) for r in rows]
279
295
 
280
296
  @property
281
297
  def run_id(self) -> str:
@@ -335,7 +351,7 @@ class EvaluationRunContext:
335
351
  raise ValueError("judges must contain 1-3 model ids")
336
352
  resolved_judges = judges if judges is not None else [_DEFAULT_JUDGE_MODEL]
337
353
 
338
- print()
354
+ _say()
339
355
  with Spinner("Analyzing - AI is reviewing your results") as spinner:
340
356
  try:
341
357
  self._client.analyze_run(
@@ -355,14 +371,14 @@ class EvaluationRunContext:
355
371
  status = self._client.get_analysis_status(self._run.run_id)
356
372
 
357
373
  if not status.is_terminal:
358
- print(f" {yellow('!')} Still running after {int(timeout)}s, check the dashboard for status")
374
+ _say(f" {yellow('!')} Still running after {int(timeout)}s, check the dashboard for status")
359
375
  elif status.status == "failed":
360
376
  reason = status.failure_reason.message if status.failure_reason else "unknown error"
361
- print(f" {red('✗')} Analyze failed: {dim(reason)}")
377
+ _say(f" {red('✗')} Analyze failed: {dim(reason)}")
362
378
  else:
363
- print(f" {green('✓')} Analysis complete")
379
+ _say(f" {green('✓')} Analysis complete")
364
380
  except Exception as exc:
365
- print(f" {red('✗')} Analyze failed: {dim(str(exc))}")
381
+ _say(f" {red('✗')} Analyze failed: {dim(str(exc))}")
366
382
  logger.warning("Analyze request failed: %s", exc)
367
383
 
368
384
  try:
@@ -374,7 +390,7 @@ class EvaluationRunContext:
374
390
  # the one signal that something went wrong used to be a logger.warning that is
375
391
  # invisible unless the caller configured logging. Say it on stdout, and let the
376
392
  # status carry the truth for anything reading the object.
377
- print(f" {red('✗')} Could not fetch the report: {dim(str(exc))}")
393
+ _say(f" {red('✗')} Could not fetch the report: {dim(str(exc))}")
378
394
  logger.warning("Could not fetch report: %s", exc)
379
395
  report = Report(
380
396
  runId=self._run.run_id,
@@ -383,7 +399,7 @@ class EvaluationRunContext:
383
399
  )
384
400
 
385
401
  self._report = report
386
- print()
402
+ _say()
387
403
  print_report(report)
388
404
  return report
389
405
 
@@ -615,4 +631,4 @@ def _print_progress(
615
631
  line = f" {tag} {counter} {label} {query_preview}"
616
632
  if suffix:
617
633
  line += f" {suffix}"
618
- print(line)
634
+ _say(line)
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from typing import Any, Dict, Iterator, List, Optional
7
+
8
+ import requests
9
+
10
+ from agentx.util import api_base, get_headers
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AgentXExportError(Exception):
16
+ pass
17
+
18
+
19
+ class ExportClient:
20
+ """Surfaced as ``client.export``: bulk NDJSON egress for backup and migration (self-host).
21
+
22
+ The engine's ``GET /export`` manifest lists every exportable entity (traces, signals,
23
+ events, runs, feedback, outcomes, scorer config, ...) with live row counts;
24
+ ``GET /export/<entity>`` streams the rows as NDJSON. Everything is scoped to the API key's
25
+ project, so an export can never cross a tenant boundary.
26
+
27
+ Typical uses::
28
+
29
+ client.export.dump("./backup") # full backup, one .ndjson per entity
30
+ client.export.dump("./nightly", since=yesterday) # incremental
31
+ for row in client.export.iter("traces"): # stream without touching disk
32
+ ...
33
+
34
+ Restore paths are documented in the self-host backup runbook: replay traces through
35
+ ``client.tracer`` / ``POST /ingest/traces``, or restore at the database level
36
+ (``pg_dump`` / SQLite file copy). There is deliberately no blind row-import endpoint.
37
+ """
38
+
39
+ def __init__(self, api_key: Optional[str] = None):
40
+ self._api_key = api_key
41
+
42
+ def manifest(self) -> List[Dict[str, Any]]:
43
+ """The exportable entities with live row counts: ``[{entity, rows, path}, ...]``."""
44
+ resp = requests.get(
45
+ f"{api_base()}/export", headers=get_headers(self._api_key), timeout=30
46
+ )
47
+ if resp.status_code >= 400:
48
+ raise AgentXExportError(f"Export manifest failed ({resp.status_code}): {resp.text[:200]}")
49
+ return resp.json().get("entities", [])
50
+
51
+ def iter(self, entity: str, since: Optional[str] = None) -> Iterator[Dict[str, Any]]:
52
+ """Stream one entity's rows as dicts without buffering the whole table in memory.
53
+ ``since`` is an ISO-8601 date for incremental pulls (filters on the entity's own
54
+ timestamp column, e.g. ``createdAt`` for traces, ``lastSeenAt`` for signals)."""
55
+ params = {"since": since} if since else None
56
+ resp = requests.get(
57
+ f"{api_base()}/export/{entity}",
58
+ headers=get_headers(self._api_key),
59
+ params=params,
60
+ stream=True,
61
+ timeout=120,
62
+ )
63
+ if resp.status_code >= 400:
64
+ raise AgentXExportError(f"Export of {entity!r} failed ({resp.status_code}): {resp.text[:200]}")
65
+ for line in resp.iter_lines(decode_unicode=True):
66
+ if line and line.strip():
67
+ yield json.loads(line)
68
+
69
+ def dump(
70
+ self,
71
+ directory: str,
72
+ entities: Optional[List[str]] = None,
73
+ since: Optional[str] = None,
74
+ ) -> Dict[str, int]:
75
+ """Write ``<entity>.ndjson`` files (plus a ``manifest.json``) into ``directory`` and
76
+ return ``{entity: rows_written}``. Defaults to every entity the engine advertises;
77
+ pass ``entities`` to restrict, ``since`` for an incremental snapshot."""
78
+ os.makedirs(directory, exist_ok=True)
79
+ manifest = self.manifest()
80
+ wanted = [e["entity"] for e in manifest] if entities is None else entities
81
+ written: Dict[str, int] = {}
82
+ for entity in wanted:
83
+ path = os.path.join(directory, f"{entity}.ndjson")
84
+ count = 0
85
+ with open(path, "w", encoding="utf-8") as fh:
86
+ for row in self.iter(entity, since=since):
87
+ fh.write(json.dumps(row, ensure_ascii=False) + "\n")
88
+ count += 1
89
+ written[entity] = count
90
+ logger.debug("Exported %d %s rows to %s", count, entity, path)
91
+ with open(os.path.join(directory, "manifest.json"), "w", encoding="utf-8") as fh:
92
+ json.dump(
93
+ {"entities": manifest, "written": written, **({"since": since} if since else {})},
94
+ fh,
95
+ indent=2,
96
+ )
97
+ return written
@@ -82,6 +82,10 @@ class MonitorClient:
82
82
 
83
83
  self.patterns = MonitorPatternClient(self)
84
84
  self.signals = MonitorSignalClient(self)
85
+ from agentx.monitor.scorers import ScorersClient
86
+ # Scorers-catalog administration as code: template enable/disable, code/external scorer
87
+ # CRUD and dry runs - full parity with the dashboard's Scorers page (P1.3).
88
+ self.scorers = ScorersClient(api_key=api_key)
85
89
  self.profile = MonitorProfileClient(self)
86
90
  self.online_evaluators = MonitorOnlineEvaluatorClient(self)
87
91
  from agentx.monitor.sessions import MonitorSessionClient
@@ -209,6 +213,17 @@ class MonitorClient:
209
213
  plus deltas vs the prior window and the run-outcome breakdown."""
210
214
  return self._request("GET", "/kpis", params={"window": window})
211
215
 
216
+ def calibration(self, window: str = "7d") -> dict:
217
+ """Project-level judge calibration over a window ("24h", "7d", or "30d"): how often
218
+ AgentX's own verdicts agreed with real-world ground truth reported later (ops outcomes
219
+ via ``client.outcomes`` and end-user downvotes). Returns the dashboard's Judge
220
+ Calibration numbers: compared count, agreement, falsePositiveRate, falseNegativeRate.
221
+ Per-evaluator calibration lives on ``client.monitor.online_evaluators.calibration``."""
222
+ return self._request(
223
+ "GET", "/agent-monitoring/calibration",
224
+ base=self._api_root(), params={"window": window},
225
+ )
226
+
212
227
  # ------------------------------------------------------------------
213
228
  # Signal endpoints
214
229
  # ------------------------------------------------------------------
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional, Sequence
5
+
6
+ import requests
7
+
8
+ from agentx.util import api_base, get_headers
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AgentXScorersError(Exception):
14
+ pass
15
+
16
+
17
+ class ScorersClient:
18
+ """Surfaced as ``client.monitor.scorers``: administer the Scorers catalog as code.
19
+
20
+ Covers what the dashboard's Scorers page does:
21
+
22
+ - **Template scorers** (the shipped zero-LLM detectors): ``templates()`` lists them with
23
+ enablement, ``enable()``/``disable()`` flip them. Everything is opt-in - a fresh project
24
+ runs nothing until a scorer is enabled.
25
+ - **Code scorers**: ``create_code()`` deploys your own Python/JavaScript
26
+ ``handler(input, output, expected, metadata, trace)`` run in-engine per sampled trace.
27
+ - **External scorers**: ``create_external()`` registers your HTTP endpoint (contract v2:
28
+ the full trace record plus its span subtree).
29
+ - Shared CRUD: ``list()``, ``update()``, ``delete()``, and ``dry_run()`` (executes a code
30
+ scorer, or POSTs the sample payload to an external URL, without persisting anything).
31
+
32
+ The engine resource name for code/external scorers remains ``custom-evaluators`` on the
33
+ wire.
34
+ """
35
+
36
+ def __init__(self, api_key: Optional[str] = None):
37
+ self._api_key = api_key
38
+
39
+ def _request(self, method: str, path: str, json: Any = None, params: Any = None) -> Any:
40
+ resp = requests.request(
41
+ method,
42
+ f"{api_base()}/agent-monitoring{path}",
43
+ headers={**get_headers(self._api_key), "Content-Type": "application/json"},
44
+ json=json,
45
+ params=params,
46
+ timeout=20,
47
+ )
48
+ if resp.status_code >= 400:
49
+ try:
50
+ detail = resp.json().get("error", resp.reason)
51
+ except ValueError:
52
+ detail = resp.reason
53
+ raise AgentXScorersError(f"Scorer request failed ({resp.status_code}): {detail}")
54
+ return resp.json() if resp.text else {}
55
+
56
+ # ------------------------------------------------------------------
57
+ # Template scorers (built-in, opt-in)
58
+ # ------------------------------------------------------------------
59
+
60
+ def templates(self) -> List[Dict[str, Any]]:
61
+ """The shipped template scorers with their keys, rules, and ``enabled`` state."""
62
+ patterns = self._request("GET", "/patterns").get("patterns", [])
63
+ return [p for p in patterns if p.get("source") == "builtIn"]
64
+
65
+ def _enabled_template_keys(self) -> List[str]:
66
+ return [p["key"] for p in self.templates() if p.get("enabled")]
67
+
68
+ def enable(self, keys: Sequence[str]) -> List[str]:
69
+ """Enable template scorers by key (e.g. ``["pii-in-response"]``), preserving what is
70
+ already on. Returns the resulting enabled-key list."""
71
+ merged = sorted(set(self._enabled_template_keys()) | set(keys))
72
+ self._request("PUT", "/settings/monitoring-defaults", json={"enabledBuiltinPatterns": merged})
73
+ return merged
74
+
75
+ def disable(self, keys: Sequence[str]) -> List[str]:
76
+ """Disable template scorers by key, preserving the rest. Returns the resulting list."""
77
+ merged = sorted(set(self._enabled_template_keys()) - set(keys))
78
+ self._request("PUT", "/settings/monitoring-defaults", json={"enabledBuiltinPatterns": merged})
79
+ return merged
80
+
81
+ # ------------------------------------------------------------------
82
+ # Code / external scorers
83
+ # ------------------------------------------------------------------
84
+
85
+ def list(self) -> List[Dict[str, Any]]:
86
+ """All code and external scorers (wire kind: ``"code"`` / ``"external"``)."""
87
+ return self._request("GET", "/custom-evaluators").get("evaluators", [])
88
+
89
+ def create_code(
90
+ self,
91
+ name: str,
92
+ script: str,
93
+ *,
94
+ language: str = "python",
95
+ alert_below: float = 0.5,
96
+ sample_rate: float = 0.1,
97
+ severity: str = "medium",
98
+ enabled: bool = True,
99
+ scope_mode: str = "all",
100
+ agent_ids: Optional[Sequence[str]] = None,
101
+ ) -> Dict[str, Any]:
102
+ """Deploy a code scorer. ``script`` defines ``handler(input, output, expected,
103
+ metadata, trace)`` returning a 0..1 score, ``{"score", "name"?, "metadata"?}``, or
104
+ ``None`` to skip; a score below ``alert_below`` raises a signal."""
105
+ if language not in ("python", "javascript"):
106
+ raise AgentXScorersError('language must be "python" or "javascript"')
107
+ return self._request("POST", "/custom-evaluators", json={
108
+ "name": name,
109
+ "kind": "code",
110
+ "language": language,
111
+ "script": script,
112
+ "alertBelow": alert_below,
113
+ "sampleRate": sample_rate,
114
+ "severity": severity,
115
+ "enabled": enabled,
116
+ "scopeMode": scope_mode,
117
+ "agentIds": list(agent_ids) if agent_ids else [],
118
+ })["evaluator"]
119
+
120
+ def create_external(
121
+ self,
122
+ name: str,
123
+ url: str,
124
+ *,
125
+ sample_rate: float = 0.1,
126
+ severity: str = "medium",
127
+ enabled: bool = True,
128
+ invert_match: bool = False,
129
+ scope_mode: str = "all",
130
+ agent_ids: Optional[Sequence[str]] = None,
131
+ ) -> Dict[str, Any]:
132
+ """Register an external scorer endpoint (POSTed the v2 payload per sampled trace)."""
133
+ return self._request("POST", "/custom-evaluators", json={
134
+ "name": name,
135
+ "url": url,
136
+ "sampleRate": sample_rate,
137
+ "severity": severity,
138
+ "enabled": enabled,
139
+ "invertMatch": invert_match,
140
+ "scopeMode": scope_mode,
141
+ "agentIds": list(agent_ids) if agent_ids else [],
142
+ })["evaluator"]
143
+
144
+ def update(self, scorer_id: str, **fields: Any) -> Dict[str, Any]:
145
+ """Update a code/external scorer. snake_case kwargs are converted (``alert_below`` ->
146
+ ``alertBelow`` etc.); kind is immutable."""
147
+ wire = {_SNAKE_TO_WIRE.get(k, k): v for k, v in fields.items()}
148
+ return self._request("PUT", f"/custom-evaluators/{scorer_id}", json=wire)["evaluator"]
149
+
150
+ def delete(self, scorer_id: str) -> None:
151
+ self._request("DELETE", f"/custom-evaluators/{scorer_id}")
152
+
153
+ def events(self, scorer_id: str, window: str = "24h") -> List[Dict[str, Any]]:
154
+ """The scorer's per-check history (score, matched, justification, trace ids)."""
155
+ return self._request("GET", f"/custom-evaluators/{scorer_id}/events", params={"window": window}).get("events", [])
156
+
157
+ def dry_run(self, **payload: Any) -> Dict[str, Any]:
158
+ """Execute a scorer against the built-in sample without persisting: pass either
159
+ ``url=...`` (external) or ``kind="code", language=..., script=...`` (code)."""
160
+ wire = {_SNAKE_TO_WIRE.get(k, k): v for k, v in payload.items()}
161
+ return self._request("POST", "/custom-evaluators/dry-run", json=wire)
162
+
163
+
164
+ _SNAKE_TO_WIRE = {
165
+ "alert_below": "alertBelow",
166
+ "sample_rate": "sampleRate",
167
+ "scope_mode": "scopeMode",
168
+ "agent_ids": "agentIds",
169
+ "invert_match": "invertMatch",
170
+ }
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import requests
7
+
8
+ from agentx.util import api_base, get_headers
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AgentXProjectsError(Exception):
14
+ pass
15
+
16
+
17
+ class ProjectsClient:
18
+ """Surfaced as ``client.projects``: create, list, and delete the engine's projects
19
+ (self-host). Each project is a fully isolated tenant - own API key, own traces, scorers,
20
+ datasets, and settings. ``create()`` returns the new project's ``apiKey``; construct a new
21
+ ``AgentX(api_key=...)`` with it to work inside that project (the pattern integration tests
22
+ use for per-run isolation).
23
+
24
+ In ``AGENTX_AUTH=enabled`` mode project management is session-scoped to signed-in dashboard
25
+ users; this client covers the default self-host (auth-disabled) mode.
26
+ """
27
+
28
+ def __init__(self, api_key: Optional[str] = None):
29
+ self._api_key = api_key
30
+
31
+ def _request(self, method: str, path: str, json: Any = None) -> Any:
32
+ resp = requests.request(
33
+ method,
34
+ f"{api_base()}{path}",
35
+ headers={**get_headers(self._api_key), "Content-Type": "application/json"},
36
+ json=json,
37
+ timeout=15,
38
+ )
39
+ if resp.status_code >= 400:
40
+ try:
41
+ detail = resp.json().get("error", resp.reason)
42
+ except ValueError:
43
+ detail = resp.reason
44
+ raise AgentXProjectsError(f"Projects request failed ({resp.status_code}): {detail}")
45
+ return resp.json() if resp.text else {}
46
+
47
+ def create(self, name: str) -> Dict[str, Any]:
48
+ """Create a project; the returned dict includes ``_id``, ``name``, and ``apiKey``."""
49
+ return self._request("POST", "/projects", json={"name": name})["project"]
50
+
51
+ def list(self) -> List[Dict[str, Any]]:
52
+ """All projects on the instance, each with its ``apiKey`` and ``isDefault`` flag."""
53
+ return self._request("GET", "/projects").get("projects", [])
54
+
55
+ def delete(self, project_id: str) -> None:
56
+ """Delete a project and every row it owns (traces, scorers, datasets, runs). The
57
+ default project cannot be deleted. Irreversible."""
58
+ self._request("DELETE", f"/projects/{project_id}")
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import requests
7
+
8
+ from agentx.util import api_base, get_headers
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AgentXTracesError(Exception):
14
+ pass
15
+
16
+
17
+ class TracesClient:
18
+ """Surfaced as ``client.traces``: the READ side of tracing (``client.tracer`` writes).
19
+
20
+ ``get(trace_id)`` returns one trace's full detail (input/output/error, model, latency,
21
+ token counts incl. cache, session/span linkage, metadata, estimated cost) - the same wire
22
+ the dashboard's trace dialog reads. ``list()`` pages through the project's traces newest
23
+ first. For a whole conversation, ``client.monitor.sessions.spans(session_id)`` remains the
24
+ span-tree read.
25
+ """
26
+
27
+ def __init__(self, api_key: Optional[str] = None):
28
+ self._api_key = api_key
29
+
30
+ def _request(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
31
+ resp = requests.get(
32
+ f"{api_base()}{path}",
33
+ headers=get_headers(self._api_key),
34
+ params=params or {},
35
+ timeout=15,
36
+ )
37
+ if resp.status_code >= 400:
38
+ try:
39
+ detail = resp.json().get("error", resp.reason)
40
+ except ValueError:
41
+ detail = resp.reason
42
+ raise AgentXTracesError(f"Trace request failed ({resp.status_code}): {detail}")
43
+ return resp.json()
44
+
45
+ def get(self, trace_id: str) -> Dict[str, Any]:
46
+ """One trace's detail row. Raises on 404."""
47
+ return self._request(f"/ingest/traces/{trace_id}")
48
+
49
+ def list(
50
+ self,
51
+ limit: int = 50,
52
+ cursor: Optional[str] = None,
53
+ framework: Optional[str] = None,
54
+ ) -> Dict[str, Any]:
55
+ """A page of traces, newest first: ``{"traces": [...], "nextCursor": str | None}``.
56
+ Pass ``cursor`` from the previous page to continue."""
57
+ params: Dict[str, Any] = {"limit": limit}
58
+ if cursor:
59
+ params["cursor"] = cursor
60
+ if framework:
61
+ params["framework"] = framework
62
+ return self._request("/ingest/traces", params)
@@ -153,6 +153,15 @@ class _TraceSpan:
153
153
  if exc_val is not None and self._error is None:
154
154
  self._error = str(exc_val)
155
155
 
156
+ if self._sync and self._parent_span_id is None:
157
+ # sync=True means the WHOLE tree is delivered before this block returns: child
158
+ # spans (tool calls, LLM calls) were enqueued asynchronously during the block, so
159
+ # drain them before the root's own synchronous send. Without this, read-after-trace
160
+ # intermittently misses children (root lands, children still in flight) - the exact
161
+ # race the enterprise assessment reproduced (P0.1). Bounded by the same 5s budget
162
+ # flush() uses; child-only spans keep their async fire-and-forget behavior.
163
+ self._tracer.flush(timeout=5.0)
164
+
156
165
  self._trace_id = self._tracer._send(
157
166
  sync=self._sync,
158
167
  monitor=self._monitor,
@@ -833,7 +842,10 @@ class Tracer:
833
842
  By default the trace is queued and sent on a background thread - fire-and-forget, never
834
843
  blocks the caller, but there's no way to learn the resulting trace_id. Pass ``sync=True``
835
844
  to send it synchronously instead (blocks until ingested) so ``span.trace_id`` is populated
836
- once the ``with`` block exits - e.g. to attach the trace to an evaluation result::
845
+ once the ``with`` block exits. On a root span, ``sync=True`` covers the WHOLE tree: any
846
+ child spans recorded inside the block (tool calls, LLM calls) are drained before the
847
+ root is sent, so a read immediately after the block sees every span, not just the root.
848
+ Use it e.g. to attach the trace to an evaluation result::
837
849
 
838
850
  with client.tracer.trace("support_agent_call", framework="openai", sync=True) as span:
839
851
  resp = call_llm(...)
@@ -0,0 +1 @@
1
+ VERSION = "0.6.34"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.32
3
+ Version: 0.6.34
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
@@ -5,9 +5,12 @@ agentx/__init__.py
5
5
  agentx/agentx.py
6
6
  agentx/cli.py
7
7
  agentx/exceptions.py
8
+ agentx/export.py
8
9
  agentx/feedback.py
9
10
  agentx/outcomes.py
11
+ agentx/projects.py
10
12
  agentx/py.typed
13
+ agentx/traces.py
11
14
  agentx/util.py
12
15
  agentx/version.py
13
16
  agentx/evaluations/__init__.py
@@ -47,6 +50,7 @@ agentx/monitor/models.py
47
50
  agentx/monitor/online_evaluators.py
48
51
  agentx/monitor/patterns.py
49
52
  agentx/monitor/profile.py
53
+ agentx/monitor/scorers.py
50
54
  agentx/monitor/sessions.py
51
55
  agentx/monitor/signals.py
52
56
  agentx/resources/__init__.py
@@ -1 +0,0 @@
1
- VERSION = "0.6.32"
File without changes
File without changes
File without changes
File without changes