agentx-python 0.6.28__tar.gz → 0.6.30__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {agentx_python-0.6.28 → agentx_python-0.6.30}/PKG-INFO +1 -1
  2. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/agentx.py +44 -0
  3. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/client.py +106 -1
  4. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/evaluation_settings.py +8 -0
  5. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/prompts.py +19 -0
  6. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/runner.py +22 -0
  7. agentx_python-0.6.30/agentx/evaluations/tool_schemas.py +48 -0
  8. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/exceptions.py +4 -0
  9. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/moveworks.py +56 -8
  10. agentx_python-0.6.30/agentx/monitor/agents.py +28 -0
  11. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/client.py +89 -2
  12. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/online_evaluators.py +21 -0
  13. agentx_python-0.6.30/agentx/monitor/sessions.py +21 -0
  14. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/ingest_client.py +55 -0
  15. agentx_python-0.6.30/agentx/version.py +1 -0
  16. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/PKG-INFO +1 -1
  17. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/SOURCES.txt +3 -0
  18. agentx_python-0.6.28/agentx/version.py +0 -1
  19. {agentx_python-0.6.28 → agentx_python-0.6.30}/LICENSE +0 -0
  20. {agentx_python-0.6.28 → agentx_python-0.6.30}/README.md +0 -0
  21. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/__init__.py +0 -0
  22. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/cli.py +0 -0
  23. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/__init__.py +0 -0
  24. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/_term.py +0 -0
  25. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/__init__.py +0 -0
  26. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  27. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/precomputed.py +0 -0
  28. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/raw.py +0 -0
  29. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/datasets.py +0 -0
  30. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/models.py +0 -0
  31. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/redaction.py +0 -0
  32. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/reporting.py +0 -0
  33. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/results.py +0 -0
  34. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/tracing.py +0 -0
  35. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/feedback.py +0 -0
  36. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/__init__.py +0 -0
  37. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/_traced_call.py +0 -0
  38. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/anthropic.py +0 -0
  39. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/autogen.py +0 -0
  40. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/crewai.py +0 -0
  41. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/databricks.py +0 -0
  42. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/google_adk.py +0 -0
  43. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/google_genai.py +0 -0
  44. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/langchain.py +0 -0
  45. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/litellm.py +0 -0
  46. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/llamaindex.py +0 -0
  47. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/openai.py +0 -0
  48. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/openai_agents.py +0 -0
  49. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/__init__.py +0 -0
  50. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/models.py +0 -0
  51. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/patterns.py +0 -0
  52. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/profile.py +0 -0
  53. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/signals.py +0 -0
  54. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/outcomes.py +0 -0
  55. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/py.typed +0 -0
  56. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/__init__.py +0 -0
  57. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/agent.py +0 -0
  58. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/conversation.py +0 -0
  59. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/workforce.py +0 -0
  60. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/__init__.py +0 -0
  61. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/ci_types.py +0 -0
  62. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/tracer.py +0 -0
  63. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/util.py +0 -0
  64. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/dependency_links.txt +0 -0
  65. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/entry_points.txt +0 -0
  66. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/not-zip-safe +0 -0
  67. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/requires.txt +0 -0
  68. {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/top_level.txt +0 -0
  69. {agentx_python-0.6.28 → agentx_python-0.6.30}/setup.cfg +0 -0
  70. {agentx_python-0.6.28 → agentx_python-0.6.30}/setup.py +0 -0
  71. {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_integration.py +0 -0
  72. {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_integrations.py +0 -0
  73. {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_selfhost_analysis_fallback.py +0 -0
  74. {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_span_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.28
3
+ Version: 0.6.30
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
@@ -111,6 +111,50 @@ class AgentX:
111
111
  f"Failed to list workforces: {response.status_code} - {response.reason}"
112
112
  )
113
113
 
114
+ def ping(self) -> dict:
115
+ """Verify the client can actually reach AgentX and that the API key is accepted.
116
+
117
+ The constructor is deliberately lazy (no network call - standard SDK behavior, so
118
+ offline construction and tests work), and trace delivery is fire-and-forget, so a
119
+ wrong ``base_url`` or ``api_key`` otherwise surfaces only as a one-time warning in
120
+ logs while traces silently go nowhere. Call this once at startup of a long-running
121
+ service to fail fast instead::
122
+
123
+ client = AgentX.from_env()
124
+ client.ping() # raises immediately on a bad URL or key
125
+
126
+ Raises :class:`agentx.exceptions.AgentXConnectionError` when the URL is unreachable,
127
+ :class:`agentx.exceptions.AgentXAuthError` when the key is rejected, and
128
+ :class:`agentx.exceptions.AgentXAPIError` on any other non-OK response. Returns
129
+ ``{"ok": True, "base_url": ...}`` on success.
130
+ """
131
+ from agentx.exceptions import AgentXAPIError, AgentXAuthError, AgentXConnectionError
132
+
133
+ base = api_base()
134
+ # /monitor/patterns: the cheapest key-authenticated endpoint that exists on both the
135
+ # hosted API and the self-host engine's SDK-facing router.
136
+ url = f"{base}/monitor/patterns"
137
+ try:
138
+ response = requests.get(url, headers=get_headers(self.api_key), timeout=10)
139
+ except requests.RequestException as exc:
140
+ raise AgentXConnectionError(
141
+ f"Cannot reach AgentX at {base} ({exc.__class__.__name__}: {exc}). "
142
+ "Check base_url / AGENTX_API_BASE_URL - for self-host it should look like "
143
+ "http://localhost:4700/api/v1."
144
+ ) from exc
145
+ if response.status_code in (401, 403):
146
+ raise AgentXAuthError(
147
+ f"AgentX at {base} rejected the API key (HTTP {response.status_code}). "
148
+ "Check api_key / AGENTX_API_KEY - for self-host, copy the 'Default project "
149
+ "API key' from the engine's startup log."
150
+ )
151
+ if not response.ok:
152
+ raise AgentXAPIError(
153
+ f"AgentX at {base} responded HTTP {response.status_code} to the health probe.",
154
+ status_code=response.status_code,
155
+ )
156
+ return {"ok": True, "base_url": base}
157
+
114
158
  def get_profile(self):
115
159
  """Get the current user's profile information."""
116
160
  url = f"{api_base()}/access/getProfile"
@@ -102,6 +102,8 @@ class EvaluationsClient:
102
102
  self.datasets = DatasetClient(self)
103
103
  self.settings = EvaluationSettingsClient(self)
104
104
  self.prompts = PromptClient(self)
105
+ from agentx.evaluations.tool_schemas import ToolSchemaClient
106
+ self.tool_schemas = ToolSchemaClient(self)
105
107
 
106
108
  # ------------------------------------------------------------------
107
109
  # Low-level HTTP
@@ -136,7 +138,7 @@ class EvaluationsClient:
136
138
  ``retry=False`` disables the backoff loop entirely. Use it for any request that is
137
139
  both slow and billable: the loop retries on ``requests.RequestException``, which
138
140
  includes read timeouts, so a synchronous endpoint that outlives its timeout would
139
- otherwise be re-invoked and paid for up to four times.
141
+ otherwise be re-invoked, and paid for, up to four times.
140
142
  """
141
143
  url = f"{base or self._base_url}{path}"
142
144
  schedule = [0.0] + (_RETRY_BACKOFF if retry else [])
@@ -483,6 +485,109 @@ class EvaluationsClient:
483
485
  **body,
484
486
  )
485
487
 
488
+ # ------------------------------------------------------------------
489
+ # Prompt improvement loop (examples -> propose -> publish). These ride the engine's
490
+ # /evaluate dialect via _api_root(), same precedent get_report/_analysis already use for
491
+ # routes that live on the dashboard router (self-host only).
492
+ # ------------------------------------------------------------------
493
+
494
+ def get_prompt_examples(self, prompt_id: str, window: Optional[str] = None) -> dict:
495
+ params = {"window": window} if window else None
496
+ return self._request(
497
+ "GET", f"/evaluate/prompts/{prompt_id}/examples", base=self._api_root, params=params
498
+ )
499
+
500
+ def propose_prompt(self, prompt_id: str) -> dict:
501
+ # One real judge call - no retry (see _request's retry note).
502
+ return self._request(
503
+ "POST", f"/evaluate/prompts/{prompt_id}/propose", base=self._api_root, timeout=180, retry=False
504
+ )
505
+
506
+ def publish_prompt_version(
507
+ self, prompt_id: str, *, text: str, source: str = "proposed",
508
+ reasoning: Optional[str] = None, based_on_version: Optional[int] = None,
509
+ ) -> dict:
510
+ payload: dict = {"text": text, "source": source}
511
+ if reasoning is not None:
512
+ payload["reasoning"] = reasoning
513
+ if based_on_version is not None:
514
+ payload["basedOnVersion"] = based_on_version
515
+ return self._request(
516
+ "POST", f"/evaluate/prompts/{prompt_id}/versions", base=self._api_root, json=payload
517
+ )
518
+
519
+ # ------------------------------------------------------------------
520
+ # Tool schema registry (same version-scoped propose/publish loop as prompts)
521
+ # ------------------------------------------------------------------
522
+
523
+ def list_tool_schemas(self) -> List[dict]:
524
+ data = self._request("GET", "/evaluate/tool-schemas", base=self._api_root)
525
+ return data.get("toolSchemas", []) if isinstance(data, dict) else data
526
+
527
+ def create_tool_schema(self, *, name: str, definition: str, description: Optional[str] = None) -> dict:
528
+ payload: dict = {"name": name, "definition": definition}
529
+ if description is not None:
530
+ payload["description"] = description
531
+ return self._request("POST", "/evaluate/tool-schemas", base=self._api_root, json=payload)
532
+
533
+ def get_tool_schema_examples(self, tool_schema_id: str, window: Optional[str] = None) -> dict:
534
+ params = {"window": window} if window else None
535
+ return self._request(
536
+ "GET", f"/evaluate/tool-schemas/{tool_schema_id}/examples", base=self._api_root, params=params
537
+ )
538
+
539
+ def propose_tool_schema(self, tool_schema_id: str, window: Optional[str] = None) -> dict:
540
+ payload = {"window": window} if window else {}
541
+ return self._request(
542
+ "POST", f"/evaluate/tool-schemas/{tool_schema_id}/propose",
543
+ base=self._api_root, json=payload, timeout=180, retry=False,
544
+ )
545
+
546
+ def publish_tool_schema_version(
547
+ self, tool_schema_id: str, *, definition: str, source: str = "proposed",
548
+ reasoning: Optional[str] = None, based_on_version: Optional[int] = None,
549
+ ) -> dict:
550
+ payload: dict = {"definition": definition, "source": source}
551
+ if reasoning is not None:
552
+ payload["reasoning"] = reasoning
553
+ if based_on_version is not None:
554
+ payload["basedOnVersion"] = based_on_version
555
+ return self._request(
556
+ "POST", f"/evaluate/tool-schemas/{tool_schema_id}/versions", base=self._api_root, json=payload
557
+ )
558
+
559
+ # ------------------------------------------------------------------
560
+ # CI gate history + conversation simulation (self-host)
561
+ # ------------------------------------------------------------------
562
+
563
+ def list_gates(self) -> List[dict]:
564
+ """Recorded CI gate verdicts, newest first - the dashboard's CI Gates history."""
565
+ data = self._request("GET", "/evaluate/ci/gates", base=self._api_root)
566
+ return data.get("gates", []) if isinstance(data, dict) else data
567
+
568
+ def simulate_conversation(
569
+ self, *, model: str, system_prompt: str, persona: str, goal: str,
570
+ max_turns: int = 5, tools: Optional[List[dict]] = None, agent_name: Optional[str] = None,
571
+ ) -> dict:
572
+ """Run a persona-driven multi-turn simulation against a prompt (the Playground's
573
+ "Simulate conversation"). Blocking and judge-billed: one LLM call per simulated turn
574
+ plus the closing judgment, so expect it to take tens of seconds."""
575
+ payload: dict = {
576
+ "model": model,
577
+ "messages": [{"role": "system", "content": system_prompt}],
578
+ "persona": persona,
579
+ "goal": goal,
580
+ "maxTurns": max_turns,
581
+ }
582
+ if tools:
583
+ payload["tools"] = tools
584
+ if agent_name:
585
+ payload["agentName"] = agent_name
586
+ return self._request(
587
+ "POST", "/evaluate/playground/simulate", base=self._api_root, json=payload,
588
+ timeout=600, retry=False,
589
+ )
590
+
486
591
 
487
592
  # ---------------------------------------------------------------------------
488
593
  # Helpers
@@ -32,6 +32,7 @@ class EvaluationSettingsBuilder:
32
32
  rouge_score: bool = False,
33
33
  similarity_model: Optional[str] = None,
34
34
  sovereignty_models: Optional[List[str]] = None,
35
+ code_scorers: Optional[List[Dict[str, Any]]] = None,
35
36
  ):
36
37
  self._client = client
37
38
  self._payload: Dict[str, Any] = {
@@ -68,6 +69,11 @@ class EvaluationSettingsBuilder:
68
69
  "enabled": True,
69
70
  "models": list(sovereignty_models),
70
71
  }
72
+ # Sandboxed JS scorers run per result alongside the judge - each entry is
73
+ # {"name": ..., "enabled": True, "code": "..."} where the code is a JS function body
74
+ # receiving (input, output, expected, toolCalls) and returning {score, reasoning}.
75
+ if code_scorers:
76
+ self._payload["codeScorers"] = list(code_scorers)
71
77
 
72
78
  def publish(self) -> EvaluationSettings:
73
79
  logger.info("Publishing evaluation settings '%s'", self._payload["name"])
@@ -96,6 +102,7 @@ class EvaluationSettingsClient:
96
102
  rouge_score: bool = False,
97
103
  similarity_model: Optional[str] = None,
98
104
  sovereignty_models: Optional[List[str]] = None,
105
+ code_scorers: Optional[List[Dict[str, Any]]] = None,
99
106
  ) -> EvaluationSettingsBuilder:
100
107
  return EvaluationSettingsBuilder(
101
108
  self._client,
@@ -113,6 +120,7 @@ class EvaluationSettingsClient:
113
120
  rouge_score=rouge_score,
114
121
  similarity_model=similarity_model,
115
122
  sovereignty_models=sovereignty_models,
123
+ code_scorers=code_scorers,
116
124
  )
117
125
 
118
126
  def get(self, evaluation_settings_id: str) -> EvaluationSettings:
@@ -51,5 +51,24 @@ class PromptClient:
51
51
  second lookup method."""
52
52
  return self._client.get_prompt(name, version=version)
53
53
 
54
+ def examples(self, prompt_id: str, window: Optional[str] = None) -> dict:
55
+ """The merged evidence (worst eval-run results + low-rated online-evaluator traffic)
56
+ a propose() call will rewrite from - version-scoped to the prompt's current version."""
57
+ return self._client.get_prompt_examples(prompt_id, window=window)
58
+
59
+ def propose(self, prompt_id: str) -> dict:
60
+ """Ask the judge for a rewrite grounded in examples(). Returns the proposal
61
+ (revisedText/reasoning/sourceBreakdown) without publishing anything."""
62
+ return self._client.propose_prompt(prompt_id)
63
+
64
+ def publish_version(
65
+ self, prompt_id: str, *, text: str, source: str = "proposed",
66
+ reasoning: Optional[str] = None, based_on_version: Optional[int] = None,
67
+ ) -> dict:
68
+ """Publish a new version (the human-approval step of the propose flow)."""
69
+ return self._client.publish_prompt_version(
70
+ prompt_id, text=text, source=source, reasoning=reasoning, based_on_version=based_on_version
71
+ )
72
+
54
73
  def list(self) -> List[Prompt]:
55
74
  return self._client.list_prompts()
@@ -271,6 +271,13 @@ class EvaluationRunContext:
271
271
  # UI reads, computed once in the API.
272
272
  # ------------------------------------------------------------------
273
273
 
274
+ def results(self) -> list:
275
+ """Per-result rows for this run (rating, justification, code scorer rows, trace ids,
276
+ latency/tokens, similarity metrics) - what the dashboard's run detail table shows,
277
+ fetched fresh from the engine."""
278
+ detail = self._client.get_run(self.run_id)
279
+ return detail.get("results", []) if isinstance(detail, dict) else []
280
+
274
281
  @property
275
282
  def run_id(self) -> str:
276
283
  """The server-side run id - handy for fetching the run's full results afterwards."""
@@ -401,6 +408,7 @@ class EvaluationsRunner:
401
408
  self.datasets = client.datasets
402
409
  self.settings = client.settings
403
410
  self.prompts = client.prompts
411
+ self.tool_schemas = client.tool_schemas
404
412
 
405
413
  def list_models(self, provider: Optional[str] = None) -> List[ModelInfo]:
406
414
  """List the LLM models AgentX supports - the same set selectable for
@@ -409,6 +417,20 @@ class EvaluationsRunner:
409
417
  against."""
410
418
  return self._client.list_models(provider)
411
419
 
420
+ def list_gates(self) -> list:
421
+ """Recorded CI gate verdicts, newest first (the dashboard's CI Gates history)."""
422
+ return self._client.list_gates()
423
+
424
+ def simulate_conversation(self, **kwargs) -> dict:
425
+ """Persona-driven multi-turn simulation against a prompt - see
426
+ EvaluationsClient.simulate_conversation for parameters."""
427
+ return self._client.simulate_conversation(**kwargs)
428
+
429
+ def get_run(self, run_id: str) -> dict:
430
+ """Run summary + per-result rows by id, without needing the EvaluationRunContext that
431
+ created it (e.g. from a separate process)."""
432
+ return self._client.get_run(run_id)
433
+
412
434
  def get_analysis_status(self, run_id: str) -> AnalysisStatus:
413
435
  """Check on an in-progress ``.analyze()`` job by run id, without needing
414
436
  the ``EvaluationRunContext`` that started it (e.g. from a separate
@@ -0,0 +1,48 @@
1
+ """Tool schema registry client, surfaced as ``client.evaluations.tool_schemas``.
2
+
3
+ The tool-definition analog of the prompt registry: register the JSON definition your agent
4
+ actually passes to its LLM, let production failures accumulate as evidence against it, then
5
+ propose -> validate -> publish improved versions. See the dashboard's Improve > Tools & MCPs.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import List, Optional, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from agentx.evaluations.client import EvaluationsClient
13
+
14
+
15
+ class ToolSchemaClient:
16
+ def __init__(self, client: "EvaluationsClient"):
17
+ self._client = client
18
+
19
+ def list(self) -> List[dict]:
20
+ return self._client.list_tool_schemas()
21
+
22
+ def create(self, *, name: str, definition: str, description: Optional[str] = None) -> dict:
23
+ return self._client.create_tool_schema(name=name, definition=definition, description=description)
24
+
25
+ def get_or_create(self, *, name: str, definition: str, description: Optional[str] = None) -> dict:
26
+ """Idempotent register: returns the existing schema of this name if present."""
27
+ existing = next((t for t in self.list() if t.get("name") == name), None)
28
+ if existing:
29
+ return existing
30
+ return self.create(name=name, definition=definition, description=description)
31
+
32
+ def examples(self, tool_schema_id: str, window: Optional[str] = None) -> dict:
33
+ """Failure evidence recorded against this tool (agent-tool-failure signals, low-rated
34
+ eval results that called it) - what propose() rewrites from."""
35
+ return self._client.get_tool_schema_examples(tool_schema_id, window=window)
36
+
37
+ def propose(self, tool_schema_id: str, window: Optional[str] = None) -> dict:
38
+ """Judge-written definition rewrite grounded in examples(). Nothing is published."""
39
+ return self._client.propose_tool_schema(tool_schema_id, window=window)
40
+
41
+ def publish_version(
42
+ self, tool_schema_id: str, *, definition: str, source: str = "proposed",
43
+ reasoning: Optional[str] = None, based_on_version: Optional[int] = None,
44
+ ) -> dict:
45
+ return self._client.publish_tool_schema_version(
46
+ tool_schema_id, definition=definition, source=source,
47
+ reasoning=reasoning, based_on_version=based_on_version,
48
+ )
@@ -15,6 +15,10 @@ class AgentXAuthError(AgentXError):
15
15
  """Invalid or missing API key."""
16
16
 
17
17
 
18
+ class AgentXConnectionError(AgentXError):
19
+ """The AgentX API (or self-host engine) could not be reached at the configured base_url."""
20
+
21
+
18
22
  class AgentXAPIError(AgentXError):
19
23
  """Unexpected API error."""
20
24
 
@@ -173,6 +173,17 @@ class MoveworksSyncReport:
173
173
  self.sessions_judged = 0
174
174
  self.sessions_judge_skipped = 0
175
175
  self.sessions_judge_failed = 0
176
+ # --evaluate-against: per-trace offline grading of the imported interactions.
177
+ self.traces_evaluated = 0
178
+ self.trace_eval_skipped_deduped = 0
179
+ self.trace_eval_failed = 0
180
+ self.trace_eval_ratings: List[float] = []
181
+
182
+ @property
183
+ def trace_eval_average(self) -> Optional[float]:
184
+ if not self.trace_eval_ratings:
185
+ return None
186
+ return sum(self.trace_eval_ratings) / len(self.trace_eval_ratings)
176
187
 
177
188
  def __repr__(self) -> str: # also what the CLI prints
178
189
  base = (
@@ -180,6 +191,14 @@ class MoveworksSyncReport:
180
191
  f"ingested={self.ingested}, failed={self.failed}, "
181
192
  f"plugin_calls_attached={self.plugin_calls_attached}, skipped_no_time={self.skipped_no_time}"
182
193
  )
194
+ if self.traces_evaluated or self.trace_eval_failed or self.trace_eval_skipped_deduped:
195
+ avg = self.trace_eval_average
196
+ base += (
197
+ f", traces_evaluated={self.traces_evaluated}"
198
+ + (f" (avg {avg:.1f}/10)" if avg is not None else "")
199
+ + f", eval_skipped_deduped={self.trace_eval_skipped_deduped}"
200
+ + f", eval_failed={self.trace_eval_failed}"
201
+ )
183
202
  if self.sessions_judged or self.sessions_judge_skipped or self.sessions_judge_failed:
184
203
  base += (
185
204
  f", sessions_judged={self.sessions_judged}, "
@@ -369,6 +388,7 @@ class MoveworksImporter:
369
388
  *,
370
389
  monitor: bool = False,
371
390
  judge_sessions: bool = False,
391
+ evaluate_against: Optional[str] = None,
372
392
  dry_run: bool = False,
373
393
  on_payload: Optional[Any] = None,
374
394
  ) -> MoveworksSyncReport:
@@ -376,12 +396,19 @@ class MoveworksImporter:
376
396
  Import every interaction in ``[since, until)``. Safe to re-run over the same window - the
377
397
  engine deduplicates on the deterministic ``span_id`` (and skips re-judging deduped spans).
378
398
 
379
- ``monitor=True`` sets ``monitor: true`` on every trace - the engine's explicit opt-in that
380
- runs pattern/built-in checks (PII, empty response, tool failure, active patterns) on each
381
- imported trace. ``judge_sessions=True`` additionally asks the engine to judge every
399
+ Pattern/built-in checks and trace-scoped online evaluators run on every imported trace
400
+ by default (the engine's normal ingest-time posture); ``monitor=True`` is kept for
401
+ explicitness/compat. ``judge_sessions=True`` additionally asks the engine to judge every
382
402
  imported session with each enabled session-scoped evaluator after the sync; the request
383
403
  carries ``ifStale=true`` so a session already scored (e.g. by the engine's own 24h sweep)
384
- is never judged twice. Returns a summary report.
404
+ is never judged twice.
405
+
406
+ ``evaluate_against=<dataset_or_config_id>`` is the offline-eval path for an agent that
407
+ can't be invoked from outside: each imported interaction's recorded input/output is
408
+ graded against that grading config's criteria (one judge call per NEW interaction -
409
+ spans the engine already had are skipped via its dedupe, so re-syncing a window never
410
+ re-bills). Ratings land as evaluation results linked to each trace, and the report
411
+ carries the average. Returns a summary report.
385
412
  """
386
413
  report = MoveworksSyncReport()
387
414
  conversations = self._conversation_index(since, until)
@@ -407,10 +434,23 @@ class MoveworksImporter:
407
434
  on_payload(wire)
408
435
  if dry_run:
409
436
  continue
410
- if self._ingest.send_trace_sync(wire) is not None:
411
- report.ingested += 1
412
- else:
437
+ detail = self._ingest.send_trace_sync_detailed(wire)
438
+ if detail is None:
413
439
  report.failed += 1
440
+ continue
441
+ report.ingested += 1
442
+ if evaluate_against:
443
+ if detail.get("deduped"):
444
+ report.trace_eval_skipped_deduped += 1
445
+ continue
446
+ try:
447
+ verdict = self._ingest.evaluate_trace(detail["trace_id"], evaluate_against)
448
+ report.traces_evaluated += 1
449
+ if isinstance(verdict.get("rating"), (int, float)):
450
+ report.trace_eval_ratings.append(float(verdict["rating"]))
451
+ except Exception as exc:
452
+ report.trace_eval_failed += 1
453
+ print(f"evaluate_against failed for trace {detail.get('trace_id')}: {exc}", file=sys.stderr)
414
454
 
415
455
  report.session_ids = session_ids
416
456
  if judge_sessions and not dry_run:
@@ -479,7 +519,14 @@ def cli_main(argv: Optional[List[str]] = None) -> None:
479
519
  sync.add_argument(
480
520
  "--monitor",
481
521
  action="store_true",
482
- help="Run pattern/built-in checks (PII, empty response, tool failure, active patterns) on each imported trace",
522
+ help="Kept for compat - pattern/built-in checks and trace-scoped online evaluators already run "
523
+ "on every imported trace by default",
524
+ )
525
+ sync.add_argument(
526
+ "--evaluate-against",
527
+ metavar="DATASET_ID",
528
+ help="Offline eval during sync: grade each NEW imported interaction's recorded input/output "
529
+ "against this grading config (one judge call per interaction; re-synced spans are skipped)",
483
530
  )
484
531
  sync.add_argument(
485
532
  "--judge-sessions",
@@ -522,6 +569,7 @@ def cli_main(argv: Optional[List[str]] = None) -> None:
522
569
  until,
523
570
  monitor=args.monitor,
524
571
  judge_sessions=args.judge_sessions,
572
+ evaluate_against=args.evaluate_against,
525
573
  dry_run=args.dry_run,
526
574
  on_payload=on_payload,
527
575
  )
@@ -0,0 +1,28 @@
1
+ """Agent registry helpers, surfaced as ``client.monitor.agents`` (self-host).
2
+
3
+ Agents in self-host are lightweight name rows - normally auto-created the first time a trace
4
+ arrives under a name. These helpers exist for flows that need the agent id before any traffic
5
+ (e.g. enabling a monitoring profile up front).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import List, Optional, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from agentx.monitor.client import MonitorClient
13
+
14
+
15
+ class MonitorAgentClient:
16
+ def __init__(self, client: "MonitorClient"):
17
+ self._client = client
18
+
19
+ def list(self) -> List[dict]:
20
+ return self._client.list_agents()
21
+
22
+ def create(self, name: str) -> dict:
23
+ return self._client.create_agent(name)
24
+
25
+ def ensure(self, name: str) -> dict:
26
+ """Get-or-create by name - idempotent, safe to re-run."""
27
+ existing = next((a for a in self.list() if a.get("name") == name), None)
28
+ return existing if existing is not None else self.create(name)
@@ -84,6 +84,10 @@ class MonitorClient:
84
84
  self.signals = MonitorSignalClient(self)
85
85
  self.profile = MonitorProfileClient(self)
86
86
  self.online_evaluators = MonitorOnlineEvaluatorClient(self)
87
+ from agentx.monitor.sessions import MonitorSessionClient
88
+ from agentx.monitor.agents import MonitorAgentClient
89
+ self.sessions = MonitorSessionClient(self)
90
+ self.agents = MonitorAgentClient(self)
87
91
 
88
92
  # ------------------------------------------------------------------
89
93
  # Low-level HTTP
@@ -97,8 +101,17 @@ class MonitorClient:
97
101
  def _workspace_params(self) -> Optional[dict]:
98
102
  return {"workspaceId": self._workspace_id} if self._workspace_id else None
99
103
 
100
- def _request(self, method: str, path: str, timeout: int = 30, **kwargs) -> Any:
101
- url = f"{self._base_url}{path}"
104
+ def _api_root(self) -> str:
105
+ """The API base with the ``/monitor`` suffix removed - for the handful of self-host
106
+ routes that live on the engine's other routers (ingest sessions, agent-monitoring
107
+ calibration/tuning/portability), same precedent EvaluationsClient._api_root sets."""
108
+ suffix = "/monitor"
109
+ if self._base_url.endswith(suffix):
110
+ return self._base_url[: -len(suffix)]
111
+ return self._base_url
112
+
113
+ def _request(self, method: str, path: str, timeout: int = 30, base: Optional[str] = None, **kwargs) -> Any:
114
+ url = f"{base or self._base_url}{path}"
102
115
  last_exc: Optional[Exception] = None
103
116
  for attempt, wait in enumerate([0.0] + _RETRY_BACKOFF):
104
117
  if wait:
@@ -228,6 +241,80 @@ class MonitorClient:
228
241
  profile = data.get("profile")
229
242
  return MonitorProfile(**profile) if profile else None
230
243
 
244
+ # ------------------------------------------------------------------
245
+ # Agents (the engine's SDK-facing /agents router - self-host)
246
+ # ------------------------------------------------------------------
247
+
248
+ def list_agents(self) -> List[dict]:
249
+ data = self._request("GET", "/agents", base=self._api_root())
250
+ return data.get("agents", []) if isinstance(data, dict) else data
251
+
252
+ def create_agent(self, name: str) -> dict:
253
+ data = self._request("POST", "/agents", base=self._api_root(), json={"name": name})
254
+ return data.get("agent", data) if isinstance(data, dict) else data
255
+
256
+ # ------------------------------------------------------------------
257
+ # Sessions (self-host)
258
+ # ------------------------------------------------------------------
259
+
260
+ def run_session_coherence_check(self, session_id: str) -> dict:
261
+ """One judge call over the assembled session - the dashboard's "Check coherence"
262
+ button. Raises AgentXMonitorError if the engine has no judge key configured."""
263
+ data = self._request(
264
+ "POST", f"/agent-monitoring/sessions/{session_id}/coherence-check",
265
+ base=self._api_root(), timeout=180,
266
+ )
267
+ return data.get("score", data) if isinstance(data, dict) else data
268
+
269
+ def list_session_spans(self, session_id: str) -> List[dict]:
270
+ data = self._request("GET", f"/ingest/sessions/{session_id}/spans", base=self._api_root())
271
+ return data.get("spans", []) if isinstance(data, dict) else data
272
+
273
+ # ------------------------------------------------------------------
274
+ # Model portability (self-host): replay a trace's input against other models
275
+ # ------------------------------------------------------------------
276
+
277
+ def run_model_portability(self, trace_id: str, model_ids: List[str]) -> dict:
278
+ """Replay the trace's captured input against ``model_ids`` and judge each output -
279
+ the dashboard trace detail's "Compare models" action. One LLM call per candidate
280
+ plus judging, so expect tens of seconds."""
281
+ return self._request(
282
+ "POST", f"/agent-monitoring/traces/{trace_id}/portability",
283
+ base=self._api_root(), json={"modelIds": model_ids}, timeout=300,
284
+ )
285
+
286
+ # ------------------------------------------------------------------
287
+ # Judge tuning (self-host): calibrate an online evaluator against recorded
288
+ # ground truth, then rewrite/validate/publish its criteria
289
+ # ------------------------------------------------------------------
290
+
291
+ def get_online_evaluator_calibration(self, evaluator_id: str, window: str = "7d") -> dict:
292
+ return self._request(
293
+ "GET", f"/agent-monitoring/online-evaluators/{evaluator_id}/calibration",
294
+ base=self._api_root(), params={"window": window},
295
+ )
296
+
297
+ def propose_online_evaluator_tuning(self, evaluator_id: str, window: str = "7d") -> dict:
298
+ data = self._request(
299
+ "POST", f"/agent-monitoring/online-evaluators/{evaluator_id}/tune",
300
+ base=self._api_root(), json={"window": window}, timeout=300,
301
+ )
302
+ return data.get("proposal", data) if isinstance(data, dict) else data
303
+
304
+ def validate_online_evaluator_tuning(
305
+ self, evaluator_id: str, criteria: dict, window: str = "7d"
306
+ ) -> dict:
307
+ return self._request(
308
+ "POST", f"/agent-monitoring/online-evaluators/{evaluator_id}/tune/validate",
309
+ base=self._api_root(), json={**criteria, "window": window}, timeout=600,
310
+ )
311
+
312
+ def publish_online_evaluator_tuning(self, evaluator_id: str, criteria: dict) -> dict:
313
+ return self._request(
314
+ "POST", f"/agent-monitoring/online-evaluators/{evaluator_id}/tune/publish",
315
+ base=self._api_root(), json=criteria, timeout=60,
316
+ )
317
+
231
318
  def update_profile(self, agent_id: str, payload: dict) -> MonitorProfile:
232
319
  data = self._request(
233
320
  "PUT", f"/profiles/{agent_id}", json=self._with_workspace(payload)
@@ -121,6 +121,27 @@ class MonitorOnlineEvaluatorClient:
121
121
  def delete(self, evaluator_id: str) -> None:
122
122
  self._client.delete_online_evaluator(evaluator_id)
123
123
 
124
+ def calibration(self, evaluator_id: str, window: str = "7d") -> dict:
125
+ """How often this evaluator's verdicts agreed with recorded ground truth (outcomes,
126
+ user feedback, human re-scores) in the window - including the disagreement cases a
127
+ tune() proposal would rewrite from."""
128
+ return self._client.get_online_evaluator_calibration(evaluator_id, window)
129
+
130
+ def tune(self, evaluator_id: str, window: str = "7d") -> dict:
131
+ """Judge-written rewrite of this evaluator's own criteria, grounded in calibration
132
+ disagreements. Returns the proposal (criteria + reasoning + changes); publishes nothing."""
133
+ return self._client.propose_online_evaluator_tuning(evaluator_id, window)
134
+
135
+ def validate_tuning(self, evaluator_id: str, criteria: dict, window: str = "7d") -> dict:
136
+ """Exact re-judging: candidate criteria re-judge the cases the current criteria got
137
+ wrong plus a control set they got right, measured against recorded reality. `criteria`
138
+ is {acceptanceCriteria, rejectionCriteria, evaluationCriteria} from tune()."""
139
+ return self._client.validate_online_evaluator_tuning(evaluator_id, criteria, window)
140
+
141
+ def publish_tuning(self, evaluator_id: str, criteria: dict) -> dict:
142
+ """Publish tuned criteria onto the evaluator's config (the human-approval step)."""
143
+ return self._client.publish_online_evaluator_tuning(evaluator_id, criteria)
144
+
124
145
  def ratings(self, evaluator_id: str, window: str = "7d") -> List[OnlineEvaluatorRatingPoint]:
125
146
  """Bucketed average-rating-over-time for this evaluator. ``window`` is one of
126
147
  ``"24h"``, ``"7d"``, ``"30d"``."""
@@ -0,0 +1,21 @@
1
+ """Session helpers, surfaced as ``client.monitor.sessions`` (self-host)."""
2
+ from __future__ import annotations
3
+
4
+ from typing import List, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from agentx.monitor.client import MonitorClient
8
+
9
+
10
+ class MonitorSessionClient:
11
+ def __init__(self, client: "MonitorClient"):
12
+ self._client = client
13
+
14
+ def coherence_check(self, session_id: str) -> dict:
15
+ """Judge the assembled multi-turn session for consistency/drift (one judge call).
16
+ Returns the score row: rating, justification, spanCount, driftSpanId."""
17
+ return self._client.run_session_coherence_check(session_id)
18
+
19
+ def spans(self, session_id: str) -> List[dict]:
20
+ """Every span in the session (roots and children), oldest first."""
21
+ return self._client.list_session_spans(session_id)
@@ -66,6 +66,12 @@ class IngestClient:
66
66
  }
67
67
  )
68
68
 
69
+ # First delivery failure per client logs at WARNING (visible under default logging) -
70
+ # the tracer is fire-and-forget by design, so a wrong base_url or rejected key would
71
+ # otherwise fail silently forever with an empty dashboard as the only symptom. Repeat
72
+ # failures stay at DEBUG to avoid log spam. client.ping() is the fail-fast startup check.
73
+ self._delivery_warning_emitted = False
74
+
69
75
  self._queue: queue.Queue[Optional[Dict[str, Any]]] = queue.Queue(maxsize=_QUEUE_MAX)
70
76
  self._worker = threading.Thread(target=self._drain, daemon=True, name="agentx-ingest")
71
77
  self._worker.start()
@@ -104,9 +110,11 @@ class IngestClient:
104
110
  try:
105
111
  resp = self._session.post(self._endpoint, json=payload, timeout=10)
106
112
  except requests.RequestException as exc:
113
+ self._warn_delivery(f"{exc.__class__.__name__}: {exc}")
107
114
  logger.debug("agentx ingest sync send error: %s", exc)
108
115
  return None
109
116
  if not resp.ok:
117
+ self._warn_delivery(f"HTTP {resp.status_code}", status=resp.status_code)
110
118
  logger.debug("agentx ingest sync HTTP %d: %s", resp.status_code, resp.text[:200])
111
119
  return None
112
120
  try:
@@ -114,6 +122,28 @@ class IngestClient:
114
122
  except Exception:
115
123
  return None
116
124
 
125
+ def send_trace_sync_detailed(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
126
+ """``send_trace_sync`` returning the full response body instead of just the id - the
127
+ engine includes ``deduped: true`` for a span it had already ingested, which importers
128
+ (agentx-moveworks) use to skip re-evaluating on window re-syncs. Never raises."""
129
+ if self._workspace_id:
130
+ payload = {**payload, "workspaceId": self._workspace_id}
131
+ try:
132
+ resp = self._session.post(self._endpoint, json=payload, timeout=10)
133
+ except requests.RequestException as exc:
134
+ self._warn_delivery(f"{exc.__class__.__name__}: {exc}")
135
+ logger.debug("agentx ingest sync send error: %s", exc)
136
+ return None
137
+ if not resp.ok:
138
+ self._warn_delivery(f"HTTP {resp.status_code}", status=resp.status_code)
139
+ logger.debug("agentx ingest sync HTTP %d: %s", resp.status_code, resp.text[:200])
140
+ return None
141
+ try:
142
+ body = resp.json()
143
+ return body if isinstance(body, dict) and body.get("trace_id") else None
144
+ except Exception:
145
+ return None
146
+
117
147
  def evaluate_trace(
118
148
  self,
119
149
  trace_id: str,
@@ -310,6 +340,29 @@ class IngestClient:
310
340
  # Internal
311
341
  # ------------------------------------------------------------------
312
342
 
343
+ def _warn_delivery(self, detail: str, status: Optional[int] = None) -> None:
344
+ if self._delivery_warning_emitted:
345
+ return
346
+ self._delivery_warning_emitted = True
347
+ if status in (401, 403):
348
+ hint = (
349
+ "the API key was rejected - check api_key / AGENTX_API_KEY (for self-host, copy "
350
+ "the 'Default project API key' from the engine's startup log)"
351
+ )
352
+ else:
353
+ hint = (
354
+ "check base_url / AGENTX_API_BASE_URL (for self-host it should look like "
355
+ "http://localhost:4700/api/v1)"
356
+ )
357
+ logger.warning(
358
+ "AgentX traces are NOT being delivered to %s (%s) - %s. "
359
+ "Call client.ping() at startup to fail fast on misconfiguration. "
360
+ "Further delivery failures will log at DEBUG only.",
361
+ self._endpoint,
362
+ detail,
363
+ hint,
364
+ )
365
+
313
366
  def _drain(self) -> None:
314
367
  while True:
315
368
  payload = self._queue.get()
@@ -338,8 +391,10 @@ class IngestClient:
338
391
  last_exc = Exception(f"HTTP {resp.status_code}")
339
392
  continue
340
393
  if not resp.ok:
394
+ self._warn_delivery(f"HTTP {resp.status_code}", status=resp.status_code)
341
395
  logger.debug("agentx ingest HTTP %d: %s", resp.status_code, resp.text[:200])
342
396
  return
343
397
  return
344
398
 
399
+ self._warn_delivery(str(last_exc))
345
400
  logger.debug("agentx ingest failed after retries: %s", last_exc)
@@ -0,0 +1 @@
1
+ VERSION = "0.6.30"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.28
3
+ Version: 0.6.30
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
@@ -21,6 +21,7 @@ agentx/evaluations/redaction.py
21
21
  agentx/evaluations/reporting.py
22
22
  agentx/evaluations/results.py
23
23
  agentx/evaluations/runner.py
24
+ agentx/evaluations/tool_schemas.py
24
25
  agentx/evaluations/tracing.py
25
26
  agentx/evaluations/adapters/__init__.py
26
27
  agentx/evaluations/adapters/http_endpoint.py
@@ -41,11 +42,13 @@ agentx/integrations/moveworks.py
41
42
  agentx/integrations/openai.py
42
43
  agentx/integrations/openai_agents.py
43
44
  agentx/monitor/__init__.py
45
+ agentx/monitor/agents.py
44
46
  agentx/monitor/client.py
45
47
  agentx/monitor/models.py
46
48
  agentx/monitor/online_evaluators.py
47
49
  agentx/monitor/patterns.py
48
50
  agentx/monitor/profile.py
51
+ agentx/monitor/sessions.py
49
52
  agentx/monitor/signals.py
50
53
  agentx/resources/__init__.py
51
54
  agentx/resources/agent.py
@@ -1 +0,0 @@
1
- VERSION = "0.6.28"
File without changes
File without changes
File without changes
File without changes