dbt-cortex-agent 0.0.3__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 (40) hide show
  1. dbt_cortex_agent/__init__.py +5 -0
  2. dbt_cortex_agent/artifacts.py +26 -0
  3. dbt_cortex_agent/cli.py +90 -0
  4. dbt_cortex_agent/commands/__init__.py +1 -0
  5. dbt_cortex_agent/commands/agent.py +115 -0
  6. dbt_cortex_agent/commands/bootstrap.py +92 -0
  7. dbt_cortex_agent/commands/common.py +62 -0
  8. dbt_cortex_agent/commands/eval.py +143 -0
  9. dbt_cortex_agent/commands/manifest.py +48 -0
  10. dbt_cortex_agent/commands/skill.py +129 -0
  11. dbt_cortex_agent/config.py +88 -0
  12. dbt_cortex_agent/dbt_runner.py +93 -0
  13. dbt_cortex_agent/doctor.py +204 -0
  14. dbt_cortex_agent/eval/__init__.py +0 -0
  15. dbt_cortex_agent/eval/baseline.py +44 -0
  16. dbt_cortex_agent/eval/compare.py +68 -0
  17. dbt_cortex_agent/eval/dataset.py +128 -0
  18. dbt_cortex_agent/eval/gate.py +31 -0
  19. dbt_cortex_agent/eval/lifecycle.py +567 -0
  20. dbt_cortex_agent/eval/results.py +195 -0
  21. dbt_cortex_agent/execution_context.py +136 -0
  22. dbt_cortex_agent/identifiers.py +46 -0
  23. dbt_cortex_agent/init.py +322 -0
  24. dbt_cortex_agent/invoke.py +140 -0
  25. dbt_cortex_agent/manifest.py +285 -0
  26. dbt_cortex_agent/skills.py +181 -0
  27. dbt_cortex_agent/snow.py +11 -0
  28. dbt_cortex_agent/starters/orders/models/agents/orders_assistant/agent.yml +7 -0
  29. dbt_cortex_agent/starters/orders/models/agents/orders_assistant/evals/core.yml +54 -0
  30. dbt_cortex_agent/starters/orders/models/agents/orders_assistant/evals/orders_assistant_core.sql +50 -0
  31. dbt_cortex_agent/starters/orders/models/agents/orders_assistant/orders_assistant.sql +54 -0
  32. dbt_cortex_agent/starters/orders/models/semantic/sem_orders.sql +40 -0
  33. dbt_cortex_agent/starters/orders/seeds/orders.csv +4 -0
  34. dbt_cortex_agent/starters/orders/seeds/orders.yml +19 -0
  35. dbt_cortex_agent-0.0.3.dist-info/METADATA +196 -0
  36. dbt_cortex_agent-0.0.3.dist-info/RECORD +40 -0
  37. dbt_cortex_agent-0.0.3.dist-info/WHEEL +5 -0
  38. dbt_cortex_agent-0.0.3.dist-info/entry_points.txt +2 -0
  39. dbt_cortex_agent-0.0.3.dist-info/licenses/LICENSE +201 -0
  40. dbt_cortex_agent-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,567 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import hashlib
5
+ import re
6
+ import time
7
+ from dataclasses import dataclass, replace
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any, Callable
11
+
12
+ from ..config import Config
13
+ from ..dbt_runner import CommandRunner, run_dbt_operation, run_dbt_parse
14
+ from ..identifiers import fqn, identifier
15
+ from ..manifest import assert_resource_databases_allowed
16
+ from ..skills import assert_apply_safety
17
+ from .dataset import annotate_rows, validate_table
18
+ from .results import build_candidate, write_candidate, write_diagnostic
19
+
20
+
21
+ TERMINAL_SUCCESS = {"COMPLETED", "SUCCEEDED", "DONE"}
22
+ TERMINAL_FAILURE = {"FAILED", "ERROR", "CANCELLED", "INVOCATION_FAILED", "INVOCATION_ERROR"}
23
+ PARTIAL_COMPLETION = {"INVOCATION_PARTIALLY_COMPLETED"}
24
+ RETRYABLE_DETAILS = (
25
+ "invocation failed",
26
+ "service is currently unavailable",
27
+ "internal error",
28
+ "timed out",
29
+ "timeout",
30
+ "rate limit",
31
+ )
32
+ _PATH_COMPONENT = re.compile(r"^[A-Za-z0-9_$.-]+$")
33
+ PLAN_SCHEMA_VERSION = 2
34
+ _PLAN_PREFIX = "CORTEX_EVAL_PLAN_JSON="
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class EvalPlan:
39
+ schema_version: int
40
+ agent_name: str
41
+ suite_name: str
42
+ eval_model: str
43
+ table_fqn: str
44
+ agent_fqn: str
45
+ stage_fqn: str
46
+ result_database: str
47
+ result_schema: str
48
+ target_name: str
49
+ target_role: str
50
+ target_database: str
51
+ target_schema: str
52
+ target_warehouse: str | None
53
+ native_eval_config: dict[str, Any]
54
+ dataset_name_token: str
55
+ config_filename_template: str
56
+ metric_names: list[str]
57
+ thresholds: dict[str, float]
58
+ regression_tolerances: dict[str, float]
59
+ ordered_ground_truth_refs: list[str]
60
+ suite_signature: str
61
+ plan_identity: dict[str, Any]
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class PollResult:
66
+ status: str
67
+ details: str
68
+ request_ids: tuple[str, ...] = ()
69
+ inference_ids: tuple[str, ...] = ()
70
+ error_code: str | None = None
71
+ observed_status: str | None = None
72
+ expected_records: int | None = None
73
+ actual_records: int | None = None
74
+
75
+ @property
76
+ def succeeded(self) -> bool:
77
+ return self.status in TERMINAL_SUCCESS
78
+
79
+
80
+ def _numeric_policy(raw: Any, label: str) -> dict[str, float]:
81
+ if not isinstance(raw, dict):
82
+ raise ValueError(f"Evaluation plan {label} must be an object")
83
+ try:
84
+ values = {str(name): float(value) for name, value in raw.items()}
85
+ except (TypeError, ValueError) as exc:
86
+ raise ValueError(f"Evaluation plan {label} values must be numeric") from exc
87
+ if any(value < 0 for value in values.values()):
88
+ raise ValueError(f"Evaluation plan {label} values must be non-negative")
89
+ return values
90
+
91
+
92
+ def _plan_from_payload(payload: dict[str, Any], agent_name: str, suite_name: str) -> EvalPlan:
93
+ if payload.get("schema_version") != PLAN_SCHEMA_VERSION:
94
+ raise ValueError(f"Evaluation plan schema_version must be {PLAN_SCHEMA_VERSION}")
95
+ identity = payload.get("identity")
96
+ if not isinstance(identity, dict):
97
+ raise ValueError("Evaluation plan identity must be an object")
98
+ required_identity = {
99
+ "agent_name", "suite_name", "eval_model", "agent_fqn",
100
+ "dataset_fqn", "stage_fqn", "target_name", "target_role", "target_database",
101
+ "target_schema", "result_database", "result_schema",
102
+ }
103
+ missing_identity = sorted(required_identity - identity.keys())
104
+ if missing_identity:
105
+ raise ValueError(
106
+ f"Evaluation plan identity is missing required fields: {', '.join(missing_identity)}"
107
+ )
108
+ if identity.get("agent_name") != agent_name or identity.get("suite_name") != suite_name:
109
+ raise ValueError("Evaluation plan identity does not match the selected Agent and suite")
110
+ if "projection" in identity or "projection" in payload:
111
+ raise ValueError("Evaluation plan must not contain projection identity")
112
+ signature_material = payload.get("signature_material")
113
+ if not isinstance(signature_material, str):
114
+ raise ValueError("Evaluation plan signature_material must be a string")
115
+ expected_signature = hashlib.md5(signature_material.encode("utf-8")).hexdigest()
116
+ if payload.get("suite_signature") != expected_signature:
117
+ raise ValueError("Evaluation plan suite_signature does not match signature_material")
118
+ try:
119
+ signed = json.loads(signature_material)
120
+ except json.JSONDecodeError as exc:
121
+ raise ValueError("Evaluation plan signature_material is not valid JSON") from exc
122
+ signed_fields = {
123
+ "plan_schema_version": payload["schema_version"],
124
+ "identity": identity,
125
+ "native_eval_config": payload.get("native_eval_config"),
126
+ "metric_names": payload.get("metric_names"),
127
+ "thresholds": payload.get("thresholds"),
128
+ "regression_tolerances": payload.get("regression_tolerances"),
129
+ "ordered_ground_truth_refs": payload.get("ordered_ground_truth_refs"),
130
+ }
131
+ if signed != signed_fields:
132
+ raise ValueError("Evaluation plan signed fields do not match signature_material")
133
+ config_template = payload.get("native_eval_config")
134
+ refs = payload.get("ordered_ground_truth_refs")
135
+ metric_names = payload.get("metric_names")
136
+ if not isinstance(config_template, dict) or not isinstance(refs, list) or not isinstance(metric_names, list):
137
+ raise ValueError("Evaluation plan config, metric_names, and ordered refs have invalid types")
138
+ config_agent = (
139
+ config_template.get("evaluation", {}).get("agent_params", {}).get("agent_name")
140
+ )
141
+ if config_agent != identity.get("agent_fqn"):
142
+ raise ValueError("Evaluation plan native config Agent must match signed agent_fqn")
143
+ refs = [str(value) for value in refs]
144
+ if not refs or any(not value for value in refs) or len(refs) != len(set(refs)):
145
+ raise ValueError("Evaluation plan ordered_ground_truth_refs must be non-empty and unique")
146
+ metric_names = [str(value) for value in metric_names]
147
+ if not metric_names or len(metric_names) != len(set(metric_names)):
148
+ raise ValueError("Evaluation plan metric_names must be non-empty and unique")
149
+ token = payload.get("dataset_name_token")
150
+ filename_template = payload.get("config_filename_template")
151
+ if not isinstance(token, str) or not token or not isinstance(filename_template, str):
152
+ raise ValueError("Evaluation plan dataset token and config filename template are required")
153
+ if not identity.get("target_role"):
154
+ raise ValueError("Evaluation plan target_role is required")
155
+ return EvalPlan(
156
+ schema_version=PLAN_SCHEMA_VERSION,
157
+ agent_name=agent_name,
158
+ suite_name=suite_name,
159
+ eval_model=str(identity["eval_model"]),
160
+ table_fqn=fqn(str(identity["dataset_fqn"]), "eval table"),
161
+ agent_fqn=fqn(str(identity["agent_fqn"]), "Agent object"),
162
+ stage_fqn=fqn(str(identity["stage_fqn"]), "evaluation stage"),
163
+ result_database=identifier(str(identity["result_database"]), "result database"),
164
+ result_schema=identifier(str(identity["result_schema"]), "result schema"),
165
+ target_name=str(identity["target_name"]),
166
+ target_role=identifier(str(identity["target_role"]), "target role"),
167
+ target_database=identifier(str(identity["target_database"]), "target database"),
168
+ target_schema=identifier(str(identity["target_schema"]), "target schema"),
169
+ target_warehouse=(str(identity["target_warehouse"]) if identity.get("target_warehouse") else None),
170
+ native_eval_config=config_template,
171
+ dataset_name_token=token,
172
+ config_filename_template=filename_template,
173
+ metric_names=metric_names,
174
+ thresholds=_numeric_policy(payload.get("thresholds"), "thresholds"),
175
+ regression_tolerances=_numeric_policy(payload.get("regression_tolerances"), "regression_tolerances"),
176
+ ordered_ground_truth_refs=refs,
177
+ suite_signature=expected_signature,
178
+ plan_identity=dict(identity),
179
+ )
180
+
181
+
182
+ def _extract_plan(stdout: str, agent_name: str, suite_name: str) -> EvalPlan:
183
+ payloads = [line.split(_PLAN_PREFIX, 1)[1].strip() for line in stdout.splitlines() if _PLAN_PREFIX in line]
184
+ if len(payloads) != 1:
185
+ raise ValueError(f"Expected exactly one dbt evaluation plan payload, found {len(payloads)}")
186
+ try:
187
+ payload = json.loads(payloads[0])
188
+ except json.JSONDecodeError as exc:
189
+ raise ValueError("dbt evaluation plan payload is not valid JSON") from exc
190
+ if not isinstance(payload, dict):
191
+ raise ValueError("dbt evaluation plan payload must be a JSON object")
192
+ return _plan_from_payload(payload, agent_name, suite_name)
193
+
194
+
195
+ def build_plan(
196
+ config: Config,
197
+ *,
198
+ agent_name: str,
199
+ suite_name: str,
200
+ parse: bool = True,
201
+ runner: CommandRunner | None = None,
202
+ plan_payload: dict[str, Any] | None = None,
203
+ ) -> EvalPlan:
204
+ if plan_payload is not None:
205
+ return _plan_from_payload(plan_payload, agent_name, suite_name)
206
+ command_runner = runner or CommandRunner()
207
+ if parse:
208
+ parsed = run_dbt_parse(
209
+ config.dbt_executable,
210
+ config.project_dir,
211
+ config.target,
212
+ command_runner,
213
+ config.dbt_env,
214
+ )
215
+ if parsed.returncode != 0:
216
+ raise RuntimeError(parsed.stderr.strip() or parsed.stdout.strip() or "dbt parse failed")
217
+ rendered = run_dbt_operation(
218
+ config.dbt_executable,
219
+ config.project_dir,
220
+ config.target,
221
+ "dbt_cortex_agent.cortex_eval__execution_plan",
222
+ {"agent_name": agent_name, "suite_name": suite_name},
223
+ command_runner,
224
+ config.dbt_env,
225
+ )
226
+ if rendered.returncode != 0:
227
+ raise RuntimeError(rendered.stderr.strip() or rendered.stdout.strip() or "dbt evaluation plan render failed")
228
+ return _extract_plan(rendered.stdout, agent_name, suite_name)
229
+
230
+
231
+ def render_eval_config(plan: EvalPlan, dataset_name: str) -> str:
232
+ rendered = json.dumps(plan.native_eval_config, separators=(",", ":"))
233
+ if rendered.count(plan.dataset_name_token) != 2:
234
+ raise ValueError("dbt evaluation plan must contain exactly two dataset-name tokens")
235
+ return rendered.replace(plan.dataset_name_token, dataset_name)
236
+
237
+
238
+ def flatten_status_details(raw: Any) -> str:
239
+ if raw in (None, ""):
240
+ return ""
241
+ if isinstance(raw, (list, tuple)):
242
+ return "; ".join(str(item) for item in raw if item)
243
+ text = str(raw).strip()
244
+ if text.startswith("[") and text.endswith("]"):
245
+ try:
246
+ parsed = json.loads(text)
247
+ if isinstance(parsed, list):
248
+ return "; ".join(str(item) for item in parsed if item)
249
+ except json.JSONDecodeError:
250
+ pass
251
+ return text
252
+
253
+
254
+ def is_retryable(details: Any) -> bool:
255
+ value = flatten_status_details(details).lower()
256
+ return bool(value) and any(pattern in value for pattern in RETRYABLE_DETAILS)
257
+
258
+
259
+ def _is_retryable_result(status: PollResult) -> bool:
260
+ return (
261
+ status.status in PARTIAL_COMPLETION
262
+ or status.observed_status in PARTIAL_COMPLETION
263
+ or status.status == "INCOMPLETE_RESULTS"
264
+ or is_retryable(status.details)
265
+ )
266
+
267
+
268
+ def poll(
269
+ cursor,
270
+ run_name: str,
271
+ config_path: str,
272
+ *,
273
+ attempts: int,
274
+ interval: float,
275
+ sleep: Callable[[float], None] = time.sleep,
276
+ ) -> PollResult:
277
+ if attempts < 1:
278
+ raise ValueError("Poll attempts must be at least 1")
279
+ last_result: PollResult | None = None
280
+ for attempt in range(attempts):
281
+ cursor.execute(
282
+ "CALL EXECUTE_AI_EVALUATION('STATUS', OBJECT_CONSTRUCT('run_name', %s), %s)",
283
+ (run_name, config_path),
284
+ )
285
+ rows = cursor.fetchall()
286
+ if rows:
287
+ columns = [str(item[0]).upper() for item in cursor.description]
288
+ row = rows[0]
289
+ status = str(row[columns.index("STATUS")] if "STATUS" in columns else row[3]).upper()
290
+ details = flatten_status_details(
291
+ row[columns.index("STATUS_DETAILS")] if "STATUS_DETAILS" in columns else row[4] if len(row) > 4 else ""
292
+ )
293
+ request_ids = _status_identifiers(row, columns, "REQUEST_ID")
294
+ inference_ids = _status_identifiers(row, columns, "INFERENCE_ID")
295
+ error_code = _status_value(row, columns, "ERROR_CODE")
296
+ last_result = PollResult(
297
+ status,
298
+ details,
299
+ request_ids,
300
+ inference_ids,
301
+ error_code,
302
+ observed_status=status,
303
+ )
304
+ if status in TERMINAL_SUCCESS | TERMINAL_FAILURE:
305
+ return last_result
306
+ if attempt + 1 < attempts:
307
+ sleep(interval)
308
+ if last_result is None:
309
+ return PollResult("TIMEOUT", "poll attempts exhausted")
310
+ return replace(
311
+ last_result,
312
+ status="TIMEOUT",
313
+ details="poll attempts exhausted",
314
+ )
315
+
316
+
317
+ def _status_value(row: tuple[Any, ...], columns: list[str], name: str) -> str | None:
318
+ if name not in columns:
319
+ return None
320
+ value = row[columns.index(name)]
321
+ if value in (None, ""):
322
+ return None
323
+ return str(value)
324
+
325
+
326
+ def _status_identifiers(
327
+ row: tuple[Any, ...], columns: list[str], name: str
328
+ ) -> tuple[str, ...]:
329
+ value = _status_value(row, columns, name)
330
+ return (value,) if value else ()
331
+
332
+
333
+ def _diagnostic_category(status: PollResult) -> str:
334
+ if status.observed_status in PARTIAL_COMPLETION or status.status == "INCOMPLETE_RESULTS":
335
+ return "transient_platform_failure"
336
+ if status.status == "TIMEOUT":
337
+ return "timeout"
338
+ if is_retryable(status.details):
339
+ return "transient_platform_failure"
340
+ return "terminal_evaluation_failure"
341
+
342
+
343
+ def _failure_diagnostic(plan: EvalPlan, run_name: str, status: PollResult) -> dict[str, Any]:
344
+ diagnostic = {
345
+ "schema_version": 1,
346
+ "artifact_type": "evaluation_diagnostic",
347
+ "agent": plan.agent_name,
348
+ "agent_fqn": plan.agent_fqn,
349
+ "plan_identity": plan.plan_identity,
350
+ "suite": plan.suite_name,
351
+ "run_name": run_name,
352
+ "status": status.status,
353
+ "request_ids": list(status.request_ids),
354
+ "inference_ids": list(status.inference_ids),
355
+ "error": {
356
+ "category": _diagnostic_category(status),
357
+ "code": status.error_code,
358
+ },
359
+ }
360
+ if status.status == "TIMEOUT" and status.observed_status:
361
+ diagnostic["observed_status"] = status.observed_status
362
+ if status.expected_records is not None:
363
+ diagnostic["expected_records"] = status.expected_records
364
+ if status.actual_records is not None:
365
+ diagnostic["actual_records"] = status.actual_records
366
+ return diagnostic
367
+
368
+
369
+ def _upload_config(cursor, plan: EvalPlan, filename: str, content: str) -> str:
370
+ if not _PATH_COMPONENT.fullmatch(filename):
371
+ raise ValueError(f"Invalid evaluation config filename: {filename!r}")
372
+ cursor.execute(f"CREATE STAGE IF NOT EXISTS {plan.stage_fqn}")
373
+ cursor.execute(
374
+ f"COPY INTO @{plan.stage_fqn}/{filename} FROM (SELECT %s) "
375
+ "FILE_FORMAT=(TYPE=CSV FIELD_DELIMITER=NONE RECORD_DELIMITER=NONE COMPRESSION=NONE) "
376
+ "SINGLE=TRUE OVERWRITE=TRUE",
377
+ (content,),
378
+ )
379
+ return f"@{plan.stage_fqn}/{filename}"
380
+
381
+
382
+ def _results_complete(rows: list[dict[str, Any]], plan: EvalPlan) -> bool:
383
+ expected_inputs = len(plan.ordered_ground_truth_refs)
384
+ expected_metrics = {name.lower() for name in plan.metric_names}
385
+ metrics_by_input: dict[str, set[str]] = {}
386
+ for row in rows:
387
+ input_query = row.get("input")
388
+ metric_name = row.get("metric_name")
389
+ if not input_query or not metric_name:
390
+ continue
391
+ metrics_by_input.setdefault(str(input_query), set()).add(str(metric_name).lower())
392
+ return (
393
+ len(metrics_by_input) == expected_inputs
394
+ and all(metrics == expected_metrics for metrics in metrics_by_input.values())
395
+ )
396
+
397
+
398
+ def _fetch_rows(cursor, plan: EvalPlan, run_name: str, retries: int, sleep: Callable[[float], None]) -> list[dict[str, Any]]:
399
+ rows: list[dict[str, Any]] = []
400
+ database, schema, agent = plan.agent_fqn.split(".")
401
+ for attempt in range(retries + 1):
402
+ cursor.execute(
403
+ "SELECT * FROM TABLE(SNOWFLAKE.LOCAL.GET_AI_EVALUATION_DATA(%s,%s,%s,'CORTEX AGENT',%s))",
404
+ (database, schema, agent, run_name),
405
+ )
406
+ columns = [str(item[0]).lower() for item in cursor.description]
407
+ rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
408
+ if _results_complete(rows, plan):
409
+ return rows
410
+ if attempt < retries:
411
+ sleep(1)
412
+ return rows
413
+
414
+
415
+ def _result_record_count(rows: list[dict[str, Any]]) -> int:
416
+ return len({str(row.get("input")) for row in rows if row.get("input")})
417
+
418
+
419
+ def _agent_provenance(cursor, plan: EvalPlan) -> dict[str, Any]:
420
+ cursor.execute(f"DESCRIBE AGENT {plan.agent_fqn}")
421
+ columns = [str(item[0]).lower() for item in cursor.description]
422
+ row = cursor.fetchone()
423
+ if row is None:
424
+ raise RuntimeError(f"Agent {plan.agent_fqn} does not exist")
425
+ aliases: dict[str, Any] = {}
426
+ if "aliases" in columns and row[columns.index("aliases")]:
427
+ raw = row[columns.index("aliases")]
428
+ aliases = json.loads(raw) if isinstance(raw, str) else dict(raw)
429
+ default_version = aliases.get("DEFAULT")
430
+ if not default_version:
431
+ raise RuntimeError(f"Agent {plan.agent_fqn} has no resolvable DEFAULT version")
432
+ return {"default_version": str(default_version), "aliases": aliases}
433
+
434
+
435
+ def _assert_agent_exists_with_default(cursor, plan: EvalPlan) -> dict[str, Any]:
436
+ try:
437
+ provenance = _agent_provenance(cursor, plan)
438
+ except Exception as exc:
439
+ raise RuntimeError(
440
+ f"Evaluation requires existing Agent {plan.agent_fqn} with a resolvable DEFAULT version"
441
+ ) from exc
442
+ return provenance
443
+
444
+
445
+ def _default_connect(connection: str):
446
+ try:
447
+ import snowflake.connector
448
+ except ImportError as exc:
449
+ raise RuntimeError(
450
+ "Evaluation execution requires the 'runtime' extra: pip install 'dbt-cortex-agent[runtime]'"
451
+ ) from exc
452
+ return snowflake.connector.connect(connection_name=connection)
453
+
454
+
455
+ def run_evaluation(
456
+ config: Config,
457
+ plan: EvalPlan,
458
+ *,
459
+ apply: bool,
460
+ run_name: str | None = None,
461
+ poll_attempts: int = 60,
462
+ poll_interval: float = 30,
463
+ transient_retries: int = 1,
464
+ allowed_targets: list[str] | None = None,
465
+ allowed_databases: list[str] | None = None,
466
+ connect: Callable[[str], Any] = _default_connect,
467
+ sleep: Callable[[float], None] = time.sleep,
468
+ ) -> Path | None:
469
+ if not apply:
470
+ return None
471
+ if not config.connection_explicit or not config.connection or not config.warehouse:
472
+ raise ValueError("Evaluation apply requires explicit --connection and --warehouse")
473
+ assert_apply_safety(config, allowed_targets or [], allowed_databases or [])
474
+ resource_databases = {
475
+ plan.agent_fqn.split(".", 1)[0],
476
+ plan.table_fqn.split(".", 1)[0],
477
+ plan.stage_fqn.split(".", 1)[0],
478
+ plan.result_database,
479
+ plan.target_database,
480
+ }
481
+ assert_resource_databases_allowed(resource_databases, allowed_databases or [])
482
+ if transient_retries < 0:
483
+ raise ValueError("Transient retries must be non-negative")
484
+ if config.target and config.target != plan.target_name:
485
+ raise ValueError(
486
+ f"Configured target {config.target!r} does not match dbt plan target {plan.target_name!r}"
487
+ )
488
+ base_run = run_name or (
489
+ f"{plan.agent_name}_{plan.suite_name}_"
490
+ f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"
491
+ )
492
+ conn = connect(config.connection)
493
+ cursor = conn.cursor()
494
+ try:
495
+ cursor.execute(f"USE ROLE {plan.target_role}")
496
+ cursor.execute(f"USE WAREHOUSE {identifier(plan.target_warehouse or config.warehouse, 'warehouse')}")
497
+ cursor.execute(f"USE DATABASE {plan.result_database}")
498
+ cursor.execute(f"USE SCHEMA {plan.result_schema}")
499
+ initial_provenance = _assert_agent_exists_with_default(cursor, plan)
500
+ validate_table(cursor, plan.table_fqn, plan.metric_names, plan.ordered_ground_truth_refs)
501
+ final_run = base_run
502
+ status = PollResult("FAILED", "evaluation was not started")
503
+ rows: list[dict[str, Any]] = []
504
+ pre_start: dict[str, Any] | None = None
505
+ for retry in range(transient_retries + 1):
506
+ final_run = base_run if retry == 0 else f"{base_run}_r{retry}"
507
+ dataset_name = f"{final_run}_dataset"
508
+ filename = plan.config_filename_template.replace("__RUN_NAME__", final_run)
509
+ stage_path = _upload_config(cursor, plan, filename, render_eval_config(plan, dataset_name))
510
+ pre_start = _assert_agent_exists_with_default(cursor, plan)
511
+ if pre_start["default_version"] != initial_provenance["default_version"]:
512
+ raise RuntimeError(
513
+ f"Agent {plan.agent_fqn} DEFAULT version changed before evaluation START"
514
+ )
515
+ cursor.execute(
516
+ "CALL EXECUTE_AI_EVALUATION('START', OBJECT_CONSTRUCT('run_name', %s), %s)",
517
+ (final_run, stage_path),
518
+ )
519
+ status = poll(
520
+ cursor, final_run, stage_path, attempts=poll_attempts, interval=poll_interval, sleep=sleep
521
+ )
522
+ expected_records = len(plan.ordered_ground_truth_refs)
523
+ if status.succeeded or status.observed_status in PARTIAL_COMPLETION:
524
+ rows = _fetch_rows(cursor, plan, final_run, retries=2, sleep=sleep)
525
+ actual_records = _result_record_count(rows)
526
+ if status.succeeded and not _results_complete(rows, plan):
527
+ status = replace(
528
+ status,
529
+ status="INCOMPLETE_RESULTS",
530
+ details="evaluation returned incomplete result cardinality",
531
+ expected_records=expected_records,
532
+ actual_records=actual_records,
533
+ )
534
+ else:
535
+ status = replace(
536
+ status,
537
+ expected_records=expected_records,
538
+ actual_records=actual_records,
539
+ )
540
+ if status.succeeded or not _is_retryable_result(status) or retry == transient_retries:
541
+ break
542
+ if not status.succeeded:
543
+ write_diagnostic(_failure_diagnostic(plan, final_run, status), config.artifact_dir)
544
+ raise RuntimeError(f"Evaluation {final_run} ended in {status.status}")
545
+ if not rows or not any(row.get("metric_name") for row in rows):
546
+ raise RuntimeError(f"Evaluation {final_run} returned no scored metric rows")
547
+ annotate_rows(cursor, plan.table_fqn, rows)
548
+ post_completion = _assert_agent_exists_with_default(cursor, plan)
549
+ if pre_start is None:
550
+ raise RuntimeError("Evaluation provenance was not captured before START")
551
+ provenance = {
552
+ "agent_fqn": plan.agent_fqn,
553
+ "plan_identity": plan.plan_identity,
554
+ "evaluated_version": pre_start["default_version"],
555
+ "pre_start": pre_start,
556
+ "post_completion": post_completion,
557
+ "default_version_changed": (
558
+ pre_start["default_version"] != post_completion["default_version"]
559
+ ),
560
+ }
561
+ candidate = build_candidate(
562
+ plan=plan, run_name=final_run, rows=rows, provenance=provenance
563
+ )
564
+ return write_candidate(candidate, config.artifact_dir)
565
+ finally:
566
+ cursor.close()
567
+ conn.close()