agentx-python 0.8.7__py3-none-any.whl → 0.8.8__py3-none-any.whl

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/cli.py CHANGED
@@ -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 _download(url: str, dest: Path) -> None:
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
- web_url = _release_url("agentx-web.tar.gz", version)
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 if not already present there.
138
- Returns the path to the agentx-server executable. Set AGENTX_INSTALL_DIR to change where
139
- this looks/installs; set AGENTX_TRACE_EVAL_VERSION to pin a release tag instead of latest."""
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
- if not server_path.exists():
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)] + sys.argv[1:])
228
+ os.execv(str(server_path), [str(server_path)] + args)
155
229
 
156
230
 
157
231
  if __name__ == "__main__":
agentx/version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "0.8.7"
1
+ VERSION = "0.8.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.7
3
+ Version: 0.8.8
4
4
  Summary: Official Python SDK for AgentX (https://www.agentx.so/)
5
5
  Home-page: https://github.com/AgentX-ai/AgentX-python
6
6
  Author: Robin Wang and AgentX Team
@@ -1,6 +1,6 @@
1
1
  agentx/__init__.py,sha256=4iMiGLNU4S-I-WyX0Z7l0BFUY_krVJ3seMQPqyO-ReA,602
2
2
  agentx/agentx.py,sha256=WuzVQtXSPqBhRwKxkUlQ8VG2cwvGpbMcti4n6aTNPwA,8816
3
- agentx/cli.py,sha256=ueKYPBhW8qd63nvQBQeZ4Mgz9NwEVaM0KH2WvLQ0bGs,6097
3
+ agentx/cli.py,sha256=4c6hPaz5wCvG-IOvzwqlO3PRpkWK4ocf9K-6UXbEfbU,9504
4
4
  agentx/exceptions.py,sha256=2tDLdZjriFBQbjgzyIcah12RazpbujhqZSRbSXgAml4,1377
5
5
  agentx/export.py,sha256=FNT00e-j9AQRkNDksUwgHJXgr8-XVPscEH4xUqJHmfM,4265
6
6
  agentx/feedback.py,sha256=hMPU3ivUHvhOXOH7gvT4Jy1yztVmoIMOw1MbUNRs5Gk,3643
@@ -10,7 +10,7 @@ agentx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  agentx/testing.py,sha256=0shZEid_vhJgBegpO_loUq6APYDxQrUgD3jvcRdIDV8,7372
11
11
  agentx/traces.py,sha256=sz9gxutlDKNgf0fdCkyzKsGwK5eMAJuIEl3NIwGrsuo,2287
12
12
  agentx/util.py,sha256=lt2Kpg4Fj8whrSuAhLEahcPEIYRVpK9RCC5y9ShdTJs,703
13
- agentx/version.py,sha256=kG_bb-Ryd4AoiM5iOElOryBDv_WEdw73ouYtbd5SIQE,18
13
+ agentx/version.py,sha256=KtsrDPnnfwQ504ziRmPTLeUxADR8tN7iC1IRjn_cl_M,18
14
14
  agentx/evaluations/__init__.py,sha256=Erv7RGFlRxGTG4rVb2uHhCqLLEX_iAWKqkZKzK6CumE,262
15
15
  agentx/evaluations/_term.py,sha256=WFpiNzdgDBeJJ-Gg-6X7TwwxllobuE4OqFUTuDQvS3Y,2529
16
16
  agentx/evaluations/client.py,sha256=miUmEq4IthUDl-QVAczqT7iaOu4X6QMbHhlm2rSSJpg,31239
@@ -63,9 +63,9 @@ agentx/tracing/ci_types.py,sha256=zFVcZGvc1qbVDdOlEPQ1i5R6terqJHxCzFF4ZjouxxI,14
63
63
  agentx/tracing/eval_scope.py,sha256=ElMbPxpqpQBVuaUnHNlw9RIQu71oyOpd0yR55bDH8eI,2144
64
64
  agentx/tracing/ingest_client.py,sha256=5X6RyxtMq1XYgOzsMd3b2f2qpmYpsMnhKm7NB3oup00,17664
65
65
  agentx/tracing/tracer.py,sha256=qQojU-T4O8IJKyf9vue0Em66qPBTZKl3FMyr3KJKPnI,53770
66
- agentx_python-0.8.7.dist-info/licenses/LICENSE,sha256=gZVsM-nLsE8vlaY6NXXsVoo6IlCClxkToAWmhgT3y_s,10762
67
- agentx_python-0.8.7.dist-info/METADATA,sha256=aZ0vtzylZ2kxkbb5f5jdSDaNmchZreVE-dzKnl33yCA,20992
68
- agentx_python-0.8.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
69
- agentx_python-0.8.7.dist-info/entry_points.txt,sha256=rQqF1JTY3T1yfviBU1r8PU-5mBdfOk6P4bZ1L4BskIM,172
70
- agentx_python-0.8.7.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
71
- agentx_python-0.8.7.dist-info/RECORD,,
66
+ agentx_python-0.8.8.dist-info/licenses/LICENSE,sha256=gZVsM-nLsE8vlaY6NXXsVoo6IlCClxkToAWmhgT3y_s,10762
67
+ agentx_python-0.8.8.dist-info/METADATA,sha256=554KW0UbN4YVvfAtI4sudXO-2Dscr0biu4Q2W8VxaHg,20992
68
+ agentx_python-0.8.8.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
69
+ agentx_python-0.8.8.dist-info/entry_points.txt,sha256=rQqF1JTY3T1yfviBU1r8PU-5mBdfOk6P4bZ1L4BskIM,172
70
+ agentx_python-0.8.8.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
71
+ agentx_python-0.8.8.dist-info/RECORD,,