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.
- {agentx_python-0.6.28 → agentx_python-0.6.30}/PKG-INFO +1 -1
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/agentx.py +44 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/client.py +106 -1
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/evaluation_settings.py +8 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/prompts.py +19 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/runner.py +22 -0
- agentx_python-0.6.30/agentx/evaluations/tool_schemas.py +48 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/exceptions.py +4 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/moveworks.py +56 -8
- agentx_python-0.6.30/agentx/monitor/agents.py +28 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/client.py +89 -2
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/online_evaluators.py +21 -0
- agentx_python-0.6.30/agentx/monitor/sessions.py +21 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/ingest_client.py +55 -0
- agentx_python-0.6.30/agentx/version.py +1 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/PKG-INFO +1 -1
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/SOURCES.txt +3 -0
- agentx_python-0.6.28/agentx/version.py +0 -1
- {agentx_python-0.6.28 → agentx_python-0.6.30}/LICENSE +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/README.md +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/cli.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/datasets.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/models.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/redaction.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/feedback.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/models.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/outcomes.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/py.typed +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/agent.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/tracing/tracer.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx/util.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/setup.cfg +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/setup.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_integration.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_integrations.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.6.28 → agentx_python-0.6.30}/tests/test_span_tree.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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.
|
|
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
|
-
|
|
411
|
-
|
|
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="
|
|
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
|
|
101
|
-
|
|
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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|