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.
- {agentx_python-0.6.31 → agentx_python-0.6.33}/PKG-INFO +1 -1
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/agentx.py +10 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/_term.py +6 -1
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/datasets.py +16 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/models.py +59 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/results.py +1 -2
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/runner.py +41 -26
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/client.py +4 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/models.py +1 -2
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/profile.py +2 -4
- agentx_python-0.6.33/agentx/monitor/scorers.py +170 -0
- agentx_python-0.6.33/agentx/projects.py +58 -0
- agentx_python-0.6.33/agentx/traces.py +62 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/tracer.py +13 -1
- agentx_python-0.6.33/agentx/version.py +1 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/PKG-INFO +1 -1
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/SOURCES.txt +3 -1
- agentx_python-0.6.31/agentx/evaluations/redaction.py +0 -57
- agentx_python-0.6.31/agentx/version.py +0 -1
- {agentx_python-0.6.31 → agentx_python-0.6.33}/LICENSE +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/README.md +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/cli.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/client.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/evaluation_settings.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/exceptions.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/feedback.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/outcomes.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/py.typed +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/agent.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx/util.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/setup.cfg +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/setup.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_integration.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_integrations.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.6.31 → agentx_python-0.6.33}/tests/test_span_tree.py +0 -0
|
@@ -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 =
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
+
_say(f" {green('✓')} Finalized")
|
|
224
235
|
logger.info("Run %s finalized", self._run.run_id)
|
|
225
236
|
except Exception as exc:
|
|
226
|
-
|
|
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
|
-
|
|
270
|
+
_say()
|
|
260
271
|
for check in result.checks:
|
|
261
272
|
mark = green("✓") if check.get("passed") else red("✗")
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
+
_say(f" {red('✗')} Analyze failed: {dim(reason)}")
|
|
363
378
|
else:
|
|
364
|
-
|
|
379
|
+
_say(f" {green('✓')} Analysis complete")
|
|
365
380
|
except Exception as exc:
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|