agentx-python 0.8.6__tar.gz → 0.8.8__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.8.6 → agentx_python-0.8.8}/PKG-INFO +1 -1
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/cli.py +85 -11
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/client.py +27 -1
- agentx_python-0.8.8/agentx/monitor/rules.py +78 -0
- agentx_python-0.8.8/agentx/version.py +1 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/PKG-INFO +1 -1
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/SOURCES.txt +2 -0
- agentx_python-0.8.8/tests/test_cli_launcher.py +89 -0
- agentx_python-0.8.6/agentx/version.py +0 -1
- {agentx_python-0.8.6 → agentx_python-0.8.8}/LICENSE +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/README.md +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/agentx.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/client.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/datasets.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/evaluation_settings.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/models.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/runner.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/export.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/feedback.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/judge_scorers.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/models.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/review_queue.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/scorers.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/projects.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/py.typed +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/resources/agent.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/testing.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/traces.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/tracing/tracer.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx/util.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/setup.cfg +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/setup.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_docs_match_sdk.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_integration.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_integrations.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_judge_scorers.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_runner_features.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_span_tree.py +0 -0
- {agentx_python-0.8.6 → agentx_python-0.8.8}/tests/test_testing.py +0 -0
|
@@ -7,26 +7,35 @@ AgentX SaaS and would never touch it, so it isn't bundled in this package. Inste
|
|
|
7
7
|
downloads the matching release into ~/.agentx/bin the first time it's needed (mirroring
|
|
8
8
|
AgentX-trace-eval's own install.sh) and then hands off to the real `agentx-server` binary.
|
|
9
9
|
|
|
10
|
+
The installed release tag is stamped in ~/.agentx/bin/.version. The launcher never silently
|
|
11
|
+
re-downloads, but it re-installs when you ask: `--update` fetches the newest release (engine +
|
|
12
|
+
dashboard), and setting AGENTX_TRACE_EVAL_VERSION to a tag other than the stamped one switches
|
|
13
|
+
to that release. When running "latest", a quick fail-open check against GitHub tells you if a
|
|
14
|
+
newer release exists.
|
|
15
|
+
|
|
10
16
|
Usage:
|
|
11
17
|
agentx-trace-eval --dev
|
|
18
|
+
agentx-trace-eval --update --dev
|
|
12
19
|
agentx-trace-eval --port 5000 --db-url postgres://...
|
|
13
20
|
"""
|
|
14
21
|
|
|
15
22
|
import os
|
|
16
23
|
import platform
|
|
24
|
+
import re
|
|
17
25
|
import shutil
|
|
18
26
|
import stat
|
|
19
27
|
import sys
|
|
20
28
|
import tarfile
|
|
21
29
|
import tempfile
|
|
22
30
|
from pathlib import Path
|
|
23
|
-
from typing import Tuple
|
|
31
|
+
from typing import Optional, Tuple
|
|
24
32
|
|
|
25
33
|
import requests
|
|
26
34
|
|
|
27
35
|
REPO = "AgentX-ai/AgentX-trace-eval"
|
|
28
36
|
INSTALL_DIR = Path(os.environ.get("AGENTX_INSTALL_DIR", str(Path.home() / ".agentx" / "bin")))
|
|
29
37
|
_BIN_NAMES = ("agentx", "agentx-server", "agentx-engine")
|
|
38
|
+
_VERSION_STAMP = ".version"
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
def _platform_tag() -> Tuple[str, str]:
|
|
@@ -57,12 +66,22 @@ def _release_url(asset: str, version: str) -> str:
|
|
|
57
66
|
return f"https://github.com/{REPO}/releases/download/{version}/{asset}"
|
|
58
67
|
|
|
59
68
|
|
|
60
|
-
def
|
|
69
|
+
def _tag_from_release_url(url: str) -> Optional[str]:
|
|
70
|
+
# GitHub redirects releases/latest/download/... to releases/download/<tag>/...; the final
|
|
71
|
+
# URL is the one place the resolved tag shows up without a second API call.
|
|
72
|
+
match = re.search(r"/releases/download/([^/]+)/", url)
|
|
73
|
+
return match.group(1) if match else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _download(url: str, dest: Path) -> Optional[str]:
|
|
77
|
+
"""Downloads url to dest; returns the release tag resolved from the final (post-redirect)
|
|
78
|
+
URL, or None if it can't be determined."""
|
|
61
79
|
response = requests.get(url, stream=True, timeout=60)
|
|
62
80
|
response.raise_for_status()
|
|
63
81
|
with open(dest, "wb") as f:
|
|
64
82
|
for chunk in response.iter_content(chunk_size=1 << 16):
|
|
65
83
|
f.write(chunk)
|
|
84
|
+
return _tag_from_release_url(response.url)
|
|
66
85
|
|
|
67
86
|
|
|
68
87
|
def _extract_tar(archive: Path, dest_dir: Path) -> None:
|
|
@@ -76,6 +95,25 @@ def _extract_tar(archive: Path, dest_dir: Path) -> None:
|
|
|
76
95
|
tar.extractall(dest_dir) # Python < 3.12: filter kwarg doesn't exist yet
|
|
77
96
|
|
|
78
97
|
|
|
98
|
+
def _stamped_version() -> Optional[str]:
|
|
99
|
+
try:
|
|
100
|
+
return (INSTALL_DIR / _VERSION_STAMP).read_text().strip() or None
|
|
101
|
+
except OSError:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _latest_release_tag() -> Optional[str]:
|
|
106
|
+
"""The newest published tag, or None when it can't be determined (offline, rate-limited).
|
|
107
|
+
Fail-open by design: an update NOTICE must never break launching the engine."""
|
|
108
|
+
try:
|
|
109
|
+
response = requests.get(f"https://api.github.com/repos/{REPO}/releases/latest", timeout=3)
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
tag = response.json().get("tag_name")
|
|
112
|
+
return tag if isinstance(tag, str) and tag else None
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
79
117
|
def _install(version: str = "latest") -> None:
|
|
80
118
|
os_name, arch = _platform_tag()
|
|
81
119
|
INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -86,7 +124,7 @@ def _install(version: str = "latest") -> None:
|
|
|
86
124
|
archive = tmp_dir / "agentx.tar.gz"
|
|
87
125
|
url = _release_url(f"agentx_{os_name}_{arch}.tar.gz", version)
|
|
88
126
|
try:
|
|
89
|
-
_download(url, archive)
|
|
127
|
+
resolved_tag = _download(url, archive)
|
|
90
128
|
except requests.HTTPError as exc:
|
|
91
129
|
raise SystemExit(
|
|
92
130
|
f"agentx-trace-eval: failed to download {url} ({exc}).\n"
|
|
@@ -108,6 +146,11 @@ def _install(version: str = "latest") -> None:
|
|
|
108
146
|
if not found_any:
|
|
109
147
|
raise SystemExit(f"agentx-trace-eval: downloaded archive from {url} didn't contain any of {_BIN_NAMES}")
|
|
110
148
|
|
|
149
|
+
stamp = resolved_tag or (version if version != "latest" else None)
|
|
150
|
+
if stamp:
|
|
151
|
+
(INSTALL_DIR / _VERSION_STAMP).write_text(stamp + "\n")
|
|
152
|
+
print(f"agentx-trace-eval: installed {stamp}", file=sys.stderr)
|
|
153
|
+
|
|
111
154
|
if os.environ.get("AGENTX_TRACE_EVAL_SKIP_WEB"):
|
|
112
155
|
return
|
|
113
156
|
|
|
@@ -118,7 +161,9 @@ def _install(version: str = "latest") -> None:
|
|
|
118
161
|
web_dir = INSTALL_DIR / "web"
|
|
119
162
|
with tempfile.TemporaryDirectory() as tmp:
|
|
120
163
|
web_archive = Path(tmp) / "agentx-web.tar.gz"
|
|
121
|
-
|
|
164
|
+
# Pin the dashboard to the tag the engine actually resolved to, so the two halves of one
|
|
165
|
+
# install can't skew (releases/latest assets are updated independently).
|
|
166
|
+
web_url = _release_url("agentx-web.tar.gz", stamp or version)
|
|
122
167
|
try:
|
|
123
168
|
_download(web_url, web_archive)
|
|
124
169
|
except requests.HTTPError:
|
|
@@ -133,25 +178,54 @@ def _install(version: str = "latest") -> None:
|
|
|
133
178
|
_extract_tar(web_archive, web_dir)
|
|
134
179
|
|
|
135
180
|
|
|
136
|
-
def ensure_installed(version: str = "latest") -> Path:
|
|
137
|
-
"""Downloads agentx-server (+ its engine) into ~/.agentx/bin
|
|
138
|
-
|
|
139
|
-
|
|
181
|
+
def ensure_installed(version: str = "latest", force: bool = False) -> Path:
|
|
182
|
+
"""Downloads agentx-server (+ its engine and dashboard) into ~/.agentx/bin when it's missing
|
|
183
|
+
there, when `force` is set, or when `version` is a specific tag that differs from the
|
|
184
|
+
installed one. Returns the path to the agentx-server executable. Set AGENTX_INSTALL_DIR to
|
|
185
|
+
change where this looks/installs; set AGENTX_TRACE_EVAL_VERSION to pin a release tag."""
|
|
140
186
|
server_path = INSTALL_DIR / "agentx-server"
|
|
141
|
-
|
|
187
|
+
installed = _stamped_version()
|
|
188
|
+
pin_changed = version != "latest" and installed is not None and installed != version
|
|
189
|
+
if force or pin_changed or not server_path.exists():
|
|
142
190
|
_install(version=version)
|
|
143
191
|
return server_path
|
|
144
192
|
|
|
145
193
|
|
|
194
|
+
def _maybe_print_update_notice(installed: Optional[str]) -> None:
|
|
195
|
+
latest = _latest_release_tag()
|
|
196
|
+
if installed is None:
|
|
197
|
+
# Pre-stamp install (or a wiped stamp): age unknown, so always point at --update.
|
|
198
|
+
print(
|
|
199
|
+
"agentx-trace-eval: installed engine version unknown"
|
|
200
|
+
+ (f" (newest release is {latest})" if latest else "")
|
|
201
|
+
+ "; run with --update to fetch the newest release",
|
|
202
|
+
file=sys.stderr,
|
|
203
|
+
)
|
|
204
|
+
elif latest and latest != installed:
|
|
205
|
+
print(
|
|
206
|
+
f"agentx-trace-eval: engine {installed} is installed but {latest} is available; "
|
|
207
|
+
"run with --update to upgrade",
|
|
208
|
+
file=sys.stderr,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
146
212
|
def main() -> None:
|
|
213
|
+
args = sys.argv[1:]
|
|
214
|
+
force_update = "--update" in args
|
|
215
|
+
# --update belongs to this launcher, not to agentx-server - consume it before the handoff.
|
|
216
|
+
args = [a for a in args if a != "--update"]
|
|
217
|
+
|
|
147
218
|
version = os.environ.get("AGENTX_TRACE_EVAL_VERSION", "latest")
|
|
148
|
-
server_path = ensure_installed(version=version)
|
|
219
|
+
server_path = ensure_installed(version=version, force=force_update)
|
|
149
220
|
if not server_path.exists():
|
|
150
221
|
raise SystemExit(f"agentx-trace-eval: {server_path} still missing after install, giving up")
|
|
151
222
|
|
|
223
|
+
if not force_update and version == "latest":
|
|
224
|
+
_maybe_print_update_notice(_stamped_version())
|
|
225
|
+
|
|
152
226
|
# os.execv replaces this process rather than spawning a subprocess: signals, stdio, and the
|
|
153
227
|
# exit code all pass straight through to agentx-server, same as invoking it directly.
|
|
154
|
-
os.execv(str(server_path), [str(server_path)] +
|
|
228
|
+
os.execv(str(server_path), [str(server_path)] + args)
|
|
155
229
|
|
|
156
230
|
|
|
157
231
|
if __name__ == "__main__":
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
|
-
from typing import Any, List, Optional
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
@@ -116,6 +116,10 @@ class MonitorClient:
|
|
|
116
116
|
# The human-review queue (list / queue / label / dismiss) - what makes the
|
|
117
117
|
# label-and-calibrate loop scriptable instead of dashboard-only.
|
|
118
118
|
self.review_queue = ReviewQueueClient(self)
|
|
119
|
+
from agentx.monitor.rules import MonitorRulesClient
|
|
120
|
+
|
|
121
|
+
# Automation rules: route matching traffic into review / a dataset / a webhook.
|
|
122
|
+
self.rules = MonitorRulesClient(self)
|
|
119
123
|
from agentx.monitor.scorers import ScorersClient
|
|
120
124
|
# Scorers-catalog administration as code: template enable/disable, code/external scorer
|
|
121
125
|
# CRUD and dry runs - full parity with the dashboard's Scorers page (P1.3).
|
|
@@ -264,6 +268,28 @@ class MonitorClient:
|
|
|
264
268
|
plus deltas vs the prior window and the run-outcome breakdown."""
|
|
265
269
|
return self._request("GET", "/kpis", params={"window": window})
|
|
266
270
|
|
|
271
|
+
def topics(self, window: str = "7d") -> dict:
|
|
272
|
+
"""The Topics view's data over a window ("24h", "7d", "30d"): LLM-classified themes of
|
|
273
|
+
sampled production traffic with per-topic counts and sentiment. Empty until Topics is
|
|
274
|
+
enabled project-wide via ``set_topics(True)`` - classification spends one judge call
|
|
275
|
+
per sampled trace, so it is off by default."""
|
|
276
|
+
return self._request(
|
|
277
|
+
"GET", "/agent-monitoring/topics",
|
|
278
|
+
base=self._api_root(), params={"window": window},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def set_topics(self, enabled: bool, sample_rate: Optional[float] = None) -> dict:
|
|
282
|
+
"""Turn Topics classification on/off for the whole project (Platform Settings >
|
|
283
|
+
Monitoring Defaults). ``sample_rate`` (0-1) optionally bounds what fraction of traffic
|
|
284
|
+
is classified - each classified trace costs one judge call."""
|
|
285
|
+
payload: Dict[str, Any] = {"topicsEnabled": enabled}
|
|
286
|
+
if sample_rate is not None:
|
|
287
|
+
payload["topicsSampleRate"] = sample_rate
|
|
288
|
+
return self._request(
|
|
289
|
+
"PUT", "/agent-monitoring/settings/monitoring-defaults",
|
|
290
|
+
base=self._api_root(), json=payload,
|
|
291
|
+
)
|
|
292
|
+
|
|
267
293
|
def calibration(self, window: str = "7d") -> "CalibrationSummary":
|
|
268
294
|
"""Project-level judge calibration over a window ("24h", "7d", or "30d"): how often
|
|
269
295
|
AgentX's own verdicts agreed with real-world ground truth reported later (ops outcomes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from agentx.monitor.client import MonitorClient
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MonitorRule(dict):
|
|
13
|
+
"""Wire object for one automation rule (dict subclass so unknown fields round-trip)."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def id(self) -> str:
|
|
17
|
+
return self["_id"]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def enabled(self) -> bool:
|
|
21
|
+
return bool(self.get("enabled"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MonitorRulesClient:
|
|
25
|
+
"""Surfaced as ``client.monitor.rules``: automation rules, evaluated on every ingested root
|
|
26
|
+
trace. A RULE routes traffic somewhere (it never scores): ``action`` is one of
|
|
27
|
+
|
|
28
|
+
- ``"review"`` - sample matching traces into the human-review queue (the stream that feeds
|
|
29
|
+
judge calibration and tuning),
|
|
30
|
+
- ``"dataset"`` - append matching traces as cases on a dataset (``action_config
|
|
31
|
+
{"datasetId": ...}``),
|
|
32
|
+
- ``"webhook"`` - POST the matching trace to your URL (``action_config {"url": ...}``).
|
|
33
|
+
|
|
34
|
+
``filter`` narrows what matches: ``{"model": ..., "status": "error"|"any", "contains": ...,
|
|
35
|
+
"scopeMode": "all"|"selected", "agentIds": [...]}``; ``sample_rate`` (0-1, default 1)
|
|
36
|
+
down-samples the matches.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, client: "MonitorClient"):
|
|
40
|
+
self._client = client
|
|
41
|
+
|
|
42
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
43
|
+
return self._client._request(method, path, base=self._client._api_root(), **kwargs)
|
|
44
|
+
|
|
45
|
+
def list(self) -> List[MonitorRule]:
|
|
46
|
+
data = self._request("GET", "/agent-monitoring/rules")
|
|
47
|
+
return [MonitorRule(r) for r in data.get("rules", [])]
|
|
48
|
+
|
|
49
|
+
def create(
|
|
50
|
+
self,
|
|
51
|
+
name: str,
|
|
52
|
+
action: str,
|
|
53
|
+
*,
|
|
54
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
55
|
+
sample_rate: Optional[float] = None,
|
|
56
|
+
action_config: Optional[Dict[str, Any]] = None,
|
|
57
|
+
enabled: bool = True,
|
|
58
|
+
) -> MonitorRule:
|
|
59
|
+
payload: Dict[str, Any] = {"name": name, "action": action, "enabled": enabled}
|
|
60
|
+
if filter is not None:
|
|
61
|
+
payload["filter"] = filter
|
|
62
|
+
if sample_rate is not None:
|
|
63
|
+
payload["sampleRate"] = sample_rate
|
|
64
|
+
if action_config is not None:
|
|
65
|
+
payload["actionConfig"] = action_config
|
|
66
|
+
data = self._request("POST", "/agent-monitoring/rules", json=payload)
|
|
67
|
+
return MonitorRule(data.get("rule", data))
|
|
68
|
+
|
|
69
|
+
def update(self, rule_id: str, **fields: Any) -> MonitorRule:
|
|
70
|
+
"""Sparse update. snake_case keys are mapped to the wire (``sample_rate`` ->
|
|
71
|
+
``sampleRate``, ``action_config`` -> ``actionConfig``)."""
|
|
72
|
+
aliases = {"sample_rate": "sampleRate", "action_config": "actionConfig"}
|
|
73
|
+
payload = {aliases.get(k, k): v for k, v in fields.items()}
|
|
74
|
+
data = self._request("PUT", f"/agent-monitoring/rules/{rule_id}", json=payload)
|
|
75
|
+
return MonitorRule(data.get("rule", data))
|
|
76
|
+
|
|
77
|
+
def delete(self, rule_id: str) -> None:
|
|
78
|
+
self._request("DELETE", f"/agent-monitoring/rules/{rule_id}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.8.8"
|
|
@@ -53,6 +53,7 @@ agentx/monitor/online_evaluators.py
|
|
|
53
53
|
agentx/monitor/patterns.py
|
|
54
54
|
agentx/monitor/profile.py
|
|
55
55
|
agentx/monitor/review_queue.py
|
|
56
|
+
agentx/monitor/rules.py
|
|
56
57
|
agentx/monitor/scorers.py
|
|
57
58
|
agentx/monitor/sessions.py
|
|
58
59
|
agentx/monitor/signals.py
|
|
@@ -72,6 +73,7 @@ agentx_python.egg-info/entry_points.txt
|
|
|
72
73
|
agentx_python.egg-info/not-zip-safe
|
|
73
74
|
agentx_python.egg-info/requires.txt
|
|
74
75
|
agentx_python.egg-info/top_level.txt
|
|
76
|
+
tests/test_cli_launcher.py
|
|
75
77
|
tests/test_deep_dive_fixes.py
|
|
76
78
|
tests/test_docs_match_sdk.py
|
|
77
79
|
tests/test_eval_scope.py
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The agentx-trace-eval launcher's staleness fixes: before these, ensure_installed downloaded
|
|
2
|
+
once and then ran that binary forever - a July install silently served a July engine months
|
|
3
|
+
later, with no version stamp, no way to ask for an update, and the dashboard fetched from
|
|
4
|
+
releases/latest independently of the engine (so the two halves of one install could skew)."""
|
|
5
|
+
|
|
6
|
+
import io
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from agentx import cli
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def install_dir(tmp_path, monkeypatch):
|
|
16
|
+
monkeypatch.setattr(cli, "INSTALL_DIR", tmp_path)
|
|
17
|
+
return tmp_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _fake_install(calls):
|
|
21
|
+
def fake(version="latest"):
|
|
22
|
+
calls.append(version)
|
|
23
|
+
|
|
24
|
+
return fake
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_tag_resolved_from_redirected_release_url():
|
|
28
|
+
url = "https://github.com/AgentX-ai/AgentX-trace-eval/releases/download/v0.3.0/agentx_darwin_arm64.tar.gz"
|
|
29
|
+
assert cli._tag_from_release_url(url) == "v0.3.0"
|
|
30
|
+
assert cli._tag_from_release_url("https://example.com/not-a-release") is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_existing_install_is_not_redownloaded(install_dir, monkeypatch):
|
|
34
|
+
(install_dir / "agentx-server").write_text("bin")
|
|
35
|
+
calls = []
|
|
36
|
+
monkeypatch.setattr(cli, "_install", _fake_install(calls))
|
|
37
|
+
cli.ensure_installed(version="latest")
|
|
38
|
+
assert calls == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_force_reinstalls_over_existing(install_dir, monkeypatch):
|
|
42
|
+
(install_dir / "agentx-server").write_text("bin")
|
|
43
|
+
calls = []
|
|
44
|
+
monkeypatch.setattr(cli, "_install", _fake_install(calls))
|
|
45
|
+
cli.ensure_installed(version="latest", force=True)
|
|
46
|
+
assert calls == ["latest"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_changed_version_pin_reinstalls(install_dir, monkeypatch):
|
|
50
|
+
(install_dir / "agentx-server").write_text("bin")
|
|
51
|
+
(install_dir / ".version").write_text("v0.2.0\n")
|
|
52
|
+
calls = []
|
|
53
|
+
monkeypatch.setattr(cli, "_install", _fake_install(calls))
|
|
54
|
+
|
|
55
|
+
# Same pin: no download. Different pin: download. "latest": trusts what's there.
|
|
56
|
+
cli.ensure_installed(version="v0.2.0")
|
|
57
|
+
assert calls == []
|
|
58
|
+
cli.ensure_installed(version="v0.3.0")
|
|
59
|
+
assert calls == ["v0.3.0"]
|
|
60
|
+
calls.clear()
|
|
61
|
+
cli.ensure_installed(version="latest")
|
|
62
|
+
assert calls == []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_update_notice_names_both_versions(install_dir, monkeypatch, capsys):
|
|
66
|
+
monkeypatch.setattr(cli, "_latest_release_tag", lambda: "v0.9.0")
|
|
67
|
+
cli._maybe_print_update_notice("v0.2.0")
|
|
68
|
+
err = capsys.readouterr().err
|
|
69
|
+
assert "v0.2.0" in err and "v0.9.0" in err and "--update" in err
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_update_notice_handles_unstamped_install(install_dir, monkeypatch, capsys):
|
|
73
|
+
monkeypatch.setattr(cli, "_latest_release_tag", lambda: None)
|
|
74
|
+
cli._maybe_print_update_notice(None)
|
|
75
|
+
assert "--update" in capsys.readouterr().err
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_update_notice_is_silent_when_current(install_dir, monkeypatch, capsys):
|
|
79
|
+
monkeypatch.setattr(cli, "_latest_release_tag", lambda: "v0.2.0")
|
|
80
|
+
cli._maybe_print_update_notice("v0.2.0")
|
|
81
|
+
assert capsys.readouterr().err == ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_latest_release_tag_fails_open(monkeypatch):
|
|
85
|
+
def boom(*args, **kwargs):
|
|
86
|
+
raise OSError("offline")
|
|
87
|
+
|
|
88
|
+
monkeypatch.setattr(cli.requests, "get", boom)
|
|
89
|
+
assert cli._latest_release_tag() is None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.8.6"
|
|
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
|
|
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
|