agentx-python 0.6.31__tar.gz → 0.6.33__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.31 → agentx_python-0.6.33}/PKG-INFO +1 -1
  2. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/agentx.py +10 -0
  3. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/_term.py +6 -1
  4. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/datasets.py +16 -0
  5. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/models.py +59 -0
  6. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/results.py +1 -2
  7. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/runner.py +41 -26
  8. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/client.py +4 -0
  9. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/models.py +1 -2
  10. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/profile.py +2 -4
  11. agentx_python-0.6.33/agentx/monitor/scorers.py +170 -0
  12. agentx_python-0.6.33/agentx/projects.py +58 -0
  13. agentx_python-0.6.33/agentx/traces.py +62 -0
  14. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/tracer.py +13 -1
  15. agentx_python-0.6.33/agentx/version.py +1 -0
  16. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/PKG-INFO +1 -1
  17. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/SOURCES.txt +3 -1
  18. agentx_python-0.6.31/agentx/evaluations/redaction.py +0 -57
  19. agentx_python-0.6.31/agentx/version.py +0 -1
  20. {agentx_python-0.6.31 → agentx_python-0.6.33}/LICENSE +0 -0
  21. {agentx_python-0.6.31 → agentx_python-0.6.33}/README.md +0 -0
  22. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/__init__.py +0 -0
  23. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/cli.py +0 -0
  24. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/__init__.py +0 -0
  25. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/__init__.py +0 -0
  26. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  27. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/precomputed.py +0 -0
  28. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/raw.py +0 -0
  29. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/client.py +0 -0
  30. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/evaluation_settings.py +0 -0
  31. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/prompts.py +0 -0
  32. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/reporting.py +0 -0
  33. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/tool_schemas.py +0 -0
  34. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/tracing.py +0 -0
  35. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/exceptions.py +0 -0
  36. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/feedback.py +0 -0
  37. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/__init__.py +0 -0
  38. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/_traced_call.py +0 -0
  39. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/anthropic.py +0 -0
  40. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/autogen.py +0 -0
  41. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/crewai.py +0 -0
  42. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/databricks.py +0 -0
  43. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/google_adk.py +0 -0
  44. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/google_genai.py +0 -0
  45. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/langchain.py +0 -0
  46. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/litellm.py +0 -0
  47. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/llamaindex.py +0 -0
  48. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/moveworks.py +0 -0
  49. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/openai.py +0 -0
  50. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/openai_agents.py +0 -0
  51. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/__init__.py +0 -0
  52. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/agents.py +0 -0
  53. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/online_evaluators.py +0 -0
  54. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/patterns.py +0 -0
  55. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/sessions.py +0 -0
  56. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/signals.py +0 -0
  57. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/outcomes.py +0 -0
  58. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/py.typed +0 -0
  59. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/__init__.py +0 -0
  60. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/agent.py +0 -0
  61. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/conversation.py +0 -0
  62. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/workforce.py +0 -0
  63. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/__init__.py +0 -0
  64. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/ci_types.py +0 -0
  65. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/ingest_client.py +0 -0
  66. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/util.py +0 -0
  67. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/dependency_links.txt +0 -0
  68. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/entry_points.txt +0 -0
  69. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/not-zip-safe +0 -0
  70. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/requires.txt +0 -0
  71. {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/top_level.txt +0 -0
  72. {agentx_python-0.6.31 → agentx_python-0.6.33}/setup.cfg +0 -0
  73. {agentx_python-0.6.31 → agentx_python-0.6.33}/setup.py +0 -0
  74. {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_integration.py +0 -0
  75. {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_integrations.py +0 -0
  76. {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_selfhost_analysis_fallback.py +0 -0
  77. {agentx_python-0.6.31 → agentx_python-0.6.33}/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.31
3
+ Version: 0.6.33
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,16 @@ 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
+
63
73
  from agentx.feedback import FeedbackClient
64
74
 
65
75
  # 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
  # ---------------------------------------------------------------------------
@@ -10,7 +10,6 @@ from agentx.evaluations.models import (
10
10
  ResultTimings,
11
11
  )
12
12
  from agentx.evaluations.tracing import build_trace
13
- from agentx.evaluations.redaction import redact_dict
14
13
 
15
14
 
16
15
  def _to_int(value: Any) -> Optional[int]:
@@ -59,7 +58,7 @@ def normalize_result(
59
58
  retrieval_context = raw.get("retrieval_context") or raw.get("retrievalContext")
60
59
  meta_raw = raw.get("metadata")
61
60
  if isinstance(meta_raw, dict):
62
- metadata = redact_dict(meta_raw)
61
+ metadata = meta_raw
63
62
 
64
63
  # Extract token counts - top-level keys take priority, fall back to metadata
65
64
  input_tokens = _to_int(raw.get("input_tokens"))
@@ -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
@@ -21,7 +22,6 @@ from agentx.evaluations.models import (
21
22
  ModelInfo,
22
23
  Report,
23
24
  )
24
- from agentx.evaluations.redaction import redact_dict
25
25
  from agentx.evaluations.reporting import print_report
26
26
  from agentx.evaluations.results import normalize_result, normalize_error
27
27
  from agentx.evaluations._term import (
@@ -38,6 +38,17 @@ from agentx.evaluations._term import (
38
38
 
39
39
  logger = logging.getLogger(__name__)
40
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
+
41
52
  AdapterLike = Union[
42
53
  Callable[[EvaluationCase], Any],
43
54
  RawCallableAdapter,
@@ -127,18 +138,18 @@ class EvaluationRunContext:
127
138
  )
128
139
  n_smoke = sum(1 for c in cases if c.is_smoke_test_variant)
129
140
 
130
- print(cyan(sep))
131
- print(f" {bold('AgentX Evaluation')} {dim(' - ')} {name}")
132
- print(cyan(sep))
133
- 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)}")
134
145
  if display:
135
- print(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
136
- print()
146
+ _say(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
147
+ _say()
137
148
  exec_line = f"{bold('Executing')} {n_q} question{'s' if n_q != 1 else ''} × {n_r} run{'s' if n_r != 1 else ''}"
138
149
  if n_smoke:
139
150
  variant_word = "variant" if n_smoke == 1 else "variants"
140
151
  exec_line += f" {dim(f'(+{n_smoke} smoke-test {variant_word})')}"
141
- print(exec_line)
152
+ _say(exec_line)
142
153
 
143
154
  # Resume: skip already-submitted keys
144
155
  already_done = self._fetch_submitted_keys()
@@ -186,7 +197,7 @@ class EvaluationRunContext:
186
197
  resp = self._client.append_results(self._run.run_id, batch_id, batch)
187
198
  if resp.live_statistics is not None:
188
199
  self._live_stats = resp.live_statistics
189
- print(
200
+ _say(
190
201
  f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
191
202
  )
192
203
  logger.info(
@@ -197,7 +208,7 @@ class EvaluationRunContext:
197
208
  resp.failed_validation,
198
209
  )
199
210
  except Exception as exc:
200
- print(f" {red('✗')} Scoring failed: {dim(str(exc))}")
211
+ _say(f" {red('✗')} Scoring failed: {dim(str(exc))}")
201
212
  logger.error("Failed to submit batch %s: %s", batch_id[:8], exc)
202
213
 
203
214
  def _fetch_submitted_keys(self) -> Set[str]:
@@ -214,16 +225,16 @@ class EvaluationRunContext:
214
225
  # ------------------------------------------------------------------
215
226
 
216
227
  def finalize(self) -> "EvaluationRunContext":
217
- print()
228
+ _say()
218
229
  with Spinner("Finalizing - submitting results"):
219
230
  try:
220
231
  data = self._client.finalize_run(self._run.run_id)
221
232
  if isinstance(data, dict) and data.get("liveStatistics") is not None:
222
233
  self._live_stats = LiveStatistics(**data["liveStatistics"])
223
- print(f" {green('✓')} Finalized")
234
+ _say(f" {green('✓')} Finalized")
224
235
  logger.info("Run %s finalized", self._run.run_id)
225
236
  except Exception as exc:
226
- print(f" {red('✗')} Finalize failed: {dim(str(exc))}")
237
+ _say(f" {red('✗')} Finalize failed: {dim(str(exc))}")
227
238
  logger.error("Finalize failed: %s", exc)
228
239
  return self
229
240
 
@@ -256,11 +267,11 @@ class EvaluationRunContext:
256
267
  caller=caller,
257
268
  )
258
269
  result = GateResult(data)
259
- print()
270
+ _say()
260
271
  for check in result.checks:
261
272
  mark = green("✓") if check.get("passed") else red("✗")
262
- print(f" {mark} [{check.get('check')}] {check.get('detail')}")
263
- 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')}")
264
275
  return result
265
276
 
266
277
  # ------------------------------------------------------------------
@@ -274,9 +285,13 @@ class EvaluationRunContext:
274
285
  def results(self) -> list:
275
286
  """Per-result rows for this run (rating, justification, code scorer rows, trace ids,
276
287
  latency/tokens, similarity metrics) - what the dashboard's run detail table shows,
277
- 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
+
278
292
  detail = self._client.get_run(self.run_id)
279
- 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]
280
295
 
281
296
  @property
282
297
  def run_id(self) -> str:
@@ -336,7 +351,7 @@ class EvaluationRunContext:
336
351
  raise ValueError("judges must contain 1-3 model ids")
337
352
  resolved_judges = judges if judges is not None else [_DEFAULT_JUDGE_MODEL]
338
353
 
339
- print()
354
+ _say()
340
355
  with Spinner("Analyzing - AI is reviewing your results") as spinner:
341
356
  try:
342
357
  self._client.analyze_run(
@@ -356,14 +371,14 @@ class EvaluationRunContext:
356
371
  status = self._client.get_analysis_status(self._run.run_id)
357
372
 
358
373
  if not status.is_terminal:
359
- 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")
360
375
  elif status.status == "failed":
361
376
  reason = status.failure_reason.message if status.failure_reason else "unknown error"
362
- print(f" {red('✗')} Analyze failed: {dim(reason)}")
377
+ _say(f" {red('✗')} Analyze failed: {dim(reason)}")
363
378
  else:
364
- print(f" {green('✓')} Analysis complete")
379
+ _say(f" {green('✓')} Analysis complete")
365
380
  except Exception as exc:
366
- print(f" {red('✗')} Analyze failed: {dim(str(exc))}")
381
+ _say(f" {red('✗')} Analyze failed: {dim(str(exc))}")
367
382
  logger.warning("Analyze request failed: %s", exc)
368
383
 
369
384
  try:
@@ -375,7 +390,7 @@ class EvaluationRunContext:
375
390
  # the one signal that something went wrong used to be a logger.warning that is
376
391
  # invisible unless the caller configured logging. Say it on stdout, and let the
377
392
  # status carry the truth for anything reading the object.
378
- print(f" {red('✗')} Could not fetch the report: {dim(str(exc))}")
393
+ _say(f" {red('✗')} Could not fetch the report: {dim(str(exc))}")
379
394
  logger.warning("Could not fetch report: %s", exc)
380
395
  report = Report(
381
396
  runId=self._run.run_id,
@@ -384,7 +399,7 @@ class EvaluationRunContext:
384
399
  )
385
400
 
386
401
  self._report = report
387
- print()
402
+ _say()
388
403
  print_report(report)
389
404
  return report
390
405
 
@@ -616,4 +631,4 @@ def _print_progress(
616
631
  line = f" {tag} {counter} {label} {query_preview}"
617
632
  if suffix:
618
633
  line += f" {suffix}"
619
- print(line)
634
+ _say(line)
@@ -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
@@ -122,7 +122,7 @@ class MonitorProfile(BaseModel):
122
122
  ``None`` from ``get()`` means this agent has never been configured and is running on
123
123
  platform defaults (e.g. the built-in latency threshold defaults to 20000ms).
124
124
 
125
- Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``/``redaction_mode``, and
125
+ Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``, and
126
126
  ``threshold_overrides["latencyMs"]`` are project-level defaults now (set once for every agent
127
127
  via the dashboard's Platform Settings screen), not real per-agent settings - this model and
128
128
  ``update()`` still accept/return them for wire compatibility, but a self-host engine no longer
@@ -141,7 +141,6 @@ class MonitorProfile(BaseModel):
141
141
  channels: List[str] = Field(default_factory=list)
142
142
  threshold_overrides: Optional[Dict[str, Any]] = Field(default=None, alias="thresholdOverrides")
143
143
  retention_days: int = Field(default=30, alias="retentionDays")
144
- redaction_mode: str = Field(default="standard", alias="redactionMode")
145
144
  approval_policy: Optional[Dict[str, str]] = Field(default=None, alias="approvalPolicy")
146
145
  created_at: Optional[str] = Field(default=None, alias="createdAt")
147
146
  updated_at: Optional[str] = Field(default=None, alias="updatedAt")
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
13
13
 
14
14
  class MonitorProfileClient:
15
15
  """Thin wrapper surfaced as ``client.monitor.profile``: get/update a single agent's Monitor
16
- coverage and detection settings (coverage mode, sample rate, retention, redaction, approval
16
+ coverage and detection settings (coverage mode, sample rate, retention, approval
17
17
  policy, and threshold_overrides, e.g. the built-in "Latency regression" pattern's threshold).
18
18
 
19
19
  Unlike patterns/signals, a profile is scoped to one agent per call, since that mirrors how
@@ -41,13 +41,12 @@ class MonitorProfileClient:
41
41
  dataset_id: Optional[str] = None,
42
42
  threshold_overrides: Optional[Dict[str, Any]] = None,
43
43
  retention_days: Optional[int] = None,
44
- redaction_mode: Optional[str] = None,
45
44
  approval_policy: Optional[Dict[str, str]] = None,
46
45
  ) -> MonitorProfile:
47
46
  """Update (and enable, if not already) this agent's Monitor profile. Only fields passed
48
47
  here are changed; everything else on the existing profile is left as is.
49
48
 
50
- Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``/``redaction_mode``,
49
+ Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``,
51
50
  and ``threshold_overrides["latencyMs"]`` are project-level defaults now (see
52
51
  ``MonitorProfile``'s docstring) - set them via the dashboard's Platform Settings screen
53
52
  instead, passing them here is accepted but has no effect. ``enabled``/
@@ -67,7 +66,6 @@ class MonitorProfileClient:
67
66
  "datasetId": dataset_id,
68
67
  "thresholdOverrides": threshold_overrides,
69
68
  "retentionDays": retention_days,
70
- "redactionMode": redaction_mode,
71
69
  "approvalPolicy": approval_policy,
72
70
  }
73
71
  payload = {k: v for k, v in payload.items() if v is not None}
@@ -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.33"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.31
3
+ Version: 0.6.33
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,7 +7,9 @@ agentx/cli.py
7
7
  agentx/exceptions.py
8
8
  agentx/feedback.py
9
9
  agentx/outcomes.py
10
+ agentx/projects.py
10
11
  agentx/py.typed
12
+ agentx/traces.py
11
13
  agentx/util.py
12
14
  agentx/version.py
13
15
  agentx/evaluations/__init__.py
@@ -17,7 +19,6 @@ agentx/evaluations/datasets.py
17
19
  agentx/evaluations/evaluation_settings.py
18
20
  agentx/evaluations/models.py
19
21
  agentx/evaluations/prompts.py
20
- agentx/evaluations/redaction.py
21
22
  agentx/evaluations/reporting.py
22
23
  agentx/evaluations/results.py
23
24
  agentx/evaluations/runner.py
@@ -48,6 +49,7 @@ agentx/monitor/models.py
48
49
  agentx/monitor/online_evaluators.py
49
50
  agentx/monitor/patterns.py
50
51
  agentx/monitor/profile.py
52
+ agentx/monitor/scorers.py
51
53
  agentx/monitor/sessions.py
52
54
  agentx/monitor/signals.py
53
55
  agentx/resources/__init__.py
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import Any, Dict
5
-
6
- # Patterns that look like secrets
7
- _SECRET_PATTERNS = [
8
- re.compile(r"sk-[A-Za-z0-9]{20,}"), # OpenAI / Anthropic style keys
9
- re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", re.IGNORECASE),
10
- re.compile(r"(?i)(password|passwd|secret|token|api[_-]?key)\s*[=:]\s*\S+"),
11
- re.compile(r"[A-Za-z0-9+/]{40,}={0,2}"), # long base64-like strings
12
- ]
13
-
14
- _REDACTED = "[REDACTED]"
15
-
16
-
17
- def redact_string(value: str) -> str:
18
- for pattern in _SECRET_PATTERNS:
19
- value = pattern.sub(_REDACTED, value)
20
- return value
21
-
22
-
23
- def redact_dict(obj: Any, _depth: int = 0) -> Any:
24
- if _depth > 10:
25
- return obj
26
- if isinstance(obj, dict):
27
- return {k: _redact_value(k, v, _depth) for k, v in obj.items()}
28
- if isinstance(obj, list):
29
- return [redact_dict(item, _depth + 1) for item in obj]
30
- if isinstance(obj, str):
31
- return redact_string(obj)
32
- return obj
33
-
34
-
35
- _SENSITIVE_KEYS = {
36
- "password",
37
- "passwd",
38
- "secret",
39
- "token",
40
- "api_key",
41
- "apikey",
42
- "authorization",
43
- "auth",
44
- "cookie",
45
- "session",
46
- "credential",
47
- "private_key",
48
- "privatekey",
49
- "access_key",
50
- "accesskey",
51
- }
52
-
53
-
54
- def _redact_value(key: str, value: Any, depth: int) -> Any:
55
- if isinstance(key, str) and key.lower() in _SENSITIVE_KEYS:
56
- return _REDACTED
57
- return redact_dict(value, depth + 1)
@@ -1 +0,0 @@
1
- VERSION = "0.6.31"
File without changes
File without changes
File without changes
File without changes