semlayer 0.3.0b1__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 (49) hide show
  1. semlayer/__init__.py +5 -0
  2. semlayer/benchmark.py +115 -0
  3. semlayer/cli.py +246 -0
  4. semlayer/compile.py +352 -0
  5. semlayer/connectors/__init__.py +1 -0
  6. semlayer/connectors/bigquery.py +66 -0
  7. semlayer/connectors/snowflake.py +82 -0
  8. semlayer/context.py +400 -0
  9. semlayer/cq/__init__.py +1 -0
  10. semlayer/cq/answer.py +226 -0
  11. semlayer/cq/generate.py +83 -0
  12. semlayer/describe/__init__.py +2 -0
  13. semlayer/describe/run.py +157 -0
  14. semlayer/drift.py +309 -0
  15. semlayer/enrich/__init__.py +2 -0
  16. semlayer/enrich/run.py +671 -0
  17. semlayer/errors.py +33 -0
  18. semlayer/export/__init__.py +1 -0
  19. semlayer/export/dbt.py +130 -0
  20. semlayer/init_cmd.py +60 -0
  21. semlayer/link/__init__.py +2 -0
  22. semlayer/link/candidates.py +214 -0
  23. semlayer/link/run.py +255 -0
  24. semlayer/link/table_type.py +71 -0
  25. semlayer/link/validate_llm.py +62 -0
  26. semlayer/llm/__init__.py +1 -0
  27. semlayer/llm/provider.py +161 -0
  28. semlayer/mcp_server.py +198 -0
  29. semlayer/ontology/__init__.py +2 -0
  30. semlayer/ontology/base.py +105 -0
  31. semlayer/pipeline.py +85 -0
  32. semlayer/profile/__init__.py +2 -0
  33. semlayer/profile/llm_typing.py +110 -0
  34. semlayer/profile/run.py +192 -0
  35. semlayer/profile/stats.py +155 -0
  36. semlayer/profile/typing_rules.py +239 -0
  37. semlayer/review.py +133 -0
  38. semlayer/scoring.py +108 -0
  39. semlayer/source.py +110 -0
  40. semlayer/spec/cq-suite.schema.json +58 -0
  41. semlayer/spec/semantic-layer.schema.json +550 -0
  42. semlayer/telemetry.py +49 -0
  43. semlayer/validate.py +275 -0
  44. semlayer-0.3.0b1.dist-info/METADATA +80 -0
  45. semlayer-0.3.0b1.dist-info/RECORD +49 -0
  46. semlayer-0.3.0b1.dist-info/WHEEL +5 -0
  47. semlayer-0.3.0b1.dist-info/entry_points.txt +2 -0
  48. semlayer-0.3.0b1.dist-info/licenses/LICENSE +201 -0
  49. semlayer-0.3.0b1.dist-info/top_level.txt +1 -0
semlayer/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """semlayer: open semantic layer format and inference engine."""
2
+
3
+ __version__ = "0.1.0.dev0"
4
+
5
+ from semlayer.validate import ValidationResult, validate_document, validate_file # noqa: F401
semlayer/benchmark.py ADDED
@@ -0,0 +1,115 @@
1
+ """Reproducible benchmark runner (M6): the ablation, hardened for publication.
2
+
3
+ Reproducibility contract (adversarial B5): results are exact under pinned
4
+ engine + prompt versions + models via committed cassettes; cross-model
5
+ variance is REPORTED, never hidden. `run_benchmark` returns structured
6
+ results; report generation renders docs/benchmark.md.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections import defaultdict
12
+
13
+ CONDITIONS = ["schema_only", "semantic", "semantic+ontology"]
14
+
15
+
16
+ def run_fixture(fixture: str, answer_model: str | None = None,
17
+ conditions: list[str] | None = None) -> dict:
18
+ """Run one fixture's CQ suite under each ablation condition and score results."""
19
+ import importlib
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ import duckdb
24
+ import yaml
25
+
26
+ oss = Path(__file__).resolve().parent.parent.parent
27
+ sys.path.insert(0, str(oss / "fixtures"))
28
+
29
+ from semlayer.cq.answer import (
30
+ answer_with_repair,
31
+ ontology_context,
32
+ schema_only_context,
33
+ score_answer,
34
+ semantic_context,
35
+ )
36
+ from semlayer.llm.provider import AnthropicProvider
37
+ from semlayer.ontology import build_base_graph
38
+ from semlayer.pipeline import infer
39
+ from semlayer.source import DuckDBSource
40
+
41
+ mod = importlib.import_module(f"generators.{fixture}")
42
+ con = duckdb.connect(":memory:")
43
+ mod.build(con)
44
+ doc = infer(DuckDBSource(con), llm=AnthropicProvider())
45
+ graph = build_base_graph(doc)
46
+ cq_path = oss / "fixtures" / "cqs" / f"{fixture}.yaml"
47
+ suite = yaml.safe_load(cq_path.read_text())["cq_suite"]["questions"]
48
+
49
+ out: dict = {"fixture": fixture, "n_cqs": len(suite), "conditions": {}}
50
+ for cond in conditions or CONDITIONS:
51
+ llm = AnthropicProvider(model=answer_model) if answer_model else AnthropicProvider()
52
+ passed = 0
53
+ fails: list[dict] = []
54
+ by_cat: dict[str, list[int]] = defaultdict(lambda: [0, 0])
55
+ for cq in suite:
56
+ q = cq["question"]
57
+ ctx = (schema_only_context(doc) if cond == "schema_only"
58
+ else semantic_context(doc, q) if cond == "semantic"
59
+ else ontology_context(doc, graph, q))
60
+ ok, detail = score_answer(con, cq, answer_with_repair(llm, con, ctx, q))
61
+ is_meta_kind = cq["expected_kind"] in ("refusal", "clarification")
62
+ cat = cq.get("trap") or (cq["expected_kind"] if is_meta_kind else cq["complexity"])
63
+ by_cat[cat][0] += ok
64
+ by_cat[cat][1] += 1
65
+ passed += ok
66
+ if not ok:
67
+ fails.append({"id": cq["id"], "category": cat, "detail": detail[:60]})
68
+ out["conditions"][cond] = {
69
+ "pass_rate": round(passed / len(suite), 3),
70
+ "passed": passed,
71
+ "by_category": {k: {"passed": v[0], "total": v[1]} for k, v in sorted(by_cat.items())},
72
+ "fails": fails,
73
+ "spend": llm.spend_summary,
74
+ "model": llm.model,
75
+ }
76
+ con.close()
77
+ return out
78
+
79
+
80
+ def render_report(results: list[dict], engine_version: str, notes: list[str]) -> str:
81
+ """Render the ablation results table + failure breakdown as a markdown report."""
82
+ lines = [
83
+ "# Benchmark: text-to-SQL accuracy — raw schema vs. inferred semantic layer",
84
+ "",
85
+ f"Engine {engine_version}; answers execution-scored against gold CQ suites",
86
+ "(fixed questions, human-verified expected SQL, executed live). Reproducible",
87
+ "under pinned engine+model via committed cassettes; re-run with:",
88
+ "`python -m semlayer.benchmark`.",
89
+ "",
90
+ "| Fixture | CQs | Answerer | schema_only | semantic | semantic+ontology |",
91
+ "|---|---|---|---|---|---|",
92
+ ]
93
+ for r in results:
94
+ conds = r["conditions"]
95
+ model = next(iter(conds.values()))["model"].split("-2")[0]
96
+ row = [r["fixture"], str(r["n_cqs"]), model]
97
+ for c in CONDITIONS:
98
+ row.append(f"{conds[c]['pass_rate']:.2f}" if c in conds else "—")
99
+ lines.append("| " + " | ".join(row) + " |")
100
+ lines.append("")
101
+ lines.append("## Failure breakdown by category (semantic condition)")
102
+ lines.append("")
103
+ lines.append("| Fixture | Category | Passed/Total |")
104
+ lines.append("|---|---|---|")
105
+ for r in results:
106
+ sem = r["conditions"].get("semantic")
107
+ if not sem:
108
+ continue
109
+ for cat, v in sem["by_category"].items():
110
+ lines.append(f"| {r['fixture']} | {cat} | {v['passed']}/{v['total']} |")
111
+ lines.append("")
112
+ lines.append("## Notes")
113
+ for n in notes:
114
+ lines.append(f"- {n}")
115
+ return "\n".join(lines) + "\n"
semlayer/cli.py ADDED
@@ -0,0 +1,246 @@
1
+ """semlayer command-line interface.
2
+
3
+ Structure: one handler per subcommand, dispatched from `main`. The CLI is the
4
+ error boundary (BETA Q1): `SemlayerError` renders as message + actionable
5
+ hint; raw third-party tracebacks never reach users. Exit codes: 0 success,
6
+ 1 domain outcome (invalid doc / drift found), 2 usage or environment error.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from semlayer.errors import SemlayerError
17
+
18
+
19
+ def _build_parser() -> argparse.ArgumentParser:
20
+ parser = argparse.ArgumentParser(prog="semlayer", description="Semantic layer toolkit")
21
+ parser.add_argument("-v", "--verbose", action="store_true",
22
+ help="Stage-by-stage progress logging")
23
+ sub = parser.add_subparsers(dest="command", required=True)
24
+
25
+ p = sub.add_parser("validate", help="Validate semantic layer document(s) against the spec")
26
+ p.add_argument("paths", nargs="+", help="YAML/JSON document paths")
27
+ p.add_argument("-q", "--quiet", action="store_true", help="Only print failures")
28
+
29
+ p = sub.add_parser("init", help="Generate minimal-grant setup script for a warehouse")
30
+ p.add_argument("warehouse", choices=["snowflake", "bigquery"])
31
+ p.add_argument("--database", default="<your_database>")
32
+ p.add_argument("--project", default="<your_project>")
33
+ p.add_argument("--warehouse-name", default="<your_warehouse>")
34
+
35
+ p = sub.add_parser("infer", help="Infer a semantic layer from a warehouse")
36
+ p.add_argument("source", help="duckdb:<path> | snowflake | bigquery")
37
+ p.add_argument("-o", "--out", default="semantic_layer.yaml")
38
+ p.add_argument("--no-llm", action="store_true", help="Deterministic dry-run (no LLM calls)")
39
+ p.add_argument("--no-sample-egress", action="store_true",
40
+ help="Never include cell values in LLM prompts")
41
+ p.add_argument("--context", action="append", metavar="PATH",
42
+ help="Knowledge docs as inference priors: files, dirs, or globs of "
43
+ ".md/.txt/.rst/.html prose or .csv/.tsv data dictionaries "
44
+ "(repeatable). Doc text IS sent to the LLM — don't pass secrets. "
45
+ "Docs never override data: contradictions land in review.")
46
+
47
+ p = sub.add_parser("review", help="Review inferred claims (terminal queue)")
48
+ p.add_argument("doc", help="semantic layer YAML path")
49
+ p.add_argument("--list", action="store_true", help="List items without prompting")
50
+
51
+ p = sub.add_parser("mcp", help="Serve a semantic layer over MCP (stdio)")
52
+ p.add_argument("doc", help="semantic layer YAML path")
53
+
54
+ p = sub.add_parser("drift", help="Detect warehouse drift vs a semantic layer")
55
+ p.add_argument("doc", help="semantic layer YAML path")
56
+ p.add_argument("source", help="duckdb:<path> | snowflake | bigquery")
57
+ p.add_argument("--apply", action="store_true",
58
+ help="Apply orphaning/conflicts to the doc (writes in place)")
59
+ p.add_argument("--cqs", help="CQ suite YAML to regression-check")
60
+ return parser
61
+
62
+
63
+ def _load_doc(path: str) -> dict:
64
+ import yaml
65
+
66
+ try:
67
+ doc = yaml.safe_load(Path(path).read_text())
68
+ except FileNotFoundError as e:
69
+ raise SemlayerError(f"document not found: {path}",
70
+ hint="run `semlayer infer` first, or check the path") from e
71
+ except yaml.YAMLError as e:
72
+ raise SemlayerError(f"document is not valid YAML: {path} ({e})") from e
73
+ if not isinstance(doc, dict) or "semantic_layer" not in doc:
74
+ raise SemlayerError(f"not a semantic layer document: {path}",
75
+ hint="expected a top-level `semantic_layer:` mapping")
76
+ return doc
77
+
78
+
79
+ def _save_doc(path: str, doc: dict) -> None:
80
+ import yaml
81
+
82
+ Path(path).write_text(yaml.safe_dump(doc, sort_keys=False, width=100))
83
+
84
+
85
+ def _cmd_validate(args: argparse.Namespace) -> int:
86
+ from semlayer.validate import validate_file
87
+
88
+ failed = 0
89
+ for p in args.paths:
90
+ result = validate_file(Path(p))
91
+ if result.ok:
92
+ if not args.quiet:
93
+ warn = f" ({len(result.warnings)} warnings)" if result.warnings else ""
94
+ print(f"OK {p}{warn}")
95
+ for w in result.warnings:
96
+ print(f" warn: {w}")
97
+ else:
98
+ failed += 1
99
+ print(f"FAIL {p}")
100
+ for e in result.errors:
101
+ print(f" {e}")
102
+ return 1 if failed else 0
103
+
104
+
105
+ def _cmd_init(args: argparse.Namespace) -> int:
106
+ from semlayer.init_cmd import render
107
+
108
+ print(render(args.warehouse, database=args.database,
109
+ project=args.project, warehouse_name=args.warehouse_name))
110
+ return 0
111
+
112
+
113
+ def _cmd_infer(args: argparse.Namespace) -> int:
114
+ import json
115
+
116
+ from semlayer.pipeline import infer_with_report, open_source_from_uri
117
+ from semlayer.review import collect
118
+ from semlayer.validate import validate_document
119
+
120
+ llm = None
121
+ if not args.no_llm:
122
+ from semlayer.llm.provider import AnthropicProvider
123
+
124
+ llm = AnthropicProvider()
125
+ context = None
126
+ if args.context:
127
+ from semlayer.context import load_context
128
+
129
+ context = load_context(args.context)
130
+ print(f"context: {len(context.chunks)} doc sections, "
131
+ f"{len(context.dictionary)} dictionary rows")
132
+ src = open_source_from_uri(args.source)
133
+ doc, report = infer_with_report(src, llm=llm, no_sample_egress=args.no_sample_egress,
134
+ context=context)
135
+ if context:
136
+ report["context"] = {"chunks": len(context.chunks),
137
+ "dictionary_rows": len(context.dictionary)}
138
+ Path(args.out + ".report.json").write_text(json.dumps(report, indent=1))
139
+ result = validate_document(doc)
140
+ _save_doc(args.out, doc)
141
+ n = len(doc["semantic_layer"]["tables"])
142
+ mode = f"({llm.spend_summary})" if llm else "(deterministic mode)"
143
+ print(f"wrote {args.out}: {n} tables, valid={result.ok} {mode}")
144
+ items = collect(doc)
145
+ if items:
146
+ print(f"{len(items)} items await review: semlayer review {args.out}")
147
+ return 0 if result.ok else 1
148
+
149
+
150
+ def _cmd_review(args: argparse.Namespace) -> int:
151
+ from semlayer.review import apply, collect
152
+
153
+ doc = _load_doc(args.doc)
154
+ items = collect(doc)
155
+ if not items:
156
+ print("review queue empty")
157
+ return 0
158
+ for i, it in enumerate(items, 1):
159
+ loc = f"{it.table}.{it.column}" if it.column else it.table
160
+ print(f"[{i}/{len(items)}] {it.kind}: {loc}\n claim: {it.claim}"
161
+ + (f"\n evidence: {it.evidence}" if it.evidence else ""))
162
+ if args.list:
163
+ continue
164
+ ans = input(" accept/reject/skip [a/r/s]? ").strip().lower()
165
+ if ans in ("a", "accept"):
166
+ apply(doc, it, "accept")
167
+ elif ans in ("r", "reject"):
168
+ apply(doc, it, "reject")
169
+ if not args.list:
170
+ _save_doc(args.doc, doc)
171
+ print(f"saved {args.doc}")
172
+ return 0
173
+
174
+
175
+ def _cmd_drift(args: argparse.Namespace) -> int:
176
+ from semlayer import drift as drift_mod
177
+ from semlayer.pipeline import open_source_from_uri
178
+
179
+ doc = _load_doc(args.doc)
180
+ src = open_source_from_uri(args.source)
181
+ # the modeled tables ARE the structural baseline: drift is defined
182
+ # relative to what the document claims, not a stored snapshot
183
+ old = {t["name"]: {c["name"]: c["sql_type"] for c in t["columns"]}
184
+ for t in doc["semantic_layer"]["tables"]
185
+ if t.get("lifecycle") not in ("orphaned",)}
186
+ events = drift_mod.diff_snapshots(old, drift_mod.snapshot(src))
187
+ events += drift_mod.semantic_drift(src, doc)
188
+ if not events:
189
+ print("no drift detected")
190
+ return 0
191
+ cs = drift_mod.apply_drift(doc, events)
192
+ if args.cqs and args.source.startswith("duckdb:"):
193
+ import duckdb
194
+ import yaml
195
+
196
+ suite = yaml.safe_load(Path(args.cqs).read_text())["cq_suite"]["questions"]
197
+ con = duckdb.connect(args.source.split(":", 1)[1])
198
+ try:
199
+ cs.broken_cqs = drift_mod.cq_regression(con, suite, events, doc)
200
+ finally:
201
+ con.close()
202
+ print(drift_mod.render_changeset(cs))
203
+ if args.apply:
204
+ _save_doc(args.doc, doc)
205
+ print(f"applied to {args.doc}")
206
+ return 1 # nonzero: drift found (CI-hook friendly)
207
+
208
+
209
+ def _cmd_mcp(args: argparse.Namespace) -> int:
210
+ from semlayer.mcp_server import build_server
211
+
212
+ build_server(_load_doc(args.doc)).run()
213
+ return 0
214
+
215
+
216
+ _HANDLERS = {
217
+ "validate": _cmd_validate,
218
+ "init": _cmd_init,
219
+ "infer": _cmd_infer,
220
+ "review": _cmd_review,
221
+ "drift": _cmd_drift,
222
+ "mcp": _cmd_mcp,
223
+ }
224
+
225
+
226
+ def main(argv: list[str] | None = None) -> int:
227
+ """Parse arguments, dispatch, and render taxonomy errors with hints."""
228
+ args = _build_parser().parse_args(argv)
229
+ logging.basicConfig(
230
+ level=logging.DEBUG if args.verbose else logging.WARNING,
231
+ format="%(levelname)s %(name)s: %(message)s",
232
+ )
233
+ from semlayer import telemetry
234
+
235
+ telemetry.record("cli", command=args.command)
236
+ try:
237
+ return _HANDLERS[args.command](args)
238
+ except SemlayerError as e:
239
+ print(f"error: {e}", file=sys.stderr)
240
+ if e.hint:
241
+ print(f"hint: {e.hint}", file=sys.stderr)
242
+ return 2
243
+
244
+
245
+ if __name__ == "__main__":
246
+ sys.exit(main())