agentx-python 0.8.24__tar.gz → 0.8.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentx_python-0.8.24/agentx_python.egg-info → agentx_python-0.8.25}/PKG-INFO +4 -2
- {agentx_python-0.8.24 → agentx_python-0.8.25}/README.md +3 -1
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/agentx.py +16 -12
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/crewai.py +2 -2
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/__init__.py +9 -0
- agentx_python-0.8.25/agentx/monitor/_transport.py +48 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/client.py +5 -3
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/improvement_groups.py +16 -4
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/judge_scorers.py +33 -6
- agentx_python-0.8.25/agentx/monitor/patterns.py +250 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/rules.py +15 -2
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/scorer_groups.py +30 -6
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/scorers.py +18 -6
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/resources/agent.py +28 -9
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/resources/conversation.py +31 -10
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/resources/workforce.py +34 -9
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/tracing/tracer.py +6 -4
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/util.py +9 -1
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/version.py +2 -2
- {agentx_python-0.8.24 → agentx_python-0.8.25/agentx_python.egg-info}/PKG-INFO +4 -2
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx_python.egg-info/SOURCES.txt +4 -1
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_judge_scorers.py +1 -1
- agentx_python-0.8.25/tests/test_pattern_update_merge.py +200 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_span_tree.py +30 -1
- agentx_python-0.8.25/tests/test_workforce_binding.py +71 -0
- agentx_python-0.8.24/agentx/monitor/patterns.py +0 -158
- {agentx_python-0.8.24 → agentx_python-0.8.25}/LICENSE +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/__init__.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/cli.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/client.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/datasets.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/evaluation_settings.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/models.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/runner.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/export.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/feedback.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/models.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/review_queue.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/projects.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/py.typed +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/testing.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/traces.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/setup.cfg +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/setup.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_docs_match_sdk.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_error_taxonomy.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_integration.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_integrations.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_multi_judge.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_runner_features.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_selfhost_compat.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_testing.py +0 -0
- {agentx_python-0.8.24 → agentx_python-0.8.25}/tests/test_wire_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-python
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.25
|
|
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
|
|
@@ -108,7 +108,9 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
|
|
|
108
108
|
pip install --upgrade agentx-python
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Requires Python 3.9 or newer.
|
|
111
|
+
Requires Python 3.9 or newer for the core SDK. Some integration extras have higher floors set
|
|
112
|
+
by their upstream packages - `[crewai]`, `[autogen]`, and `[databricks]` need Python 3.10+ (`[all]`
|
|
113
|
+
therefore does too); the core tracer and every REST surface stay 3.9-compatible.
|
|
112
114
|
|
|
113
115
|
#### Run the self-host governance suite locally
|
|
114
116
|
|
|
@@ -43,7 +43,9 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
|
|
|
43
43
|
pip install --upgrade agentx-python
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Requires Python 3.9 or newer.
|
|
46
|
+
Requires Python 3.9 or newer for the core SDK. Some integration extras have higher floors set
|
|
47
|
+
by their upstream packages - `[crewai]`, `[autogen]`, and `[databricks]` need Python 3.10+ (`[all]`
|
|
48
|
+
therefore does too); the core tracer and every REST surface stay 3.9-compatible.
|
|
47
49
|
|
|
48
50
|
#### Run the self-host governance suite locally
|
|
49
51
|
|
|
@@ -20,8 +20,8 @@ class AgentX:
|
|
|
20
20
|
# The api_key is NOT written back into os.environ (it used to be): every sub-client
|
|
21
21
|
# below receives it explicitly, and mutating process-global state from a constructor
|
|
22
22
|
# re-pointed unrelated code - the same leak the base_url write below had (deep-dive
|
|
23
|
-
# round 3, bug #1).
|
|
24
|
-
#
|
|
23
|
+
# round 3, bug #1). Flows that still read the env (bare get_headers()) now require
|
|
24
|
+
# the caller to set AGENTX_API_KEY themselves.
|
|
25
25
|
self.api_key = api_key or os.getenv("AGENTX_API_KEY")
|
|
26
26
|
|
|
27
27
|
# base_url overrides AGENTX_API_BASE_URL env var (and the SDK default). It is
|
|
@@ -147,7 +147,7 @@ class AgentX:
|
|
|
147
147
|
response = requests.get(url, headers=get_headers(self.api_key))
|
|
148
148
|
# Check if response was successful
|
|
149
149
|
if response.status_code == 200:
|
|
150
|
-
return Agent(**response.json())
|
|
150
|
+
return Agent(**response.json())._bind(self.api_key, self.base_url)
|
|
151
151
|
else:
|
|
152
152
|
raise AgentXError(
|
|
153
153
|
f"Failed to retrieve agent: {response.reason}. This endpoint is "
|
|
@@ -165,22 +165,26 @@ class AgentX:
|
|
|
165
165
|
response = requests.get(url, headers=get_headers(self.api_key))
|
|
166
166
|
# Check if response was successful
|
|
167
167
|
if response.status_code == 200:
|
|
168
|
-
return [Agent(**agent) for agent in response.json()]
|
|
168
|
+
return [Agent(**agent)._bind(self.api_key, self.base_url) for agent in response.json()]
|
|
169
169
|
else:
|
|
170
170
|
raise AgentXError(
|
|
171
171
|
f"Failed to list agents: {response.reason}. This endpoint is "
|
|
172
172
|
"hosted-platform only - on self-host use client.monitor.agents.list()."
|
|
173
173
|
)
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
response
|
|
175
|
+
def list_workforces(self) -> List["Workforce"]:
|
|
176
|
+
"""List all workforces/teams, each bound to this client's credentials - including each
|
|
177
|
+
workforce's ``manager`` and ``agents``, so their calls authenticate the same way.
|
|
178
|
+
|
|
179
|
+
This used to be documented as a static call (``AgentX.list_workforces()``); that form
|
|
180
|
+
was broken (the old staticmethod body referenced ``self`` and raised NameError on any
|
|
181
|
+
non-empty response). Construct a client instead - ``AgentX().list_workforces()`` picks
|
|
182
|
+
up AGENTX_API_KEY / AGENTX_API_BASE_URL from the environment, which is what the static
|
|
183
|
+
form effectively did."""
|
|
184
|
+
url = f"{self.base_url or api_base()}/access/teams"
|
|
185
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
182
186
|
if response.status_code == 200:
|
|
183
|
-
return [Workforce(**workforce) for workforce in response.json()]
|
|
187
|
+
return [Workforce(**workforce)._bind(self.api_key, self.base_url) for workforce in response.json()]
|
|
184
188
|
else:
|
|
185
189
|
raise Exception(
|
|
186
190
|
f"Failed to list workforces: {response.status_code} - {response.reason}"
|
|
@@ -144,14 +144,14 @@ class AgentXCrewObserver:
|
|
|
144
144
|
TaskFailedEvent,
|
|
145
145
|
TaskStartedEvent,
|
|
146
146
|
)
|
|
147
|
-
except ImportError
|
|
147
|
+
except Exception: # noqa: BLE001 - crewai import raises TypeError (PEP 604) on py3.9, not just ImportError
|
|
148
148
|
from crewai.utilities.events import crewai_event_bus
|
|
149
149
|
from crewai.utilities.events.task_events import (
|
|
150
150
|
TaskCompletedEvent,
|
|
151
151
|
TaskFailedEvent,
|
|
152
152
|
TaskStartedEvent,
|
|
153
153
|
)
|
|
154
|
-
except ImportError
|
|
154
|
+
except Exception: # noqa: BLE001 - crewai import raises TypeError (PEP 604) on py3.9, not just ImportError
|
|
155
155
|
if not _warned_no_event_bus:
|
|
156
156
|
_warned_no_event_bus = True
|
|
157
157
|
logger.warning(
|
|
@@ -10,6 +10,9 @@ from agentx.monitor.judge_scorers import (
|
|
|
10
10
|
from agentx.monitor.models import MonitorPattern, MonitorProfile, MonitorSignal, SignalOccurrence
|
|
11
11
|
from agentx.monitor.patterns import MonitorPatternBuilder, MonitorPatternClient
|
|
12
12
|
from agentx.monitor.profile import MonitorProfileClient
|
|
13
|
+
from agentx.monitor.review_queue import ReviewQueueClient, ReviewQueueItem
|
|
14
|
+
from agentx.monitor.rules import MonitorRule, MonitorRulesClient
|
|
15
|
+
from agentx.monitor.scorers import AgentXScorersError, ScorersClient
|
|
13
16
|
from agentx.monitor.scorer_groups import AgentXScorerGroupsError, ScorerGroup, ScorerGroupsClient
|
|
14
17
|
from agentx.monitor.sessions import MonitorSessionClient
|
|
15
18
|
from agentx.monitor.signals import MonitorSignalClient
|
|
@@ -19,6 +22,7 @@ __all__ = [
|
|
|
19
22
|
"AgentXJudgeScorersError",
|
|
20
23
|
"AgentXMonitorError",
|
|
21
24
|
"AgentXScorerGroupsError",
|
|
25
|
+
"AgentXScorersError",
|
|
22
26
|
"ImprovementGroupsClient",
|
|
23
27
|
"JudgeScorer",
|
|
24
28
|
"JudgeScorerBuilder",
|
|
@@ -30,10 +34,15 @@ __all__ = [
|
|
|
30
34
|
"MonitorPatternClient",
|
|
31
35
|
"MonitorProfile",
|
|
32
36
|
"MonitorProfileClient",
|
|
37
|
+
"MonitorRule",
|
|
38
|
+
"MonitorRulesClient",
|
|
33
39
|
"MonitorSessionClient",
|
|
34
40
|
"MonitorSignal",
|
|
35
41
|
"MonitorSignalClient",
|
|
42
|
+
"ReviewQueueClient",
|
|
43
|
+
"ReviewQueueItem",
|
|
36
44
|
"ScorerGroup",
|
|
45
|
+
"ScorersClient",
|
|
37
46
|
"ScorerGroupsClient",
|
|
38
47
|
"SignalOccurrence",
|
|
39
48
|
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Shared HTTP transport for the monitor sub-clients that own their ``_request`` (scorers,
|
|
2
|
+
judge_scorers, scorer_groups, improvement_groups): one retry schedule mirroring
|
|
3
|
+
``MonitorClient._request``, so ``retry=False`` means the same thing everywhere the client.py
|
|
4
|
+
comment promises it ("retry=False for ANY non-idempotent write")."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Same schedule as MonitorClient._request (agentx/monitor/client.py).
|
|
17
|
+
_RETRYABLE_STATUS = {429, 500, 502, 503, 504}
|
|
18
|
+
_RETRY_BACKOFF = [1.0, 2.0, 4.0]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def request_with_retries(
|
|
22
|
+
method: str, url: str, *, retry: bool = True, **kwargs: Any
|
|
23
|
+
) -> requests.Response:
|
|
24
|
+
"""``requests.request`` with MonitorClient's transport posture: when ``retry`` is true,
|
|
25
|
+
connection errors and retryable statuses (429/5xx) walk the backoff schedule; the last
|
|
26
|
+
response (whatever its status) is returned for the caller's own error taxonomy.
|
|
27
|
+
|
|
28
|
+
``retry=False`` is single-shot - for non-idempotent writes (creates, deletes) and
|
|
29
|
+
judge-billing POSTs, where a client-side timeout must not fire the same work twice.
|
|
30
|
+
Transport errors keep their ``requests`` exception type (callers guard on
|
|
31
|
+
``requests.Timeout`` for judge-billing endpoints)."""
|
|
32
|
+
schedule = [0.0] + _RETRY_BACKOFF if retry else [0.0]
|
|
33
|
+
last_exc: Optional[Exception] = None
|
|
34
|
+
for attempt, wait in enumerate(schedule):
|
|
35
|
+
if wait:
|
|
36
|
+
time.sleep(wait)
|
|
37
|
+
try:
|
|
38
|
+
resp = requests.request(method, url, **kwargs)
|
|
39
|
+
except requests.RequestException as e:
|
|
40
|
+
last_exc = e
|
|
41
|
+
logger.debug("Request error (attempt %d): %s", attempt + 1, e)
|
|
42
|
+
continue
|
|
43
|
+
if retry and resp.status_code in _RETRYABLE_STATUS and attempt < len(schedule) - 1:
|
|
44
|
+
logger.debug("Retryable status %d (attempt %d)", resp.status_code, attempt + 1)
|
|
45
|
+
continue
|
|
46
|
+
return resp
|
|
47
|
+
assert last_exc is not None # every non-raising path returned above
|
|
48
|
+
raise last_exc
|
|
@@ -205,9 +205,11 @@ class MonitorClient:
|
|
|
205
205
|
def _request(
|
|
206
206
|
self, method: str, path: str, timeout: int = 30, base: Optional[str] = None, retry: bool = True, **kwargs
|
|
207
207
|
) -> Any:
|
|
208
|
-
# retry=False for non-idempotent
|
|
209
|
-
#
|
|
210
|
-
#
|
|
208
|
+
# retry=False for ANY non-idempotent write - duplicating creates, deletes (a lost
|
|
209
|
+
# response + retry turns success into a spurious 404), and judge-spending POSTs
|
|
210
|
+
# (sweep, coherence, portability, tuning: a client-side timeout must not fire the
|
|
211
|
+
# same LLM-billing work twice while the first invocation still runs server-side).
|
|
212
|
+
# The judge list is the example set, not the rule. Same precedent as
|
|
211
213
|
# EvaluationsClient._request / analyze_run.
|
|
212
214
|
url = f"{base or self._base_url}{path}"
|
|
213
215
|
last_exc: Optional[Exception] = None
|
|
@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional
|
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
|
+
from agentx.monitor._transport import request_with_retries
|
|
7
8
|
from agentx.util import api_base, get_headers
|
|
8
9
|
from agentx.exceptions import AgentXError, AgentXAuthError, AgentXValidationError
|
|
9
10
|
|
|
@@ -45,7 +46,9 @@ class ImprovementGroupsClient:
|
|
|
45
46
|
self._workspace_id = workspace_id
|
|
46
47
|
self._base_url = (base_url or api_base()).rstrip("/")
|
|
47
48
|
|
|
48
|
-
def _request(
|
|
49
|
+
def _request(
|
|
50
|
+
self, method: str, path: str, json: Any = None, timeout: int = 120, retry: bool = True
|
|
51
|
+
) -> Any:
|
|
49
52
|
params = None
|
|
50
53
|
if self._workspace_id:
|
|
51
54
|
# Mirrors MonitorClient._workspace_params/_with_workspace: GETs (and DELETEs)
|
|
@@ -57,9 +60,12 @@ class ImprovementGroupsClient:
|
|
|
57
60
|
json = {**json, "workspaceId": self._workspace_id}
|
|
58
61
|
else:
|
|
59
62
|
params = {"workspaceId": self._workspace_id}
|
|
60
|
-
|
|
63
|
+
# retry=False for ANY non-idempotent write (member deletes, the report-generating
|
|
64
|
+
# POST) - MonitorClient._request's posture, via the shared monitor transport.
|
|
65
|
+
resp = request_with_retries(
|
|
61
66
|
method,
|
|
62
67
|
f"{self._base_url}/agent-monitoring{path}",
|
|
68
|
+
retry=retry,
|
|
63
69
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
64
70
|
json=json,
|
|
65
71
|
params=params,
|
|
@@ -92,7 +98,9 @@ class ImprovementGroupsClient:
|
|
|
92
98
|
|
|
93
99
|
def remove_member(self, group_id: str, member_id: str) -> None:
|
|
94
100
|
"""Prune a member before spending the group (a confirm that turned out uninteresting)."""
|
|
95
|
-
|
|
101
|
+
# retry=False: a lost response + transport retry would turn a successful delete
|
|
102
|
+
# into a spurious 404.
|
|
103
|
+
self._request("DELETE", f"/improvement-groups/{group_id}/members/{member_id}", retry=False)
|
|
96
104
|
|
|
97
105
|
def generate_report(self, group_id: str, model: Optional[str] = None) -> Dict[str, Any]:
|
|
98
106
|
"""Spend the group: one real LLM call clustering the confirmed failures into issues
|
|
@@ -101,7 +109,11 @@ class ImprovementGroupsClient:
|
|
|
101
109
|
payload: Dict[str, Any] = {}
|
|
102
110
|
if model is not None:
|
|
103
111
|
payload["model"] = model
|
|
104
|
-
|
|
112
|
+
# retry=False: spends the group (real LLM billing) - a client-side timeout must not
|
|
113
|
+
# fire the same generation twice while the first still runs server-side.
|
|
114
|
+
return self._request(
|
|
115
|
+
"POST", f"/improvement-groups/{group_id}/report", json=payload, timeout=300, retry=False
|
|
116
|
+
)["report"]
|
|
105
117
|
|
|
106
118
|
def list_reports(self) -> List[Dict[str, Any]]:
|
|
107
119
|
return self._request("GET", "/improvement-reports").get("improvementReports", [])
|
|
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
|
|
8
|
+
from agentx.monitor._transport import request_with_retries
|
|
8
9
|
from agentx.util import api_base, get_headers
|
|
9
10
|
from agentx.exceptions import AgentXError, AgentXAuthError, AgentXValidationError
|
|
10
11
|
|
|
@@ -97,7 +98,9 @@ class JudgeScorersClient:
|
|
|
97
98
|
# Captured once at construction so two clients with different bases can coexist.
|
|
98
99
|
self._base_url = (base_url or api_base()).rstrip("/")
|
|
99
100
|
|
|
100
|
-
def _request(
|
|
101
|
+
def _request(
|
|
102
|
+
self, method: str, path: str, json: Any = None, timeout: int = 60, retry: bool = True
|
|
103
|
+
) -> Any:
|
|
101
104
|
params = None
|
|
102
105
|
if self._workspace_id:
|
|
103
106
|
# Mirrors MonitorClient._workspace_params/_with_workspace: GETs (and DELETEs)
|
|
@@ -109,9 +112,13 @@ class JudgeScorersClient:
|
|
|
109
112
|
json = {**json, "workspaceId": self._workspace_id}
|
|
110
113
|
else:
|
|
111
114
|
params = {"workspaceId": self._workspace_id}
|
|
112
|
-
|
|
115
|
+
# retry=False for ANY non-idempotent write (creates, deletes) and judge-spending POST
|
|
116
|
+
# (tune/validate/publish) - MonitorClient._request's posture, via the shared monitor
|
|
117
|
+
# transport.
|
|
118
|
+
resp = request_with_retries(
|
|
113
119
|
method,
|
|
114
120
|
f"{self._base_url}/agent-monitoring{path}",
|
|
121
|
+
retry=retry,
|
|
115
122
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
116
123
|
json=json,
|
|
117
124
|
params=params,
|
|
@@ -263,7 +270,10 @@ class JudgeScorersClient:
|
|
|
263
270
|
payload["offline"] = offline
|
|
264
271
|
if online is not None:
|
|
265
272
|
payload["online"] = online
|
|
266
|
-
|
|
273
|
+
# Server-side create: a timeout + transport retry would create the scorer twice.
|
|
274
|
+
return JudgeScorer(
|
|
275
|
+
self._request("POST", "/judge-scorers", json=payload, retry=False)["judgeScorer"]
|
|
276
|
+
)
|
|
267
277
|
|
|
268
278
|
def get(self, scorer_id: str) -> JudgeScorer:
|
|
269
279
|
return JudgeScorer(self._request("GET", f"/judge-scorers/{scorer_id}")["judgeScorer"])
|
|
@@ -300,7 +310,9 @@ class JudgeScorersClient:
|
|
|
300
310
|
def delete(self, scorer_id: str) -> None:
|
|
301
311
|
"""Delete the scorer: rubric, version history, and online profile together.
|
|
302
312
|
Irreversible; refused for the built-in Session Baseline Judge."""
|
|
303
|
-
|
|
313
|
+
# retry=False: a lost response + transport retry would turn a successful delete
|
|
314
|
+
# into a spurious 404.
|
|
315
|
+
self._request("DELETE", f"/judge-scorers/{scorer_id}", retry=False)
|
|
304
316
|
|
|
305
317
|
# ------------------------------------------------------------------
|
|
306
318
|
# Online-profile pass-throughs (calibration / tuning / ratings / events)
|
|
@@ -334,8 +346,14 @@ class JudgeScorersClient:
|
|
|
334
346
|
def tune(self, scorer_id: str, window: str = "7d") -> dict:
|
|
335
347
|
"""Propose a rewrite of the rubric from calibration disagreements (LLM call, slow).
|
|
336
348
|
``window`` accepts the same values as :meth:`calibration`, including "rubric"."""
|
|
349
|
+
# retry=False (judge-spending POST, MonitorClient.propose_online_evaluator_tuning's
|
|
350
|
+
# posture): a client-side timeout must not fire the same LLM-billing work twice.
|
|
337
351
|
data = self._request(
|
|
338
|
-
"POST",
|
|
352
|
+
"POST",
|
|
353
|
+
f"/online-evaluators/{self._profile_id(scorer_id)}/tune",
|
|
354
|
+
json={"window": window},
|
|
355
|
+
timeout=300,
|
|
356
|
+
retry=False,
|
|
339
357
|
)
|
|
340
358
|
# The wire wraps the proposal ({"proposal": {...}}); unwrap like the legacy client so
|
|
341
359
|
# proposal["reasoning"] / the criteria fields are directly addressable.
|
|
@@ -344,11 +362,13 @@ class JudgeScorersClient:
|
|
|
344
362
|
def validate_tuning(self, scorer_id: str, criteria: Dict[str, Any], window: str = "7d") -> dict:
|
|
345
363
|
"""Re-judge the disagreement + control cases with candidate criteria (LLM calls, slow)."""
|
|
346
364
|
# The wire takes the criteria fields at the TOP level of the body, not nested.
|
|
365
|
+
# retry=False (judge-spending POST) - same posture as tune() above.
|
|
347
366
|
return self._request(
|
|
348
367
|
"POST",
|
|
349
368
|
f"/online-evaluators/{self._profile_id(scorer_id)}/tune/validate",
|
|
350
369
|
json={**criteria, "window": window},
|
|
351
370
|
timeout=600,
|
|
371
|
+
retry=False,
|
|
352
372
|
)
|
|
353
373
|
|
|
354
374
|
def publish_tuning(
|
|
@@ -382,7 +402,14 @@ class JudgeScorersClient:
|
|
|
382
402
|
payload["validation"] = validation_payload
|
|
383
403
|
if force:
|
|
384
404
|
payload["force"] = True
|
|
385
|
-
|
|
405
|
+
# retry=False: a non-idempotent write (each publish appends a rubric version) -
|
|
406
|
+
# MonitorClient.publish_online_evaluator_tuning's posture.
|
|
407
|
+
return self._request(
|
|
408
|
+
"POST",
|
|
409
|
+
f"/online-evaluators/{self._profile_id(scorer_id)}/tune/publish",
|
|
410
|
+
json=payload,
|
|
411
|
+
retry=False,
|
|
412
|
+
)
|
|
386
413
|
|
|
387
414
|
def ratings(self, scorer_id: str, window: str = "7d") -> "List[OnlineEvaluatorRatingPoint]":
|
|
388
415
|
"""Bucketed average-rating-over-time for this scorer's live checks - same typed points
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from agentx.monitor.models import MonitorPattern
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from agentx.monitor.client import MonitorClient
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MonitorPatternBuilder:
|
|
15
|
+
"""Fluent builder for creating a custom pattern. ``detector_kind`` selects which of
|
|
16
|
+
``include_terms``/``regex``/``semantic_prompt`` is used:
|
|
17
|
+
|
|
18
|
+
- ``"contains"`` (default): ``include_terms`` - a match if any (or all, with
|
|
19
|
+
``match_mode="all"``) phrase appears in the target text.
|
|
20
|
+
- ``"regex"``: ``regex`` - a single regular expression.
|
|
21
|
+
- ``"semantic"``: ``semantic_prompt`` - an LLM judges whether the response violates the
|
|
22
|
+
described rubric.
|
|
23
|
+
|
|
24
|
+
``conditions`` (self-host) is the engine's full N-condition model - a list of condition
|
|
25
|
+
dicts, each with its own detector kind and match settings - passed through verbatim to
|
|
26
|
+
the create payload; when set, the engine honors it as the pattern's whole rule set and
|
|
27
|
+
the flat fields above are only legacy display metadata.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
client: "MonitorClient",
|
|
33
|
+
name: str,
|
|
34
|
+
description: Optional[str] = None,
|
|
35
|
+
category: Optional[str] = None,
|
|
36
|
+
detector_kind: str = "contains",
|
|
37
|
+
match_target: Optional[List[str]] = None,
|
|
38
|
+
match_mode: str = "any",
|
|
39
|
+
include_terms: Optional[List[str]] = None,
|
|
40
|
+
exclude_terms: Optional[List[str]] = None,
|
|
41
|
+
regex: Optional[str] = None,
|
|
42
|
+
semantic_prompt: Optional[str] = None,
|
|
43
|
+
severity: str = "medium",
|
|
44
|
+
polarity: str = "failure",
|
|
45
|
+
enabled: bool = True,
|
|
46
|
+
sample_rate: float = 1.0,
|
|
47
|
+
scope_mode: str = "all",
|
|
48
|
+
agent_ids: Optional[List[str]] = None,
|
|
49
|
+
conditions: Optional[List[dict]] = None,
|
|
50
|
+
):
|
|
51
|
+
self._client = client
|
|
52
|
+
self._payload: Dict[str, Any] = {
|
|
53
|
+
"name": name,
|
|
54
|
+
"description": description,
|
|
55
|
+
"category": category,
|
|
56
|
+
"detectorKind": detector_kind,
|
|
57
|
+
"matchTarget": match_target or ["response"],
|
|
58
|
+
"matchMode": match_mode,
|
|
59
|
+
"includeTerms": include_terms or [],
|
|
60
|
+
"excludeTerms": exclude_terms or [],
|
|
61
|
+
"regex": regex,
|
|
62
|
+
"semanticPrompt": semantic_prompt,
|
|
63
|
+
"severity": severity,
|
|
64
|
+
# A "failure" pattern (default) raises a signal to triage; a "proper" pattern logs
|
|
65
|
+
# a healthy tally instead.
|
|
66
|
+
"polarity": polarity,
|
|
67
|
+
"enabled": enabled,
|
|
68
|
+
"sampleRate": sample_rate,
|
|
69
|
+
"scopeMode": scope_mode,
|
|
70
|
+
"agentIds": agent_ids or [],
|
|
71
|
+
}
|
|
72
|
+
# Passed through verbatim - the engine honors body.conditions as the full
|
|
73
|
+
# N-condition model (see the class docstring).
|
|
74
|
+
if conditions is not None:
|
|
75
|
+
self._payload["conditions"] = conditions
|
|
76
|
+
|
|
77
|
+
def publish(self) -> MonitorPattern:
|
|
78
|
+
logger.info("Publishing monitor pattern '%s'", self._payload["name"])
|
|
79
|
+
return self._client.create_pattern(self._payload)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MonitorPatternClient:
|
|
83
|
+
"""Thin wrapper surfaced as ``client.monitor.patterns``."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, client: "MonitorClient"):
|
|
86
|
+
self._client = client
|
|
87
|
+
|
|
88
|
+
def builder(
|
|
89
|
+
self,
|
|
90
|
+
name: str,
|
|
91
|
+
description: Optional[str] = None,
|
|
92
|
+
category: Optional[str] = None,
|
|
93
|
+
detector_kind: str = "contains",
|
|
94
|
+
match_target: Optional[List[str]] = None,
|
|
95
|
+
match_mode: str = "any",
|
|
96
|
+
include_terms: Optional[List[str]] = None,
|
|
97
|
+
exclude_terms: Optional[List[str]] = None,
|
|
98
|
+
regex: Optional[str] = None,
|
|
99
|
+
semantic_prompt: Optional[str] = None,
|
|
100
|
+
severity: str = "medium",
|
|
101
|
+
polarity: str = "failure",
|
|
102
|
+
enabled: bool = True,
|
|
103
|
+
sample_rate: float = 1.0,
|
|
104
|
+
scope_mode: str = "all",
|
|
105
|
+
agent_ids: Optional[List[str]] = None,
|
|
106
|
+
conditions: Optional[List[dict]] = None,
|
|
107
|
+
) -> MonitorPatternBuilder:
|
|
108
|
+
return MonitorPatternBuilder(
|
|
109
|
+
self._client,
|
|
110
|
+
name=name,
|
|
111
|
+
description=description,
|
|
112
|
+
category=category,
|
|
113
|
+
detector_kind=detector_kind,
|
|
114
|
+
match_target=match_target,
|
|
115
|
+
match_mode=match_mode,
|
|
116
|
+
include_terms=include_terms,
|
|
117
|
+
exclude_terms=exclude_terms,
|
|
118
|
+
regex=regex,
|
|
119
|
+
semantic_prompt=semantic_prompt,
|
|
120
|
+
severity=severity,
|
|
121
|
+
polarity=polarity,
|
|
122
|
+
enabled=enabled,
|
|
123
|
+
sample_rate=sample_rate,
|
|
124
|
+
scope_mode=scope_mode,
|
|
125
|
+
agent_ids=agent_ids,
|
|
126
|
+
conditions=conditions,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def delete(self, pattern_id: str) -> None:
|
|
130
|
+
"""Delete a pattern. Its historical signals remain as history."""
|
|
131
|
+
# retry=False: a lost response + transport retry would turn a successful
|
|
132
|
+
# delete into a spurious 404.
|
|
133
|
+
self._client._request(
|
|
134
|
+
"DELETE",
|
|
135
|
+
f"/agent-monitoring/patterns/{pattern_id}",
|
|
136
|
+
base=self._client._api_root(),
|
|
137
|
+
retry=False,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# snake_case -> wire camelCase, same posture as rules.update: the engine reads only the
|
|
141
|
+
# camelCase key and silently keeps the stored value for anything it does not recognize -
|
|
142
|
+
# update(sample_rate=0.05) used to 200 with the rate unchanged.
|
|
143
|
+
_UPDATE_ALIASES = {
|
|
144
|
+
"sample_rate": "sampleRate",
|
|
145
|
+
"scope_mode": "scopeMode",
|
|
146
|
+
"agent_ids": "agentIds",
|
|
147
|
+
"detector_kind": "detectorKind",
|
|
148
|
+
"include_terms": "includeTerms",
|
|
149
|
+
"exclude_terms": "excludeTerms",
|
|
150
|
+
"regex": "regex",
|
|
151
|
+
"semantic_prompt": "semanticPrompt",
|
|
152
|
+
"match_mode": "matchMode",
|
|
153
|
+
"match_target": "matchTarget",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# The engine's PUT rebuilds the pattern's WHOLE conditions array (legacyPayloadToConditions:
|
|
157
|
+
# "a full replace, not a sparse patch") exactly when the body carries one of these - its own
|
|
158
|
+
# sentConditionFields set, minus "conditions". Bodies without any of them keep the stored
|
|
159
|
+
# conditions untouched.
|
|
160
|
+
_TRIGGER_FIELDS = ("includeTerms", "regex", "semanticPrompt")
|
|
161
|
+
|
|
162
|
+
# Sent alone, these look sparse but cannot land: without a trigger field the engine never
|
|
163
|
+
# rebuilds conditions (the values are silently ignored). For excludeTerms/matchMode there is
|
|
164
|
+
# also nothing to merge them over client-side - the wire GET reports display-only
|
|
165
|
+
# placeholders (includeTerms/excludeTerms always [], matchMode always "any"; conditions is
|
|
166
|
+
# the only truth). matchTarget IS reported faithfully, but the engine only reads it during a
|
|
167
|
+
# rebuild, so alone it is the same silent no-op.
|
|
168
|
+
_UNMERGEABLE_ALONE = ("excludeTerms", "matchMode", "matchTarget")
|
|
169
|
+
|
|
170
|
+
def update(self, pattern_id: str, **fields: Any) -> MonitorPattern:
|
|
171
|
+
"""Update a pattern and return the updated :class:`MonitorPattern`. Accepts snake_case
|
|
172
|
+
kwargs (``sample_rate=0.05``) or the wire's camelCase; an unrecognized snake_case key
|
|
173
|
+
raises instead of silently changing nothing.
|
|
174
|
+
|
|
175
|
+
The real contract on self-host: the stored truth is the pattern's ``conditions`` array,
|
|
176
|
+
and the flat fields the wire GET returns are display-only placeholders (``includeTerms``/
|
|
177
|
+
``excludeTerms`` always ``[]``, ``matchMode`` always ``"any"``, ``regex``/
|
|
178
|
+
``semanticPrompt`` omitted). The engine's PUT rebuilds the WHOLE conditions array
|
|
179
|
+
whenever the body carries ``include_terms``, ``regex``, or ``semantic_prompt`` (or an
|
|
180
|
+
explicit ``conditions`` list, which wins outright). Consequences:
|
|
181
|
+
|
|
182
|
+
- ``update(pid, regex=...)`` (or include_terms/semantic_prompt) is a full detector
|
|
183
|
+
rewrite; the detector kind follows the trigger field actually sent (``regex`` ->
|
|
184
|
+
``"regex"``, ``semantic_prompt`` -> ``"semantic"``, ``include_terms`` ->
|
|
185
|
+
``"contains"``), so cross-kind updates work, and this client back-fills only
|
|
186
|
+
``matchTarget`` from the stored pattern so the rebuild keeps its target. It never
|
|
187
|
+
back-fills ``includeTerms``/``excludeTerms``/``matchMode`` - the GET values are
|
|
188
|
+
placeholders, and copying them in would destroy real conditions.
|
|
189
|
+
- ``exclude_terms=``, ``match_mode=``, or ``match_target=`` alone raises ValueError:
|
|
190
|
+
the engine silently ignores them without a rebuild. Pass ``conditions=[...]`` (built
|
|
191
|
+
from ``get(pattern_id).conditions``) instead, or pass them alongside the
|
|
192
|
+
include_terms/regex/semantic_prompt they should be rebuilt with.
|
|
193
|
+
- Everything else stays a sparse metadata edit that leaves the stored conditions
|
|
194
|
+
untouched."""
|
|
195
|
+
payload: Dict[str, Any] = {}
|
|
196
|
+
for key, value in fields.items():
|
|
197
|
+
wire_key = self._UPDATE_ALIASES.get(key, key)
|
|
198
|
+
if "_" in wire_key:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Unknown pattern field {key!r} - the engine reads camelCase keys and would "
|
|
201
|
+
"silently ignore this (see MonitorPattern for the field names)."
|
|
202
|
+
)
|
|
203
|
+
payload[wire_key] = value
|
|
204
|
+
sends_conditions = "conditions" in payload
|
|
205
|
+
triggered = any(k in payload for k in self._TRIGGER_FIELDS)
|
|
206
|
+
if not sends_conditions and not triggered:
|
|
207
|
+
offending = [k for k in self._UNMERGEABLE_ALONE if k in payload]
|
|
208
|
+
if offending:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"{' and '.join(offending)} cannot be updated on their own: the engine only "
|
|
211
|
+
"rebuilds a pattern's conditions when includeTerms/regex/semanticPrompt is "
|
|
212
|
+
"sent (alone they are silently ignored), and the wire GET returns display-"
|
|
213
|
+
"only placeholders (includeTerms/excludeTerms always [], matchMode always "
|
|
214
|
+
"'any'), so there is no stored value to merge them over. Pass "
|
|
215
|
+
"conditions=[...] built from get(pattern_id).conditions instead, or send "
|
|
216
|
+
"them alongside the trigger field they should be rebuilt with."
|
|
217
|
+
)
|
|
218
|
+
if triggered and not sends_conditions:
|
|
219
|
+
stored = self.get(pattern_id)
|
|
220
|
+
# The rebuild's detector kind follows the trigger field actually sent - back-filling
|
|
221
|
+
# the STORED kind 400s a kind change (regex= on a "contains" pattern) and silently
|
|
222
|
+
# corrupts the inverse (include_terms= on a regex pattern would write phrase
|
|
223
|
+
# conditions while keeping detectorKind "regex").
|
|
224
|
+
if "regex" in payload:
|
|
225
|
+
inferred_kind = "regex"
|
|
226
|
+
elif "semanticPrompt" in payload:
|
|
227
|
+
inferred_kind = "semantic"
|
|
228
|
+
else:
|
|
229
|
+
inferred_kind = "contains"
|
|
230
|
+
payload.setdefault("detectorKind", inferred_kind)
|
|
231
|
+
# matchTarget is the one field the wire reports faithfully - it rides along so the
|
|
232
|
+
# server-side full-replace rebuild keeps the stored target. NEVER includeTerms/
|
|
233
|
+
# excludeTerms/matchMode - see _UNMERGEABLE_ALONE's comment.
|
|
234
|
+
payload.setdefault("matchTarget", stored.match_target)
|
|
235
|
+
data = self._client._request(
|
|
236
|
+
"PUT",
|
|
237
|
+
f"/agent-monitoring/patterns/{pattern_id}",
|
|
238
|
+
base=self._client._api_root(),
|
|
239
|
+
json=payload,
|
|
240
|
+
# An idempotent merge server-side (same payload, same result) - but the read-
|
|
241
|
+
# merge-write above is not atomic, so keep the single-shot posture.
|
|
242
|
+
retry=False,
|
|
243
|
+
)
|
|
244
|
+
return MonitorPattern(**data["pattern"])
|
|
245
|
+
|
|
246
|
+
def get(self, pattern_id: str) -> MonitorPattern:
|
|
247
|
+
return self._client.get_pattern(pattern_id)
|
|
248
|
+
|
|
249
|
+
def list(self) -> List[MonitorPattern]:
|
|
250
|
+
return self._client.list_patterns()
|
|
@@ -70,9 +70,22 @@ class MonitorRulesClient:
|
|
|
70
70
|
|
|
71
71
|
def update(self, rule_id: str, **fields: Any) -> MonitorRule:
|
|
72
72
|
"""Sparse update. snake_case keys are mapped to the wire (``sample_rate`` ->
|
|
73
|
-
``sampleRate``, ``action_config`` -> ``actionConfig``)
|
|
73
|
+
``sampleRate``, ``action_config`` -> ``actionConfig``); an unrecognized snake_case
|
|
74
|
+
key raises instead of 200ing with the rule unchanged (the engine reads only camelCase
|
|
75
|
+
and silently keeps the stored value for keys it does not know)."""
|
|
74
76
|
aliases = {"sample_rate": "sampleRate", "action_config": "actionConfig"}
|
|
75
|
-
payload
|
|
77
|
+
payload: Dict[str, Any] = {}
|
|
78
|
+
for key, value in fields.items():
|
|
79
|
+
wire_key = aliases.get(key, key)
|
|
80
|
+
if "_" in wire_key:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Unknown rule field {key!r} - the engine reads camelCase keys and would "
|
|
83
|
+
"silently ignore this (see MonitorRule for the field names)."
|
|
84
|
+
)
|
|
85
|
+
payload[wire_key] = value
|
|
86
|
+
# PUT /rules/:id is an idempotent full-body merge (same payload, same result), so the
|
|
87
|
+
# transport's default retry is safe - and skipping it just drops legitimate edits on a
|
|
88
|
+
# transient failure.
|
|
76
89
|
data = self._request("PUT", f"/agent-monitoring/rules/{rule_id}", json=payload)
|
|
77
90
|
return MonitorRule(data.get("rule", data))
|
|
78
91
|
|