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,128 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ TOOL_METRICS = {"tool_selection_accuracy", "tool_execution_accuracy"}
7
+
8
+
9
+ def metric_names(metrics: list[Any]) -> list[str]:
10
+ names: list[str] = []
11
+ for metric in metrics:
12
+ if isinstance(metric, str):
13
+ name = metric
14
+ elif isinstance(metric, dict) and metric.get("name"):
15
+ name = str(metric["name"])
16
+ ranges = metric.get("score_ranges")
17
+ if ranges is not None and set(ranges) != {"min_score", "median_score", "max_score"}:
18
+ raise ValueError(f"Custom metric {name!r} must define all three score_ranges")
19
+ if not str(metric.get("prompt") or "").strip():
20
+ raise ValueError(f"Custom metric {name!r} requires a prompt")
21
+ else:
22
+ raise ValueError(f"Invalid metric declaration: {metric!r}")
23
+ if not name.strip():
24
+ raise ValueError("Metric names must not be empty")
25
+ names.append(name)
26
+ if not names:
27
+ raise ValueError("Evaluation suite must declare at least one metric")
28
+ duplicates = sorted({name for name in names if names.count(name) > 1})
29
+ if duplicates:
30
+ raise ValueError(f"Evaluation metric names must be unique: {', '.join(duplicates)}")
31
+ return names
32
+
33
+
34
+ def validate_eval_meta(meta: dict[str, Any]) -> tuple[list[str], dict[str, float], dict[str, float]]:
35
+ names = metric_names(list(meta.get("metrics") or []))
36
+ thresholds = _numeric_map(meta.get("thresholds"), "threshold")
37
+ tolerances = _numeric_map(meta.get("regression_tolerances"), "regression tolerance")
38
+ unknown = sorted((set(thresholds) | set(tolerances)) - set(names))
39
+ if unknown:
40
+ raise ValueError(f"Gates reference undeclared metrics: {', '.join(unknown)}")
41
+ return names, thresholds, tolerances
42
+
43
+
44
+ def _numeric_map(raw: Any, label: str) -> dict[str, float]:
45
+ values: dict[str, float] = {}
46
+ for name, value in (raw or {}).items():
47
+ try:
48
+ number = float(value)
49
+ except (TypeError, ValueError) as exc:
50
+ raise ValueError(f"Invalid {label} for {name!r}: {value!r}") from exc
51
+ if number < 0:
52
+ raise ValueError(f"Invalid {label} for {name!r}: must be non-negative")
53
+ values[str(name)] = number
54
+ return values
55
+
56
+
57
+ def validate_table(
58
+ cursor, table_fqn: str, metrics: list[str], ordered_ground_truth_refs: list[str]
59
+ ) -> int:
60
+ cursor.execute(f"DESCRIBE TABLE {table_fqn}")
61
+ columns = {str(row[0]).upper() for row in cursor.fetchall()}
62
+ missing = {"INPUT_QUERY", "OUTPUT"} - columns
63
+ if missing:
64
+ raise ValueError(f"Eval table {table_fqn} is missing columns: {', '.join(sorted(missing))}")
65
+ cursor.execute(f"SELECT COUNT(*) FROM {table_fqn}")
66
+ count = int(cursor.fetchone()[0])
67
+ if count == 0:
68
+ raise ValueError(f"Eval table {table_fqn} is empty")
69
+ cursor.execute(
70
+ f"SELECT input_query, output:custom_criteria:ground_truth_ref::STRING "
71
+ f"FROM {table_fqn}"
72
+ )
73
+ identity_rows = [(str(row[0]), None if row[1] is None else str(row[1])) for row in cursor.fetchall()]
74
+ inputs = [row[0] for row in identity_rows]
75
+ refs = [row[1] for row in identity_rows]
76
+ duplicate_inputs = sorted({value for value in inputs if inputs.count(value) > 1})
77
+ duplicate_refs = sorted({value for value in refs if value is not None and refs.count(value) > 1})
78
+ if duplicate_inputs:
79
+ raise ValueError(f"Eval table {table_fqn} has duplicate INPUT_QUERY values")
80
+ if any(ref is None or not ref for ref in refs):
81
+ raise ValueError(f"Eval table {table_fqn} has missing ground_truth_ref values")
82
+ if duplicate_refs:
83
+ raise ValueError(f"Eval table {table_fqn} has duplicate ground_truth_ref values")
84
+ if set(refs) != set(ordered_ground_truth_refs) or len(refs) != len(ordered_ground_truth_refs):
85
+ raise ValueError(
86
+ f"Eval table {table_fqn} ground_truth_ref content does not match the dbt execution plan"
87
+ )
88
+ if "answer_correctness" in metrics:
89
+ cursor.execute(
90
+ f"SELECT COUNT(*) FROM {table_fqn} "
91
+ "WHERE output:ground_truth_output IS NULL "
92
+ "OR TRIM(output:ground_truth_output::STRING) = ''"
93
+ )
94
+ missing_output = int(cursor.fetchone()[0])
95
+ if missing_output:
96
+ raise ValueError(f"Eval table {table_fqn} has {missing_output} row(s) missing ground_truth_output")
97
+ if TOOL_METRICS & set(metrics):
98
+ cursor.execute(
99
+ f"SELECT COUNT(*) FROM {table_fqn} "
100
+ "WHERE (output:ground_truth_invocations IS NULL "
101
+ "OR ARRAY_SIZE(output:ground_truth_invocations) = 0) "
102
+ "AND COALESCE(output:custom_criteria:test_type::STRING, 'in_scope') = 'in_scope'"
103
+ )
104
+ missing_tools = int(cursor.fetchone()[0])
105
+ if missing_tools:
106
+ raise ValueError(
107
+ f"Eval table {table_fqn} has {missing_tools} in-scope row(s) missing ground_truth_invocations"
108
+ )
109
+ return count
110
+
111
+
112
+ def annotate_rows(cursor, table_fqn: str, rows: list[dict[str, Any]]) -> None:
113
+ cursor.execute(
114
+ f"SELECT input_query, "
115
+ "COALESCE(output:custom_criteria:test_type::STRING, 'in_scope'), "
116
+ f"output:custom_criteria:ground_truth_ref::STRING FROM {table_fqn}"
117
+ )
118
+ source_rows = cursor.fetchall()
119
+ mapping = {str(row[0]): (str(row[1]), str(row[2])) for row in source_rows}
120
+ if len(mapping) != len(source_rows):
121
+ raise ValueError(f"Eval table {table_fqn} contains ambiguous duplicate INPUT_QUERY values")
122
+ for row in rows:
123
+ input_query = str(row.get("input"))
124
+ if input_query not in mapping:
125
+ raise ValueError(f"Evaluation result input is absent from {table_fqn}: {input_query!r}")
126
+ test_type, reference = mapping[input_query]
127
+ row["test_type"] = test_type
128
+ row["ground_truth_ref"] = reference
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from .compare import compare_results
7
+ from ..artifacts import contained_path
8
+ from .results import _identity_components, load_result
9
+
10
+
11
+ def baseline_path(baseline_dir: str | Path, candidate: dict[str, Any]) -> Path:
12
+ target, database, schema, agent_object = _identity_components(candidate)
13
+ return contained_path(
14
+ baseline_dir, target, database, schema, agent_object,
15
+ f"{candidate['suite']}.json",
16
+ )
17
+
18
+
19
+ def gate_candidate(
20
+ candidate_path: str | Path,
21
+ *,
22
+ baseline: str | Path | None = None,
23
+ baseline_dir: str | Path | None = None,
24
+ default_tolerance: float = 0.01,
25
+ ) -> dict[str, Any]:
26
+ candidate = load_result(candidate_path, "candidate")
27
+ if baseline is None:
28
+ if baseline_dir is None or not candidate.get("agent") or not candidate.get("suite"):
29
+ raise ValueError("Gate requires --baseline or a baseline directory plus candidate agent/suite")
30
+ baseline = baseline_path(baseline_dir, candidate)
31
+ return compare_results(load_result(baseline, "baseline"), candidate, default_tolerance)