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,5 @@
1
+ """Python companion for dbt_cortex_agent."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.3"
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+
7
+ ARTIFACT_SCHEMA_VERSION = 2
8
+ _SLUG = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
9
+
10
+
11
+ def artifact_slug(value: object, label: str) -> str:
12
+ text = str(value)
13
+ if text in {".", ".."} or not _SLUG.fullmatch(text):
14
+ raise ValueError(f"{label} must be a safe artifact slug: {value!r}")
15
+ return text
16
+
17
+
18
+ def contained_path(root: str | Path, *components: object) -> Path:
19
+ base = Path(root).resolve()
20
+ target = base.joinpath(*(artifact_slug(value, "artifact path component") for value in components))
21
+ resolved = target.resolve()
22
+ try:
23
+ resolved.relative_to(base)
24
+ except ValueError as exc:
25
+ raise ValueError(f"Artifact path escapes configured root {base}: {resolved}") from exc
26
+ return resolved
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from dataclasses import replace
7
+ from urllib.error import HTTPError, URLError
8
+
9
+ import yaml
10
+
11
+ from . import __version__
12
+ from .commands import agent, bootstrap, eval, manifest, skill
13
+ from .commands.common import shared_parser
14
+ from .config import resolve_config
15
+ from .execution_context import resolve_execution_context
16
+
17
+
18
+ EXIT_SUCCESS = 0
19
+ EXIT_DIAGNOSTIC_FAILURE = 1
20
+ EXIT_CONTROLLED_ERROR = 2
21
+ VALID_HANDLER_EXIT_CODES = {EXIT_SUCCESS, EXIT_DIAGNOSTIC_FAILURE}
22
+
23
+
24
+ def build_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(
26
+ prog="dbt-cortex-agent",
27
+ description="Operate skills and evaluations for dbt-materialized Cortex Agents.",
28
+ epilog=(
29
+ "Examples:\n"
30
+ " dbt-cortex-agent doctor --project-dir . --json\n"
31
+ " dbt-cortex-agent skill plan --agent orders_assistant\n"
32
+ " dbt-cortex-agent eval run --agent orders_assistant --suite core\n\n"
33
+ "Mutation and paid commands are dry-run by default and label the required --apply option."
34
+ ),
35
+ formatter_class=argparse.RawDescriptionHelpFormatter,
36
+ )
37
+ parser.add_argument("--version", action="version", version=__version__)
38
+ subparsers = parser.add_subparsers(dest="command", required=True)
39
+ shared = shared_parser()
40
+ for domain in (bootstrap, manifest, skill, agent, eval):
41
+ domain.register(subparsers, shared)
42
+ return parser
43
+
44
+
45
+ def _is_controlled_error(exc: Exception) -> bool:
46
+ return isinstance(
47
+ exc,
48
+ (FileNotFoundError, OSError, RuntimeError, ValueError, HTTPError, URLError, yaml.YAMLError),
49
+ ) or exc.__class__.__module__.startswith("snowflake.connector")
50
+
51
+
52
+ def _emit_error(exc: Exception, json_output: bool) -> None:
53
+ if json_output:
54
+ print(json.dumps({"error": str(exc), "exit_code": EXIT_CONTROLLED_ERROR}), file=sys.stderr)
55
+ else:
56
+ print(f"error: {exc}", file=sys.stderr)
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ parser = build_parser()
61
+ args = parser.parse_args(argv)
62
+ try:
63
+ config = resolve_config(args)
64
+ if config.connection_explicit and config.connection:
65
+ context = resolve_execution_context(
66
+ connection=config.connection,
67
+ snow_executable=config.snow_executable,
68
+ target=config.target,
69
+ database=config.database if config.database_explicit else None,
70
+ warehouse=config.warehouse if config.warehouse_explicit else None,
71
+ )
72
+ config = replace(
73
+ config,
74
+ database=context.database,
75
+ warehouse=context.warehouse,
76
+ execution_context=context,
77
+ )
78
+ exit_code = int(args.handler(args, config))
79
+ if exit_code not in VALID_HANDLER_EXIT_CODES:
80
+ raise RuntimeError(f"Command handler returned unsupported exit code {exit_code}")
81
+ return exit_code
82
+ except Exception as exc:
83
+ if not _is_controlled_error(exc):
84
+ raise
85
+ _emit_error(exc, bool(getattr(args, "json", False)))
86
+ return EXIT_CONTROLLED_ERROR
87
+
88
+
89
+ if __name__ == "__main__":
90
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Internal CLI command handlers."""
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+
6
+ from ..config import Config
7
+ from ..identifiers import identifier
8
+ from ..invoke import invoke_agent
9
+ from ..manifest import (
10
+ assert_resource_databases_allowed,
11
+ physical_agent_name,
12
+ select_agents,
13
+ )
14
+ from ..skills import assert_apply_safety
15
+ from .common import add_allowlists, emit_json, fresh_manifest, require_explicit_connection
16
+
17
+
18
+ def register(subparsers: argparse._SubParsersAction, shared: argparse.ArgumentParser) -> None:
19
+ parser = subparsers.add_parser(
20
+ "agent",
21
+ help="smoke a dbt-deployed Agent",
22
+ description="Preview or invoke an Agent already deployed by dbt build.",
23
+ epilog=(
24
+ "Examples:\n"
25
+ " dbt-cortex-agent agent smoke --agent orders_assistant "
26
+ "--question 'What was total revenue?' --json"
27
+ ),
28
+ formatter_class=argparse.RawDescriptionHelpFormatter,
29
+ )
30
+ commands = parser.add_subparsers(dest="agent_command", required=True)
31
+ smoke = commands.add_parser(
32
+ "smoke",
33
+ parents=[shared],
34
+ help="preview or invoke one Agent [RUNTIME with --apply]",
35
+ )
36
+ smoke.add_argument("--agent", required=True, help="logical Agent name")
37
+ smoke.add_argument("--question", required=True, help="question sent to the Agent")
38
+ smoke.add_argument("--expect-tool", help="exact tool name required in the response")
39
+ smoke.add_argument("--agent-object", help="physical Agent override")
40
+ smoke.add_argument("--endpoint", help="HTTPS Snowflake Agent endpoint override")
41
+ smoke.add_argument("--apply", action="store_true", help="[RUNTIME] invoke the Agent; default is preview")
42
+ add_allowlists(smoke)
43
+ smoke.set_defaults(handler=handle)
44
+
45
+ def _nonblank(value: str | None, option: str) -> str:
46
+ if value is None or not value.strip():
47
+ raise ValueError(f"{option} must be nonblank")
48
+ return value
49
+
50
+
51
+ def _handle_smoke(args: argparse.Namespace, config: Config, manifest: dict) -> int:
52
+ logical_agent = _nonblank(args.agent, "--agent")
53
+ question = _nonblank(args.question, "--question")
54
+ expected_tool = (
55
+ _nonblank(args.expect_tool, "--expect-tool") if args.expect_tool is not None else None
56
+ )
57
+ selected = select_agents(manifest, [logical_agent])[0]
58
+ if args.agent_object is not None:
59
+ agent_object = identifier(args.agent_object, "Agent object override")
60
+ else:
61
+ agent_object = physical_agent_name(selected, config.target)
62
+ response = None
63
+ passed = None
64
+ if args.apply:
65
+ require_explicit_connection(config, "Agent smoke")
66
+ assert_apply_safety(config, args.allow_target, args.allow_database)
67
+ assert_resource_databases_allowed({selected["database"]}, args.allow_database)
68
+ if config.database and config.database.upper() != selected["database"]:
69
+ raise ValueError(
70
+ f"Configured database {config.database!r} does not match selected Agent database "
71
+ f"{selected['database']!r}"
72
+ )
73
+ if config.schema and config.schema.upper() != selected["schema"]:
74
+ raise ValueError(
75
+ f"Configured schema {config.schema!r} does not match selected Agent schema "
76
+ f"{selected['schema']!r}"
77
+ )
78
+ response = invoke_agent(
79
+ selected["database"],
80
+ selected["schema"],
81
+ agent_object,
82
+ question,
83
+ str(config.connection),
84
+ args.endpoint,
85
+ )
86
+ passed = expected_tool is None or any(
87
+ item.get("name") == expected_tool for item in response.get("tool_uses", [])
88
+ )
89
+ if not passed:
90
+ raise RuntimeError(
91
+ f"Agent smoke failed: expected tool {expected_tool!r} was not selected"
92
+ )
93
+ payload = {
94
+ "command": "agent smoke",
95
+ "applied": bool(args.apply),
96
+ "agent": logical_agent,
97
+ "agent_object": agent_object,
98
+ "question": question,
99
+ "expected_tool": expected_tool,
100
+ "passed": passed,
101
+ "response": response,
102
+ }
103
+ if args.json:
104
+ emit_json(payload)
105
+ elif args.apply:
106
+ print(f"PASS {logical_agent} via {agent_object}")
107
+ print(json.dumps(response, indent=2, sort_keys=True))
108
+ else:
109
+ print(f"[DRY RUN] would smoke {logical_agent} via {agent_object}: {question}")
110
+ return 0
111
+
112
+
113
+ def handle(args: argparse.Namespace, config: Config) -> int:
114
+ manifest = fresh_manifest(config, no_parse=args.no_parse)
115
+ return _handle_smoke(args, config, manifest)
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..config import Config
6
+ from ..doctor import run_doctor
7
+ from ..init import DEFAULT_REVISION, initialize
8
+ from .common import add_allowlists, emit_json, fresh_manifest
9
+
10
+
11
+ def register(subparsers: argparse._SubParsersAction, shared: argparse.ArgumentParser) -> None:
12
+ init_parser = subparsers.add_parser(
13
+ "init",
14
+ parents=[shared],
15
+ help="preview or apply dbt package bootstrap [MUTATION with --apply]",
16
+ description="Preview dbt package bootstrap; writing files requires --apply.",
17
+ epilog="Example: dbt-cortex-agent init --package-source <git-url> --json",
18
+ )
19
+ init_parser.add_argument("--package-source", help="Git URL for a new package declaration")
20
+ init_parser.add_argument("--revision", default=DEFAULT_REVISION, help=f"immutable package revision (default: {DEFAULT_REVISION})")
21
+ init_parser.add_argument("--agent-schema", help="set cortex_agent_schema when absent")
22
+ init_parser.add_argument("--eval-schema", help="set cortex_eval_schema when absent")
23
+ init_parser.add_argument(
24
+ "--starter", choices=["orders"], help="add the deterministic Orders tutorial starter"
25
+ )
26
+ add_allowlists(init_parser)
27
+ init_parser.add_argument("--apply", action="store_true", help="[MUTATION] write bootstrap changes; default is preview")
28
+ init_parser.add_argument("--run-dbt-deps", action="store_true", help="[MUTATION] run dbt deps after --apply")
29
+ init_parser.set_defaults(handler=handle_init)
30
+
31
+ doctor_parser = subparsers.add_parser(
32
+ "doctor",
33
+ parents=[shared],
34
+ help="run non-mutating project diagnostics",
35
+ description="Run local project, executable, manifest, safety, and optional connection diagnostics.",
36
+ epilog="Example: dbt-cortex-agent doctor --project-dir . --json",
37
+ )
38
+ doctor_parser.set_defaults(handler=handle_doctor)
39
+
40
+
41
+ def handle_init(args: argparse.Namespace, config: Config) -> int:
42
+ result = initialize(
43
+ config,
44
+ apply=args.apply,
45
+ run_deps=args.run_dbt_deps,
46
+ package_source=args.package_source,
47
+ revision=args.revision,
48
+ target=args.target,
49
+ allowed_targets=args.allow_target,
50
+ allowed_databases=args.allow_database,
51
+ agent_schema=args.agent_schema,
52
+ eval_schema=args.eval_schema,
53
+ starter=args.starter,
54
+ )
55
+ if args.json:
56
+ emit_json(
57
+ {
58
+ "command": "init",
59
+ "applied": bool(args.apply),
60
+ "changed_files": [str(path) for path in result.changed_files],
61
+ "starter": args.starter,
62
+ "actions": [
63
+ {"path": str(action.path), "action": action.action}
64
+ for action in result.actions
65
+ ],
66
+ "messages": list(result.messages),
67
+ }
68
+ )
69
+ else:
70
+ for message in result.messages:
71
+ print(message)
72
+ for action in result.actions:
73
+ print(f"[{action.action.upper()}] {action.path}")
74
+ return 0
75
+
76
+
77
+ def handle_doctor(args: argparse.Namespace, config: Config) -> int:
78
+ fresh_manifest(config, no_parse=args.no_parse)
79
+ diagnostics = run_doctor(config)
80
+ failed = any(item.status == "FAIL" for item in diagnostics)
81
+ if args.json:
82
+ emit_json(
83
+ {
84
+ "command": "doctor",
85
+ "passed": not failed,
86
+ "diagnostics": [item.__dict__ for item in diagnostics],
87
+ }
88
+ )
89
+ else:
90
+ for item in diagnostics:
91
+ print(f"[{item.status}] {item.name}: {item.detail}")
92
+ return 1 if failed else 0
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from typing import Any
6
+
7
+ from ..config import Config
8
+ from ..dbt_runner import CommandRunner, run_dbt_parse
9
+ from ..manifest import load_manifest
10
+
11
+
12
+ def shared_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser(add_help=False)
14
+ parser.add_argument("--project-dir", help="dbt project directory (default: current directory)")
15
+ parser.add_argument("--manifest", help="manifest path relative to the project directory")
16
+ parser.add_argument("--target", help="explicit dbt target name")
17
+ parser.add_argument("--connection", help="explicit Snowflake connection name for runtime operations")
18
+ parser.add_argument("--database", help="expected Snowflake target database")
19
+ parser.add_argument("--schema", help="Snowflake Agent schema for runtime operations")
20
+ parser.add_argument("--warehouse", help="Snowflake warehouse for paid evaluation")
21
+ parser.add_argument("--artifact-dir", help="local evaluation artifact directory")
22
+ parser.add_argument("--dbt-executable", help="dbt executable (default: dbt)")
23
+ parser.add_argument("--snow-executable", help="Snow CLI executable (default: snow)")
24
+ parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
25
+ parser.add_argument(
26
+ "--no-parse",
27
+ action="store_true",
28
+ help="skip fresh dbt parse only for controlled test fixtures; unsafe for normal use",
29
+ )
30
+ return parser
31
+
32
+
33
+ def add_allowlists(parser: argparse.ArgumentParser) -> None:
34
+ parser.add_argument("--allow-target", action="append", default=[], help="allow mutation on this dbt target; repeatable")
35
+ parser.add_argument("--allow-database", action="append", default=[], help="allow mutation in this database; repeatable")
36
+
37
+
38
+ def fresh_manifest(
39
+ config: Config, *, no_parse: bool, runner: CommandRunner | None = None
40
+ ) -> dict[str, Any]:
41
+ if not no_parse:
42
+ result = run_dbt_parse(
43
+ config.dbt_executable,
44
+ config.project_dir,
45
+ config.target,
46
+ runner or CommandRunner(),
47
+ config.dbt_env,
48
+ )
49
+ if result.returncode != 0:
50
+ raise RuntimeError(
51
+ result.stderr.strip() or result.stdout.strip() or "dbt parse failed"
52
+ )
53
+ return load_manifest(config.manifest)
54
+
55
+
56
+ def require_explicit_connection(config: Config, operation: str) -> None:
57
+ if not config.connection_explicit or not config.connection:
58
+ raise ValueError(f"{operation} requires an explicitly supplied --connection")
59
+
60
+
61
+ def emit_json(value: Any) -> None:
62
+ print(json.dumps(value, indent=2, default=str))
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..config import Config
6
+ from ..eval.baseline import accept_baseline
7
+ from ..eval.compare import compare_results
8
+ from ..eval.gate import gate_candidate
9
+ from ..eval.lifecycle import build_plan, run_evaluation
10
+ from ..eval.results import load_result
11
+ from .common import add_allowlists, emit_json, require_explicit_connection
12
+
13
+
14
+ def register(subparsers: argparse._SubParsersAction, shared: argparse.ArgumentParser) -> None:
15
+ parser = subparsers.add_parser(
16
+ "eval",
17
+ help="run and gate manifest-owned evaluations",
18
+ description="Plan evaluations locally, opt into paid runs, and manage local comparison evidence.",
19
+ epilog=(
20
+ "Examples:\n"
21
+ " dbt-cortex-agent eval run --agent orders_assistant --suite core --json\n"
22
+ " dbt-cortex-agent eval gate candidate.json --json"
23
+ ),
24
+ formatter_class=argparse.RawDescriptionHelpFormatter,
25
+ )
26
+ commands = parser.add_subparsers(dest="eval_command", required=True)
27
+ run = commands.add_parser("run", parents=[shared], help="plan or execute evaluation [PAID with --apply]")
28
+ run.add_argument("--agent", required=True, help="logical Agent name")
29
+ run.add_argument("--suite", required=True, help="evaluation suite name")
30
+ run.add_argument("--run-name", help="explicit evaluation run name")
31
+ run.add_argument("--poll-attempts", type=int, default=60, help="maximum status polls (default: 60)")
32
+ run.add_argument("--poll-interval", type=float, default=30, help="seconds between polls (default: 30)")
33
+ run.add_argument("--transient-retries", type=int, default=1, help="retries for transient failures (default: 1)")
34
+ add_allowlists(run)
35
+ run.add_argument("--apply", action="store_true", help="[PAID] start evaluation; default only renders the plan")
36
+ run.set_defaults(handler=handle)
37
+
38
+ compare = commands.add_parser("compare", parents=[shared], help="compare baseline and candidate artifacts")
39
+ compare.add_argument("baseline")
40
+ compare.add_argument("candidate")
41
+ compare.add_argument("--tolerance", type=float, default=0.01)
42
+ compare.set_defaults(handler=handle)
43
+
44
+ accept = commands.add_parser("accept-baseline", parents=[shared], help="preview or accept a passing baseline [MUTATION with --apply]")
45
+ accept.add_argument("candidate")
46
+ accept.add_argument("--baseline-dir")
47
+ accept.add_argument("--apply", action="store_true", help="[MUTATION] write the baseline; default is preview")
48
+ accept.add_argument("--force", action="store_true", help="overwrite an existing baseline when applying")
49
+ accept.set_defaults(handler=handle)
50
+
51
+ gate = commands.add_parser("gate", parents=[shared], help="gate a candidate against thresholds and baseline")
52
+ gate.add_argument("candidate")
53
+ gate.add_argument("--baseline")
54
+ gate.add_argument("--baseline-dir")
55
+ gate.add_argument("--tolerance", type=float, default=0.01)
56
+ gate.set_defaults(handler=handle)
57
+
58
+
59
+ def handle(args: argparse.Namespace, config: Config) -> int:
60
+ if args.eval_command == "run":
61
+ if args.apply:
62
+ require_explicit_connection(config, "Evaluation apply")
63
+ plan = build_plan(
64
+ config,
65
+ agent_name=args.agent,
66
+ suite_name=args.suite,
67
+ parse=not args.no_parse,
68
+ )
69
+ plan_payload = {
70
+ "agent": plan.agent_name,
71
+ "suite": plan.suite_name,
72
+ "agent_object": plan.agent_fqn,
73
+ "eval_model": plan.eval_model,
74
+ "dataset": plan.table_fqn,
75
+ "stage": plan.stage_fqn,
76
+ "target_role": plan.target_role,
77
+ "metrics": plan.metric_names,
78
+ "plan_identity": plan.plan_identity,
79
+ "suite_signature": plan.suite_signature,
80
+ "paid_apply": bool(args.apply),
81
+ }
82
+ output = run_evaluation(
83
+ config,
84
+ plan,
85
+ apply=args.apply,
86
+ run_name=args.run_name,
87
+ poll_attempts=args.poll_attempts,
88
+ poll_interval=args.poll_interval,
89
+ transient_retries=args.transient_retries,
90
+ allowed_targets=args.allow_target,
91
+ allowed_databases=args.allow_database,
92
+ )
93
+ candidate = load_result(output) if output else None
94
+ payload = {"command": "eval run", "plan": plan_payload, "candidate": str(output) if output else None, "passed": candidate.get("passed") if candidate else None}
95
+ if args.json:
96
+ emit_json(payload)
97
+ else:
98
+ emit_json(plan_payload)
99
+ if output:
100
+ print(f"Candidate: {output}")
101
+ return 0 if candidate is None or candidate.get("passed") else 1
102
+ if args.eval_command == "compare":
103
+ result = compare_results(load_result(args.baseline), load_result(args.candidate), args.tolerance)
104
+ emit_json(result)
105
+ return 0 if result["passed"] else 1
106
+ if args.eval_command == "accept-baseline":
107
+ directory = args.baseline_dir or str(config.artifact_dir / "baselines")
108
+ candidate = load_result(args.candidate)
109
+ if args.force and not args.apply:
110
+ raise ValueError("--force requires --apply")
111
+ target = (
112
+ accept_baseline(candidate, directory, force=args.force)
113
+ if args.apply
114
+ else None
115
+ )
116
+ if args.json:
117
+ emit_json(
118
+ {
119
+ "command": "eval accept-baseline",
120
+ "applied": bool(args.apply),
121
+ "agent": candidate["agent"],
122
+ "suite": candidate["suite"],
123
+ "baseline": str(target) if target else None,
124
+ "baseline_dir": directory,
125
+ }
126
+ )
127
+ elif not args.apply:
128
+ print(
129
+ f"[DRY RUN] would accept baseline for "
130
+ f"{candidate['agent']}/{candidate['suite']} in {directory}"
131
+ )
132
+ else:
133
+ print(f"Baseline: {target}")
134
+ return 0
135
+ directory = args.baseline_dir or str(config.artifact_dir / "baselines")
136
+ result = gate_candidate(
137
+ args.candidate,
138
+ baseline=args.baseline,
139
+ baseline_dir=directory,
140
+ default_tolerance=args.tolerance,
141
+ )
142
+ emit_json(result)
143
+ return 0 if result["passed"] else 1
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..config import Config
6
+ from ..manifest import select_agents
7
+ from .common import emit_json, fresh_manifest
8
+
9
+
10
+ def register(subparsers: argparse._SubParsersAction, shared: argparse.ArgumentParser) -> None:
11
+ parser = subparsers.add_parser(
12
+ "manifest",
13
+ help="validate resolved dbt metadata",
14
+ description="Inspect freshly parsed manifest-owned Agent metadata.",
15
+ epilog="Example: dbt-cortex-agent manifest validate --agent orders_assistant --json",
16
+ )
17
+ commands = parser.add_subparsers(dest="manifest_command", required=True)
18
+ validate = commands.add_parser(
19
+ "validate", parents=[shared], help="validate manifest and selected Agents"
20
+ )
21
+ validate.add_argument("--agent", action="append", dest="agents", help="logical Agent name; repeatable")
22
+ validate.set_defaults(handler=handle_validate)
23
+
24
+
25
+ def handle_validate(args: argparse.Namespace, config: Config) -> int:
26
+ manifest = fresh_manifest(config, no_parse=args.no_parse)
27
+ agents = [
28
+ {
29
+ "name": item["name"],
30
+ "unique_id": item["unique_id"],
31
+ "database": item["database"],
32
+ "schema": item["schema"],
33
+ "object_name": item["object_name"],
34
+ "physical_fqn": item["physical_fqn"],
35
+ }
36
+ for item in select_agents(manifest, args.agents)
37
+ ]
38
+ result = {
39
+ "manifest": str(config.manifest),
40
+ "agents": [item["name"] for item in agents],
41
+ "agent_resources": agents,
42
+ }
43
+ if args.json:
44
+ emit_json(result)
45
+ else:
46
+ print(f"Manifest: {config.manifest}")
47
+ print(f"Agents: {', '.join(item['physical_fqn'] for item in agents) or 'none'}")
48
+ return 0