agentx-python 0.6.28__tar.gz → 0.6.29__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.29}/PKG-INFO +1 -1
  2. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/agentx.py +44 -0
  3. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/client.py +106 -1
  4. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/evaluation_settings.py +8 -0
  5. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/prompts.py +19 -0
  6. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/runner.py +22 -0
  7. agentx_python-0.6.29/agentx/evaluations/tool_schemas.py +48 -0
  8. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/exceptions.py +4 -0
  9. agentx_python-0.6.29/agentx/monitor/agents.py +28 -0
  10. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/client.py +89 -2
  11. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/online_evaluators.py +21 -0
  12. agentx_python-0.6.29/agentx/monitor/sessions.py +21 -0
  13. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/tracing/ingest_client.py +33 -0
  14. agentx_python-0.6.29/agentx/version.py +1 -0
  15. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/PKG-INFO +1 -1
  16. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/SOURCES.txt +3 -0
  17. agentx_python-0.6.28/agentx/version.py +0 -1
  18. {agentx_python-0.6.28 → agentx_python-0.6.29}/LICENSE +0 -0
  19. {agentx_python-0.6.28 → agentx_python-0.6.29}/README.md +0 -0
  20. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/__init__.py +0 -0
  21. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/cli.py +0 -0
  22. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/__init__.py +0 -0
  23. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/_term.py +0 -0
  24. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/adapters/__init__.py +0 -0
  25. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  26. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/adapters/precomputed.py +0 -0
  27. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/adapters/raw.py +0 -0
  28. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/datasets.py +0 -0
  29. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/models.py +0 -0
  30. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/redaction.py +0 -0
  31. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/reporting.py +0 -0
  32. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/results.py +0 -0
  33. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/evaluations/tracing.py +0 -0
  34. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/feedback.py +0 -0
  35. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/__init__.py +0 -0
  36. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/_traced_call.py +0 -0
  37. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/anthropic.py +0 -0
  38. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/autogen.py +0 -0
  39. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/crewai.py +0 -0
  40. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/databricks.py +0 -0
  41. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/google_adk.py +0 -0
  42. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/google_genai.py +0 -0
  43. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/langchain.py +0 -0
  44. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/litellm.py +0 -0
  45. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/llamaindex.py +0 -0
  46. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/moveworks.py +0 -0
  47. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/openai.py +0 -0
  48. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/integrations/openai_agents.py +0 -0
  49. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/__init__.py +0 -0
  50. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/models.py +0 -0
  51. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/patterns.py +0 -0
  52. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/profile.py +0 -0
  53. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/monitor/signals.py +0 -0
  54. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/outcomes.py +0 -0
  55. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/py.typed +0 -0
  56. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/resources/__init__.py +0 -0
  57. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/resources/agent.py +0 -0
  58. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/resources/conversation.py +0 -0
  59. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/resources/workforce.py +0 -0
  60. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/tracing/__init__.py +0 -0
  61. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/tracing/ci_types.py +0 -0
  62. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/tracing/tracer.py +0 -0
  63. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx/util.py +0 -0
  64. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/dependency_links.txt +0 -0
  65. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/entry_points.txt +0 -0
  66. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/not-zip-safe +0 -0
  67. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/requires.txt +0 -0
  68. {agentx_python-0.6.28 → agentx_python-0.6.29}/agentx_python.egg-info/top_level.txt +0 -0
  69. {agentx_python-0.6.28 → agentx_python-0.6.29}/setup.cfg +0 -0
  70. {agentx_python-0.6.28 → agentx_python-0.6.29}/setup.py +0 -0
  71. {agentx_python-0.6.28 → agentx_python-0.6.29}/tests/test_integration.py +0 -0
  72. {agentx_python-0.6.28 → agentx_python-0.6.29}/tests/test_integrations.py +0 -0
  73. {agentx_python-0.6.28 → agentx_python-0.6.29}/tests/test_selfhost_analysis_fallback.py +0 -0
  74. {agentx_python-0.6.28 → agentx_python-0.6.29}/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.29
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
 
@@ -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:
@@ -310,6 +318,29 @@ class IngestClient:
310
318
  # Internal
311
319
  # ------------------------------------------------------------------
312
320
 
321
+ def _warn_delivery(self, detail: str, status: Optional[int] = None) -> None:
322
+ if self._delivery_warning_emitted:
323
+ return
324
+ self._delivery_warning_emitted = True
325
+ if status in (401, 403):
326
+ hint = (
327
+ "the API key was rejected - check api_key / AGENTX_API_KEY (for self-host, copy "
328
+ "the 'Default project API key' from the engine's startup log)"
329
+ )
330
+ else:
331
+ hint = (
332
+ "check base_url / AGENTX_API_BASE_URL (for self-host it should look like "
333
+ "http://localhost:4700/api/v1)"
334
+ )
335
+ logger.warning(
336
+ "AgentX traces are NOT being delivered to %s (%s) - %s. "
337
+ "Call client.ping() at startup to fail fast on misconfiguration. "
338
+ "Further delivery failures will log at DEBUG only.",
339
+ self._endpoint,
340
+ detail,
341
+ hint,
342
+ )
343
+
313
344
  def _drain(self) -> None:
314
345
  while True:
315
346
  payload = self._queue.get()
@@ -338,8 +369,10 @@ class IngestClient:
338
369
  last_exc = Exception(f"HTTP {resp.status_code}")
339
370
  continue
340
371
  if not resp.ok:
372
+ self._warn_delivery(f"HTTP {resp.status_code}", status=resp.status_code)
341
373
  logger.debug("agentx ingest HTTP %d: %s", resp.status_code, resp.text[:200])
342
374
  return
343
375
  return
344
376
 
377
+ self._warn_delivery(str(last_exc))
345
378
  logger.debug("agentx ingest failed after retries: %s", last_exc)
@@ -0,0 +1 @@
1
+ VERSION = "0.6.29"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.28
3
+ Version: 0.6.29
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