agentops-accelerator 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentops/__init__.py +10 -0
- agentops/__main__.py +6 -0
- agentops/agent/__init__.py +12 -0
- agentops/agent/_legacy_ids.py +92 -0
- agentops/agent/analyzer.py +207 -0
- agentops/agent/checks/__init__.py +1 -0
- agentops/agent/checks/catalog.py +880 -0
- agentops/agent/checks/errors.py +279 -0
- agentops/agent/checks/foundry_config.py +75 -0
- agentops/agent/checks/latency.py +84 -0
- agentops/agent/checks/opex.py +157 -0
- agentops/agent/checks/opex_workspace.py +874 -0
- agentops/agent/checks/posture.py +36 -0
- agentops/agent/checks/posture_rules/__init__.py +53 -0
- agentops/agent/checks/posture_rules/content_filter.py +59 -0
- agentops/agent/checks/posture_rules/diagnostics.py +74 -0
- agentops/agent/checks/posture_rules/local_auth.py +55 -0
- agentops/agent/checks/posture_rules/managed_identity.py +59 -0
- agentops/agent/checks/posture_rules/network.py +68 -0
- agentops/agent/checks/regression.py +78 -0
- agentops/agent/checks/release_readiness.py +182 -0
- agentops/agent/checks/safety.py +247 -0
- agentops/agent/checks/spec_conformance.py +375 -0
- agentops/agent/cockpit.py +5159 -0
- agentops/agent/config.py +240 -0
- agentops/agent/findings.py +113 -0
- agentops/agent/history.py +142 -0
- agentops/agent/knowledge/__init__.py +182 -0
- agentops/agent/knowledge/waf-checklist.csv +39 -0
- agentops/agent/llm_assist/__init__.py +16 -0
- agentops/agent/llm_assist/_base.py +124 -0
- agentops/agent/llm_assist/_bundle_rule.py +154 -0
- agentops/agent/llm_assist/_client.py +347 -0
- agentops/agent/llm_assist/_dataset_rules.py +191 -0
- agentops/agent/llm_assist/_engine.py +106 -0
- agentops/agent/llm_assist/_prompt_rules.py +291 -0
- agentops/agent/llm_assist/_spec_rules.py +235 -0
- agentops/agent/production_telemetry.py +430 -0
- agentops/agent/report.py +207 -0
- agentops/agent/server/__init__.py +1 -0
- agentops/agent/server/app.py +84 -0
- agentops/agent/server/auth.py +94 -0
- agentops/agent/server/chat.py +44 -0
- agentops/agent/server/protocol.py +72 -0
- agentops/agent/sources/__init__.py +1 -0
- agentops/agent/sources/azure_monitor.py +523 -0
- agentops/agent/sources/azure_resources.py +602 -0
- agentops/agent/sources/foundry_control.py +174 -0
- agentops/agent/sources/results_history.py +494 -0
- agentops/agent/sources/spec_detectors/__init__.py +42 -0
- agentops/agent/sources/spec_detectors/_base.py +58 -0
- agentops/agent/sources/spec_detectors/agents_md.py +75 -0
- agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
- agentops/agent/time_range.py +117 -0
- agentops/cli/__init__.py +1 -0
- agentops/cli/app.py +4823 -0
- agentops/core/__init__.py +1 -0
- agentops/core/agentops_config.py +592 -0
- agentops/core/config_loader.py +22 -0
- agentops/core/evaluators.py +480 -0
- agentops/core/release_evidence.py +56 -0
- agentops/core/results.py +117 -0
- agentops/mcp/__init__.py +10 -0
- agentops/mcp/server.py +232 -0
- agentops/pipeline/__init__.py +8 -0
- agentops/pipeline/cloud_results.py +189 -0
- agentops/pipeline/cloud_runner.py +901 -0
- agentops/pipeline/comparison.py +108 -0
- agentops/pipeline/diagnostics.py +51 -0
- agentops/pipeline/invocations.py +535 -0
- agentops/pipeline/official_eval.py +414 -0
- agentops/pipeline/orchestrator.py +775 -0
- agentops/pipeline/prompt_deploy.py +377 -0
- agentops/pipeline/publisher.py +121 -0
- agentops/pipeline/reporter.py +202 -0
- agentops/pipeline/runtime.py +409 -0
- agentops/pipeline/thresholds.py +84 -0
- agentops/services/__init__.py +1 -0
- agentops/services/cicd.py +720 -0
- agentops/services/eval_analysis.py +848 -0
- agentops/services/evidence_pack.py +757 -0
- agentops/services/initializer.py +86 -0
- agentops/services/preflight.py +470 -0
- agentops/services/setup_wizard.py +709 -0
- agentops/services/skills.py +643 -0
- agentops/services/trace_promotion.py +300 -0
- agentops/services/workflow_analysis.py +1129 -0
- agentops/templates/.gitignore +15 -0
- agentops/templates/__init__.py +1 -0
- agentops/templates/agent-server/Dockerfile +23 -0
- agentops/templates/agent-server/README.md +61 -0
- agentops/templates/agent-server/main.bicep +94 -0
- agentops/templates/agent.yaml +87 -0
- agentops/templates/agentops.yaml +58 -0
- agentops/templates/foundry.svg +71 -0
- agentops/templates/icon.png +0 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
- agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
- agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
- agentops/templates/project.gitignore +36 -0
- agentops/templates/sample-traces.jsonl +3 -0
- agentops/templates/skills/agentops-agent/SKILL.md +137 -0
- agentops/templates/skills/agentops-config/SKILL.md +113 -0
- agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
- agentops/templates/skills/agentops-eval/SKILL.md +189 -0
- agentops/templates/skills/agentops-report/SKILL.md +71 -0
- agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
- agentops/templates/smoke.jsonl +3 -0
- agentops/templates/waf-checklist.README.md +84 -0
- agentops/templates/waf-checklist.csv +22 -0
- agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
- agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
- agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
- agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
- agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
- agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
- agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
- agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/workflows/agentops-pr.yml +148 -0
- agentops/templates/workflows/agentops-watchdog.yml +122 -0
- agentops/utils/__init__.py +1 -0
- agentops/utils/azd_env.py +435 -0
- agentops/utils/azure_endpoints.py +62 -0
- agentops/utils/colors.py +47 -0
- agentops/utils/dotenv_loader.py +105 -0
- agentops/utils/foundry_discovery.py +229 -0
- agentops/utils/logging.py +59 -0
- agentops/utils/telemetry.py +554 -0
- agentops/utils/yaml.py +36 -0
- agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
- agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
- agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
- agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
- agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
- agentops_accelerator-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core domain logic package."""
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""Flat ``agentops.yaml`` schema for AgentOps 1.0.
|
|
2
|
+
|
|
3
|
+
This module defines the user-facing configuration shape that replaces the
|
|
4
|
+
layered ``run.yaml`` + ``bundle.yaml`` + ``dataset.yaml`` files of pre-1.0
|
|
5
|
+
AgentOps.
|
|
6
|
+
|
|
7
|
+
Design goals:
|
|
8
|
+
|
|
9
|
+
* One file. ``agentops.yaml`` is the single source of truth.
|
|
10
|
+
* No ``scenario`` field. The toolkit derives the target type from the
|
|
11
|
+
``agent`` value and the evaluator set from the dataset row shape (see
|
|
12
|
+
:mod:`agentops.core.evaluators`).
|
|
13
|
+
* No bundle / dataset YAML configs. Datasets are plain JSONL files referenced
|
|
14
|
+
directly by path.
|
|
15
|
+
|
|
16
|
+
The minimal valid config is three lines::
|
|
17
|
+
|
|
18
|
+
version: 1
|
|
19
|
+
agent: my-rag-agent:3
|
|
20
|
+
dataset: ./qa.jsonl
|
|
21
|
+
|
|
22
|
+
The :func:`classify_agent` helper resolves ``agent`` into one of four target
|
|
23
|
+
kinds - ``foundry_prompt``, ``foundry_hosted``, ``http_json``, or
|
|
24
|
+
``model_direct`` - based on the value shape and optional ``protocol`` field.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
32
|
+
|
|
33
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Public type aliases
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
#: Wire protocol for hosted / HTTP targets.
|
|
40
|
+
Protocol = Literal["responses", "invocations", "http-json"]
|
|
41
|
+
|
|
42
|
+
#: How thresholds compare against measured metric values.
|
|
43
|
+
Criteria = Literal[">=", ">", "<=", "<", "==", "true", "false"]
|
|
44
|
+
|
|
45
|
+
#: Resolved target kind. Derived from the ``agent`` value, never set by the user.
|
|
46
|
+
TargetKind = Literal[
|
|
47
|
+
"foundry_prompt", # name:version
|
|
48
|
+
"foundry_hosted", # https://...foundry... endpoint
|
|
49
|
+
"http_json", # any other https URL
|
|
50
|
+
"model_direct", # model:<deployment>
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
#: Where to execute the agent and evaluators.
|
|
54
|
+
#:
|
|
55
|
+
#: - ``local`` (default): AgentOps invokes the agent row-by-row and runs
|
|
56
|
+
#: evaluators locally. Results are the canonical record.
|
|
57
|
+
#: - ``cloud``: Foundry runs the agent and evaluators server-side via the
|
|
58
|
+
#: OpenAI Evals API. Use this when you want the run to appear in the
|
|
59
|
+
#: New Foundry Evaluations panel as the primary record.
|
|
60
|
+
ExecutionMode = Literal["local", "cloud"]
|
|
61
|
+
|
|
62
|
+
#: How cloud evaluation submits local dataset rows to Foundry.
|
|
63
|
+
DatasetSyncMode = Literal["auto", "inline", "foundry"]
|
|
64
|
+
|
|
65
|
+
#: Internal-only literal kept for the publisher dispatch table. Derived from
|
|
66
|
+
#: ``execution`` + ``publish`` via :meth:`AgentOpsConfig.publish_target`.
|
|
67
|
+
PublishTarget = Literal["foundry", "foundry_cloud"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Threshold model
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Threshold(BaseModel):
|
|
76
|
+
"""A pass/fail rule for a single metric.
|
|
77
|
+
|
|
78
|
+
Users typically write thresholds as a dict keyed by metric name in
|
|
79
|
+
``agentops.yaml``::
|
|
80
|
+
|
|
81
|
+
thresholds:
|
|
82
|
+
groundedness: ">=3"
|
|
83
|
+
coherence: ">=3"
|
|
84
|
+
avg_latency_seconds: "<=10"
|
|
85
|
+
|
|
86
|
+
Each value is parsed by :meth:`from_expression` into a ``Threshold``.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
metric: str
|
|
90
|
+
criteria: Criteria
|
|
91
|
+
value: Optional[float] = None
|
|
92
|
+
|
|
93
|
+
model_config = ConfigDict(frozen=True)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_expression(cls, metric: str, expression: Any) -> "Threshold":
|
|
97
|
+
"""Parse a shorthand string like ``">=3"`` or a bool like ``true``."""
|
|
98
|
+
if isinstance(expression, bool):
|
|
99
|
+
return cls(metric=metric, criteria="true" if expression else "false")
|
|
100
|
+
if isinstance(expression, (int, float)):
|
|
101
|
+
return cls(metric=metric, criteria=">=", value=float(expression))
|
|
102
|
+
if not isinstance(expression, str):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"threshold for {metric!r} must be a string, number, or bool"
|
|
105
|
+
)
|
|
106
|
+
text = expression.strip()
|
|
107
|
+
if text.lower() in {"true", "false"}:
|
|
108
|
+
return cls(metric=metric, criteria=text.lower()) # type: ignore[arg-type]
|
|
109
|
+
for op in (">=", "<=", "==", ">", "<"):
|
|
110
|
+
if text.startswith(op):
|
|
111
|
+
rest = text[len(op):].strip()
|
|
112
|
+
try:
|
|
113
|
+
return cls(metric=metric, criteria=op, value=float(rest)) # type: ignore[arg-type]
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"threshold for {metric!r}: cannot parse number from {text!r}"
|
|
117
|
+
) from exc
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"threshold for {metric!r}: expected '>=N', '<=N', '>N', '<N', '==N', "
|
|
120
|
+
f"'true', or 'false'; got {text!r}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Optional evaluator override (escape hatch)
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class EvaluatorOverride(BaseModel):
|
|
130
|
+
"""Advanced override entry: force a specific evaluator into the run.
|
|
131
|
+
|
|
132
|
+
The default user flow does **not** use this. Evaluators are auto-selected
|
|
133
|
+
from the target type and dataset shape. Power users who need to bypass the
|
|
134
|
+
inference rules can list evaluator names here::
|
|
135
|
+
|
|
136
|
+
evaluators:
|
|
137
|
+
- GroundednessEvaluator
|
|
138
|
+
- CoherenceEvaluator
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
name: str
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(frozen=True)
|
|
144
|
+
|
|
145
|
+
@field_validator("name")
|
|
146
|
+
@classmethod
|
|
147
|
+
def _name_non_empty(cls, value: str) -> str:
|
|
148
|
+
if not value.strip():
|
|
149
|
+
raise ValueError("evaluator name must be non-empty")
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Dataset sync configuration
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class DatasetSyncConfig(BaseModel):
|
|
159
|
+
"""Cloud-evaluation dataset submission policy.
|
|
160
|
+
|
|
161
|
+
AgentOps keeps the local JSONL file as the source of truth. This optional
|
|
162
|
+
block tells the cloud runner whether to submit the rows inline or require a
|
|
163
|
+
Foundry dataset reference once that path is validated for the Evals API.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
mode: DatasetSyncMode = Field(
|
|
167
|
+
"auto",
|
|
168
|
+
description=(
|
|
169
|
+
"Dataset submission mode for execution: cloud. 'auto' uses the "
|
|
170
|
+
"safest supported mode, 'inline' forces file_content compatibility, "
|
|
171
|
+
"and 'foundry' requires a versioned Foundry dataset reference."
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
name: Optional[str] = Field(
|
|
175
|
+
None,
|
|
176
|
+
description="Optional stable Foundry dataset name for synced cloud runs.",
|
|
177
|
+
)
|
|
178
|
+
version: str = Field(
|
|
179
|
+
"content-hash",
|
|
180
|
+
description=(
|
|
181
|
+
"Foundry dataset version. Use 'content-hash' to derive it from the "
|
|
182
|
+
"local JSONL contents, or provide an explicit version string."
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
model_config = ConfigDict(extra="forbid")
|
|
187
|
+
|
|
188
|
+
@field_validator("name")
|
|
189
|
+
@classmethod
|
|
190
|
+
def _name_non_empty(cls, value: Optional[str]) -> Optional[str]:
|
|
191
|
+
if value is None:
|
|
192
|
+
return value
|
|
193
|
+
value = value.strip()
|
|
194
|
+
if not value:
|
|
195
|
+
raise ValueError("dataset_sync.name must be non-empty when provided")
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
@field_validator("version")
|
|
199
|
+
@classmethod
|
|
200
|
+
def _version_non_empty(cls, value: str) -> str:
|
|
201
|
+
value = value.strip()
|
|
202
|
+
if not value:
|
|
203
|
+
raise ValueError("dataset_sync.version must be non-empty")
|
|
204
|
+
return value
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Top-level config
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_LEGACY_TOP_LEVEL_KEYS = {
|
|
213
|
+
"target",
|
|
214
|
+
"bundle",
|
|
215
|
+
"output",
|
|
216
|
+
"scenario",
|
|
217
|
+
"backend",
|
|
218
|
+
"run",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class AgentOpsConfig(BaseModel):
|
|
223
|
+
"""Top-level ``agentops.yaml`` model.
|
|
224
|
+
|
|
225
|
+
Fields:
|
|
226
|
+
|
|
227
|
+
``version``
|
|
228
|
+
Schema version. Must be ``1`` in this release.
|
|
229
|
+
|
|
230
|
+
``agent``
|
|
231
|
+
The thing under evaluation. One of:
|
|
232
|
+
|
|
233
|
+
* ``"<name>:<version>"`` - a Foundry prompt agent (e.g. ``"my-rag:3"``).
|
|
234
|
+
* ``"https://..."`` - a Foundry hosted endpoint or any HTTP/JSON agent.
|
|
235
|
+
* ``"model:<deployment>"`` - a Foundry model deployment (raw model).
|
|
236
|
+
|
|
237
|
+
See :func:`classify_agent` for the full resolution table.
|
|
238
|
+
|
|
239
|
+
``dataset``
|
|
240
|
+
Relative path to a JSONL file with one evaluation row per line. Rows
|
|
241
|
+
must contain at least ``input`` and ``expected``; optional fields
|
|
242
|
+
``context``, ``tool_calls``, and ``tool_definitions`` drive evaluator
|
|
243
|
+
auto-selection.
|
|
244
|
+
|
|
245
|
+
``prompt_file``
|
|
246
|
+
Optional source-controlled instructions file for Foundry prompt-agent
|
|
247
|
+
CI/CD. Deployment workflows create a candidate Foundry agent version
|
|
248
|
+
from this file, evaluate that exact version, then mark it as deployed
|
|
249
|
+
only when the gate passes.
|
|
250
|
+
|
|
251
|
+
``thresholds``
|
|
252
|
+
Optional dict of metric name → criteria expression. When omitted, the
|
|
253
|
+
evaluator catalog provides sensible defaults per metric.
|
|
254
|
+
|
|
255
|
+
``protocol``
|
|
256
|
+
Optional, only relevant for URL-based ``agent`` values. Defaults to
|
|
257
|
+
``"responses"`` for Foundry hosted endpoints and ``"http-json"`` for
|
|
258
|
+
any other HTTPS URL.
|
|
259
|
+
|
|
260
|
+
``request_field`` / ``response_field`` / ``tool_calls_field``
|
|
261
|
+
``http-json`` and ``invocations`` only. JSON keys / dot-paths used to
|
|
262
|
+
marshal each dataset row into the request body and to extract the
|
|
263
|
+
response. Defaults are sensible for OpenAI-compatible / ACA endpoints.
|
|
264
|
+
|
|
265
|
+
``headers`` / ``auth_header_env``
|
|
266
|
+
Optional HTTP request configuration for ``http-json`` and
|
|
267
|
+
``invocations`` targets.
|
|
268
|
+
|
|
269
|
+
``evaluators``
|
|
270
|
+
Optional escape hatch: explicit list of evaluator names that overrides
|
|
271
|
+
the auto-selection rules. Most users should leave this unset.
|
|
272
|
+
|
|
273
|
+
``dataset_sync``
|
|
274
|
+
Optional cloud-evaluation dataset submission policy. The local JSONL
|
|
275
|
+
remains the source of truth; this block controls whether cloud evals
|
|
276
|
+
use inline compatibility or require a Foundry dataset reference.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
version: int = Field(..., description="Schema version. Must be 1.")
|
|
280
|
+
agent: str = Field(..., description="Target identifier (name:version, URL, or model:deployment)")
|
|
281
|
+
dataset: Path = Field(..., description="Path to a JSONL dataset file")
|
|
282
|
+
prompt_file: Optional[Path] = Field(
|
|
283
|
+
None,
|
|
284
|
+
description=(
|
|
285
|
+
"Optional source-controlled prompt/instructions file used by "
|
|
286
|
+
"prompt-agent CI/CD deployment workflows."
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
thresholds: Dict[str, Any] = Field(
|
|
291
|
+
default_factory=dict,
|
|
292
|
+
description="Metric name -> criteria expression (e.g. '>=3').",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
protocol: Optional[Protocol] = None
|
|
296
|
+
request_field: Optional[str] = None
|
|
297
|
+
response_field: Optional[str] = None
|
|
298
|
+
tool_calls_field: Optional[str] = None
|
|
299
|
+
headers: Dict[str, str] = Field(default_factory=dict)
|
|
300
|
+
auth_header_env: Optional[str] = None
|
|
301
|
+
|
|
302
|
+
evaluators: Optional[List[EvaluatorOverride]] = None
|
|
303
|
+
|
|
304
|
+
publish: bool = Field(
|
|
305
|
+
False,
|
|
306
|
+
description=(
|
|
307
|
+
"Whether to publish results to the Foundry Evaluations panel.\n"
|
|
308
|
+
"- false (default for execution: local): only local artifacts.\n"
|
|
309
|
+
"- true (forced for execution: cloud): publish to Foundry.\n"
|
|
310
|
+
"\n"
|
|
311
|
+
"Destination is derived from 'execution':\n"
|
|
312
|
+
" execution: local + publish: true → Classic Foundry (upload metrics)\n"
|
|
313
|
+
" execution: cloud + publish: true → New Foundry (server-side run)\n"
|
|
314
|
+
"\n"
|
|
315
|
+
"execution: cloud always publishes (Foundry hosts the run by "
|
|
316
|
+
"definition); setting publish: false with execution: cloud is "
|
|
317
|
+
"rejected as a contradiction."
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
execution: ExecutionMode = Field(
|
|
321
|
+
"local",
|
|
322
|
+
description=(
|
|
323
|
+
"Where to execute the agent and evaluators.\n"
|
|
324
|
+
"- local (default): AgentOps invokes the agent row-by-row locally.\n"
|
|
325
|
+
"- cloud: Foundry runs the agent and evaluators server-side, and "
|
|
326
|
+
"the run is implicitly published to the New Foundry Evaluations "
|
|
327
|
+
"panel (publish defaults to true)."
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
project_endpoint: Optional[str] = Field(
|
|
331
|
+
None,
|
|
332
|
+
description=(
|
|
333
|
+
"Optional Foundry project endpoint URL used for Foundry target "
|
|
334
|
+
"invocation and publishing. When omitted, AgentOps reads "
|
|
335
|
+
"AZURE_AI_FOUNDRY_PROJECT_ENDPOINT."
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
dataset_sync: DatasetSyncConfig = Field(
|
|
339
|
+
default_factory=DatasetSyncConfig,
|
|
340
|
+
description="Cloud evaluation dataset submission policy.",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
model_config = ConfigDict(extra="forbid")
|
|
344
|
+
|
|
345
|
+
@model_validator(mode="before")
|
|
346
|
+
@classmethod
|
|
347
|
+
def _default_publish_for_cloud(cls, data: Any) -> Any:
|
|
348
|
+
"""``execution: cloud`` implies ``publish: true`` when publish is
|
|
349
|
+
omitted, because a cloud run is always recorded by Foundry - there
|
|
350
|
+
is no way to "not publish" a server-side run.
|
|
351
|
+
"""
|
|
352
|
+
if not isinstance(data, dict):
|
|
353
|
+
return data
|
|
354
|
+
if data.get("execution") == "cloud" and "publish" not in data:
|
|
355
|
+
data["publish"] = True
|
|
356
|
+
return data
|
|
357
|
+
|
|
358
|
+
@model_validator(mode="before")
|
|
359
|
+
@classmethod
|
|
360
|
+
def _reject_legacy(cls, data: Any) -> Any:
|
|
361
|
+
if not isinstance(data, dict):
|
|
362
|
+
return data
|
|
363
|
+
legacy = _LEGACY_TOP_LEVEL_KEYS & set(data.keys())
|
|
364
|
+
if legacy:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
"agentops.yaml uses the new flat schema (see docs/concepts.md). "
|
|
367
|
+
f"Remove legacy keys: {sorted(legacy)}. The minimal config is "
|
|
368
|
+
"version + agent + dataset."
|
|
369
|
+
)
|
|
370
|
+
return data
|
|
371
|
+
|
|
372
|
+
@field_validator("version")
|
|
373
|
+
@classmethod
|
|
374
|
+
def _check_version(cls, value: int) -> int:
|
|
375
|
+
if value != 1:
|
|
376
|
+
raise ValueError(
|
|
377
|
+
f"agentops.yaml version must be 1 (got {value!r})"
|
|
378
|
+
)
|
|
379
|
+
return value
|
|
380
|
+
|
|
381
|
+
@field_validator("agent")
|
|
382
|
+
@classmethod
|
|
383
|
+
def _agent_non_empty(cls, value: str) -> str:
|
|
384
|
+
if not value.strip():
|
|
385
|
+
raise ValueError("agent must be non-empty")
|
|
386
|
+
return value.strip()
|
|
387
|
+
|
|
388
|
+
@model_validator(mode="after")
|
|
389
|
+
def _validate_publish_compat(self) -> "AgentOpsConfig":
|
|
390
|
+
"""``execution: cloud`` + ``publish: false`` is a contradiction.
|
|
391
|
+
|
|
392
|
+
A cloud run is always recorded by Foundry - the eval definition,
|
|
393
|
+
run, and artifacts live on the Foundry side as a side effect of
|
|
394
|
+
executing there. ``publish: false`` cannot prevent that.
|
|
395
|
+
"""
|
|
396
|
+
if self.execution == "cloud" and not self.publish:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
"execution: cloud always publishes to the New Foundry "
|
|
399
|
+
"Evaluations panel (Foundry hosts the run). Remove "
|
|
400
|
+
"'publish: false' or set 'execution: local' if you want "
|
|
401
|
+
"to keep results local-only."
|
|
402
|
+
)
|
|
403
|
+
return self
|
|
404
|
+
|
|
405
|
+
@model_validator(mode="after")
|
|
406
|
+
def _validate_protocol_compat(self) -> "AgentOpsConfig":
|
|
407
|
+
kind = classify_agent(self.agent, self.protocol).kind
|
|
408
|
+
if kind == "foundry_prompt" and self.protocol is not None:
|
|
409
|
+
raise ValueError(
|
|
410
|
+
"agent of the form 'name:version' is a Foundry prompt agent "
|
|
411
|
+
"and does not accept a 'protocol' field"
|
|
412
|
+
)
|
|
413
|
+
if kind == "model_direct" and self.protocol is not None:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
"agent of the form 'model:<deployment>' does not accept a "
|
|
416
|
+
"'protocol' field"
|
|
417
|
+
)
|
|
418
|
+
if kind != "http_json" and (
|
|
419
|
+
self.request_field
|
|
420
|
+
or self.response_field
|
|
421
|
+
or self.tool_calls_field
|
|
422
|
+
or self.headers
|
|
423
|
+
or self.auth_header_env
|
|
424
|
+
):
|
|
425
|
+
# Foundry hosted (responses/invocations) defines its own wire
|
|
426
|
+
# format. HTTP-only request/response shaping is invalid there.
|
|
427
|
+
if kind == "foundry_hosted" and self.protocol == "invocations":
|
|
428
|
+
# Invocations passes JSON through; users may need headers.
|
|
429
|
+
pass
|
|
430
|
+
else:
|
|
431
|
+
raise ValueError(
|
|
432
|
+
"request_field / response_field / tool_calls_field / "
|
|
433
|
+
"headers / auth_header_env are only valid for HTTP/JSON "
|
|
434
|
+
"or Foundry hosted (invocations) targets"
|
|
435
|
+
)
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
def parsed_thresholds(self) -> List[Threshold]:
|
|
439
|
+
"""Return the threshold dict parsed into structured rules."""
|
|
440
|
+
return [
|
|
441
|
+
Threshold.from_expression(metric, expression)
|
|
442
|
+
for metric, expression in self.thresholds.items()
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
def resolved_target(self) -> "TargetResolution":
|
|
446
|
+
"""Return the resolved target classification."""
|
|
447
|
+
return classify_agent(self.agent, self.protocol)
|
|
448
|
+
|
|
449
|
+
def publish_target(self) -> Optional[PublishTarget]:
|
|
450
|
+
"""Return the internal publisher dispatch key, or ``None`` if disabled.
|
|
451
|
+
|
|
452
|
+
Derived from ``execution`` + ``publish``:
|
|
453
|
+
|
|
454
|
+
- ``publish: false`` → ``None`` (no publishing)
|
|
455
|
+
- ``publish: true``, ``execution: local`` → ``"foundry"`` (Classic)
|
|
456
|
+
- ``publish: true``, ``execution: cloud`` → ``"foundry_cloud"`` (New)
|
|
457
|
+
"""
|
|
458
|
+
if not self.publish:
|
|
459
|
+
return None
|
|
460
|
+
if self.execution == "cloud":
|
|
461
|
+
return "foundry_cloud"
|
|
462
|
+
return "foundry"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ---------------------------------------------------------------------------
|
|
466
|
+
# Agent classifier
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass(frozen=True)
|
|
471
|
+
class TargetResolution:
|
|
472
|
+
"""Result of classifying the ``agent`` field."""
|
|
473
|
+
|
|
474
|
+
kind: TargetKind
|
|
475
|
+
protocol: Optional[Protocol]
|
|
476
|
+
raw: str
|
|
477
|
+
#: For ``foundry_prompt``: the agent name (left of the colon).
|
|
478
|
+
name: Optional[str] = None
|
|
479
|
+
#: For ``foundry_prompt``: the version (right of the colon).
|
|
480
|
+
version: Optional[str] = None
|
|
481
|
+
#: For ``foundry_hosted`` / ``http_json``: the target URL.
|
|
482
|
+
url: Optional[str] = None
|
|
483
|
+
#: For ``model_direct``: the deployment name.
|
|
484
|
+
deployment: Optional[str] = None
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _looks_like_foundry_url(url: str) -> bool:
|
|
488
|
+
"""Return ``True`` when ``url`` matches a Foundry hosted endpoint pattern.
|
|
489
|
+
|
|
490
|
+
Heuristic - Foundry URLs include the segment ``/agents/`` and the host
|
|
491
|
+
ends in a Foundry-recognized domain. We err on the side of accepting more
|
|
492
|
+
URLs as Foundry hosted (the user can force ``http-json`` via ``protocol``).
|
|
493
|
+
"""
|
|
494
|
+
lowered = url.lower()
|
|
495
|
+
foundry_domains = (
|
|
496
|
+
".azure.com",
|
|
497
|
+
".azureml.ms",
|
|
498
|
+
".cognitiveservices.azure.com",
|
|
499
|
+
".services.ai.azure.com",
|
|
500
|
+
".inference.ml.azure.com",
|
|
501
|
+
".azurewebsites.net", # rare; users can override
|
|
502
|
+
)
|
|
503
|
+
return any(domain in lowered for domain in foundry_domains)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def classify_agent(
|
|
507
|
+
agent: str,
|
|
508
|
+
protocol: Optional[Protocol] = None,
|
|
509
|
+
) -> TargetResolution:
|
|
510
|
+
"""Classify the ``agent`` value into a target kind.
|
|
511
|
+
|
|
512
|
+
Resolution table:
|
|
513
|
+
|
|
514
|
+
+-------------------------+--------------------------+-----------------------+
|
|
515
|
+
| ``agent`` value | ``protocol`` | ``TargetKind`` |
|
|
516
|
+
+=========================+==========================+=======================+
|
|
517
|
+
| ``model:gpt-4o`` | n/a | ``model_direct`` |
|
|
518
|
+
+-------------------------+--------------------------+-----------------------+
|
|
519
|
+
| ``my-rag:3`` | n/a | ``foundry_prompt`` |
|
|
520
|
+
+-------------------------+--------------------------+-----------------------+
|
|
521
|
+
| ``https://...foundry`` | omitted or ``responses`` | ``foundry_hosted`` |
|
|
522
|
+
| (foundry-shaped URL) | | (responses) |
|
|
523
|
+
+-------------------------+--------------------------+-----------------------+
|
|
524
|
+
| ``https://...foundry`` | ``invocations`` | ``foundry_hosted`` |
|
|
525
|
+
| | | (invocations) |
|
|
526
|
+
+-------------------------+--------------------------+-----------------------+
|
|
527
|
+
| ``https://other-host`` | omitted or ``http-json`` | ``http_json`` |
|
|
528
|
+
+-------------------------+--------------------------+-----------------------+
|
|
529
|
+
"""
|
|
530
|
+
raw = agent.strip()
|
|
531
|
+
|
|
532
|
+
if raw.lower().startswith("model:"):
|
|
533
|
+
deployment = raw.split(":", 1)[1].strip()
|
|
534
|
+
if not deployment:
|
|
535
|
+
raise ValueError("model: prefix requires a deployment name")
|
|
536
|
+
return TargetResolution(
|
|
537
|
+
kind="model_direct",
|
|
538
|
+
protocol=None,
|
|
539
|
+
raw=raw,
|
|
540
|
+
deployment=deployment,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
lowered = raw.lower()
|
|
544
|
+
if lowered.startswith(("http://", "https://")):
|
|
545
|
+
if _looks_like_foundry_url(raw):
|
|
546
|
+
resolved_protocol: Protocol = protocol or "responses"
|
|
547
|
+
if resolved_protocol not in {"responses", "invocations"}:
|
|
548
|
+
raise ValueError(
|
|
549
|
+
"Foundry hosted endpoints accept only protocol "
|
|
550
|
+
"'responses' or 'invocations'"
|
|
551
|
+
)
|
|
552
|
+
return TargetResolution(
|
|
553
|
+
kind="foundry_hosted",
|
|
554
|
+
protocol=resolved_protocol,
|
|
555
|
+
raw=raw,
|
|
556
|
+
url=raw,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
resolved_protocol = protocol or "http-json"
|
|
560
|
+
if resolved_protocol != "http-json":
|
|
561
|
+
raise ValueError(
|
|
562
|
+
"non-Foundry URLs must use protocol 'http-json' "
|
|
563
|
+
f"(got {resolved_protocol!r})"
|
|
564
|
+
)
|
|
565
|
+
return TargetResolution(
|
|
566
|
+
kind="http_json",
|
|
567
|
+
protocol="http-json",
|
|
568
|
+
raw=raw,
|
|
569
|
+
url=raw,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
if ":" in raw:
|
|
573
|
+
name, _, version = raw.partition(":")
|
|
574
|
+
name = name.strip()
|
|
575
|
+
version = version.strip()
|
|
576
|
+
if not name or not version:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
"Foundry prompt agent must be 'name:version' "
|
|
579
|
+
f"(got {raw!r})"
|
|
580
|
+
)
|
|
581
|
+
return TargetResolution(
|
|
582
|
+
kind="foundry_prompt",
|
|
583
|
+
protocol=None,
|
|
584
|
+
raw=raw,
|
|
585
|
+
name=name,
|
|
586
|
+
version=version,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
raise ValueError(
|
|
590
|
+
f"unrecognized agent value {raw!r}: expected 'name:version', "
|
|
591
|
+
"'https://...', or 'model:<deployment>'"
|
|
592
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""YAML config loaders for AgentOps schemas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from agentops.core.agentops_config import AgentOpsConfig
|
|
11
|
+
from agentops.utils.yaml import load_yaml
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_agentops_config(path: Path) -> AgentOpsConfig:
|
|
17
|
+
"""Load the flat 1.0 ``agentops.yaml`` schema."""
|
|
18
|
+
data = load_yaml(path)
|
|
19
|
+
try:
|
|
20
|
+
return AgentOpsConfig.model_validate(data)
|
|
21
|
+
except ValidationError as exc:
|
|
22
|
+
raise ValueError(f"AgentOpsConfig validation error: {exc}") from exc
|