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