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.
Files changed (142) hide show
  1. agentops/__init__.py +10 -0
  2. agentops/__main__.py +6 -0
  3. agentops/agent/__init__.py +12 -0
  4. agentops/agent/_legacy_ids.py +92 -0
  5. agentops/agent/analyzer.py +207 -0
  6. agentops/agent/checks/__init__.py +1 -0
  7. agentops/agent/checks/catalog.py +880 -0
  8. agentops/agent/checks/errors.py +279 -0
  9. agentops/agent/checks/foundry_config.py +75 -0
  10. agentops/agent/checks/latency.py +84 -0
  11. agentops/agent/checks/opex.py +157 -0
  12. agentops/agent/checks/opex_workspace.py +874 -0
  13. agentops/agent/checks/posture.py +36 -0
  14. agentops/agent/checks/posture_rules/__init__.py +53 -0
  15. agentops/agent/checks/posture_rules/content_filter.py +59 -0
  16. agentops/agent/checks/posture_rules/diagnostics.py +74 -0
  17. agentops/agent/checks/posture_rules/local_auth.py +55 -0
  18. agentops/agent/checks/posture_rules/managed_identity.py +59 -0
  19. agentops/agent/checks/posture_rules/network.py +68 -0
  20. agentops/agent/checks/regression.py +78 -0
  21. agentops/agent/checks/release_readiness.py +182 -0
  22. agentops/agent/checks/safety.py +247 -0
  23. agentops/agent/checks/spec_conformance.py +375 -0
  24. agentops/agent/cockpit.py +5159 -0
  25. agentops/agent/config.py +240 -0
  26. agentops/agent/findings.py +113 -0
  27. agentops/agent/history.py +142 -0
  28. agentops/agent/knowledge/__init__.py +182 -0
  29. agentops/agent/knowledge/waf-checklist.csv +39 -0
  30. agentops/agent/llm_assist/__init__.py +16 -0
  31. agentops/agent/llm_assist/_base.py +124 -0
  32. agentops/agent/llm_assist/_bundle_rule.py +154 -0
  33. agentops/agent/llm_assist/_client.py +347 -0
  34. agentops/agent/llm_assist/_dataset_rules.py +191 -0
  35. agentops/agent/llm_assist/_engine.py +106 -0
  36. agentops/agent/llm_assist/_prompt_rules.py +291 -0
  37. agentops/agent/llm_assist/_spec_rules.py +235 -0
  38. agentops/agent/production_telemetry.py +430 -0
  39. agentops/agent/report.py +207 -0
  40. agentops/agent/server/__init__.py +1 -0
  41. agentops/agent/server/app.py +84 -0
  42. agentops/agent/server/auth.py +94 -0
  43. agentops/agent/server/chat.py +44 -0
  44. agentops/agent/server/protocol.py +72 -0
  45. agentops/agent/sources/__init__.py +1 -0
  46. agentops/agent/sources/azure_monitor.py +523 -0
  47. agentops/agent/sources/azure_resources.py +602 -0
  48. agentops/agent/sources/foundry_control.py +174 -0
  49. agentops/agent/sources/results_history.py +494 -0
  50. agentops/agent/sources/spec_detectors/__init__.py +42 -0
  51. agentops/agent/sources/spec_detectors/_base.py +58 -0
  52. agentops/agent/sources/spec_detectors/agents_md.py +75 -0
  53. agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
  54. agentops/agent/time_range.py +117 -0
  55. agentops/cli/__init__.py +1 -0
  56. agentops/cli/app.py +4823 -0
  57. agentops/core/__init__.py +1 -0
  58. agentops/core/agentops_config.py +592 -0
  59. agentops/core/config_loader.py +22 -0
  60. agentops/core/evaluators.py +480 -0
  61. agentops/core/release_evidence.py +56 -0
  62. agentops/core/results.py +117 -0
  63. agentops/mcp/__init__.py +10 -0
  64. agentops/mcp/server.py +232 -0
  65. agentops/pipeline/__init__.py +8 -0
  66. agentops/pipeline/cloud_results.py +189 -0
  67. agentops/pipeline/cloud_runner.py +901 -0
  68. agentops/pipeline/comparison.py +108 -0
  69. agentops/pipeline/diagnostics.py +51 -0
  70. agentops/pipeline/invocations.py +535 -0
  71. agentops/pipeline/official_eval.py +414 -0
  72. agentops/pipeline/orchestrator.py +775 -0
  73. agentops/pipeline/prompt_deploy.py +377 -0
  74. agentops/pipeline/publisher.py +121 -0
  75. agentops/pipeline/reporter.py +202 -0
  76. agentops/pipeline/runtime.py +409 -0
  77. agentops/pipeline/thresholds.py +84 -0
  78. agentops/services/__init__.py +1 -0
  79. agentops/services/cicd.py +720 -0
  80. agentops/services/eval_analysis.py +848 -0
  81. agentops/services/evidence_pack.py +757 -0
  82. agentops/services/initializer.py +86 -0
  83. agentops/services/preflight.py +470 -0
  84. agentops/services/setup_wizard.py +709 -0
  85. agentops/services/skills.py +643 -0
  86. agentops/services/trace_promotion.py +300 -0
  87. agentops/services/workflow_analysis.py +1129 -0
  88. agentops/templates/.gitignore +15 -0
  89. agentops/templates/__init__.py +1 -0
  90. agentops/templates/agent-server/Dockerfile +23 -0
  91. agentops/templates/agent-server/README.md +61 -0
  92. agentops/templates/agent-server/main.bicep +94 -0
  93. agentops/templates/agent.yaml +87 -0
  94. agentops/templates/agentops.yaml +58 -0
  95. agentops/templates/foundry.svg +71 -0
  96. agentops/templates/icon.png +0 -0
  97. agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
  98. agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
  99. agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
  100. agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
  101. agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
  102. agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
  103. agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
  104. agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
  105. agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
  106. agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
  107. agentops/templates/project.gitignore +36 -0
  108. agentops/templates/sample-traces.jsonl +3 -0
  109. agentops/templates/skills/agentops-agent/SKILL.md +137 -0
  110. agentops/templates/skills/agentops-config/SKILL.md +113 -0
  111. agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
  112. agentops/templates/skills/agentops-eval/SKILL.md +189 -0
  113. agentops/templates/skills/agentops-report/SKILL.md +71 -0
  114. agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
  115. agentops/templates/smoke.jsonl +3 -0
  116. agentops/templates/waf-checklist.README.md +84 -0
  117. agentops/templates/waf-checklist.csv +22 -0
  118. agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
  119. agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
  120. agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
  121. agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
  122. agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
  123. agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
  124. agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
  125. agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
  126. agentops/templates/workflows/agentops-pr.yml +148 -0
  127. agentops/templates/workflows/agentops-watchdog.yml +122 -0
  128. agentops/utils/__init__.py +1 -0
  129. agentops/utils/azd_env.py +435 -0
  130. agentops/utils/azure_endpoints.py +62 -0
  131. agentops/utils/colors.py +47 -0
  132. agentops/utils/dotenv_loader.py +105 -0
  133. agentops/utils/foundry_discovery.py +229 -0
  134. agentops/utils/logging.py +59 -0
  135. agentops/utils/telemetry.py +554 -0
  136. agentops/utils/yaml.py +36 -0
  137. agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
  138. agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
  139. agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
  140. agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
  141. agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
  142. 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