modelable 1.0.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.
Potentially problematic release.
This version of modelable might be problematic. Click here for more details.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from modelable.commands.diff import run_diff
|
|
10
|
+
from modelable.compiler.workspace import load_workspace
|
|
11
|
+
from modelable.llm.chat import ChatState, chat_turn
|
|
12
|
+
from modelable.llm.config import resolve_llm_config
|
|
13
|
+
from modelable.llm.context import build_workspace_summary
|
|
14
|
+
from modelable.llm.engine import (
|
|
15
|
+
answer_model_question_cli,
|
|
16
|
+
attach_external_version,
|
|
17
|
+
describe_path_or_ref,
|
|
18
|
+
explain_validation,
|
|
19
|
+
generate_entity_from_prompt,
|
|
20
|
+
import_definition,
|
|
21
|
+
recommend_cli,
|
|
22
|
+
render_attach_audit_summary,
|
|
23
|
+
render_update_audit_summary,
|
|
24
|
+
render_write_audit_summary,
|
|
25
|
+
suggest_projection,
|
|
26
|
+
transform_ref_to_target,
|
|
27
|
+
update_definition,
|
|
28
|
+
validate_generated_text,
|
|
29
|
+
)
|
|
30
|
+
from modelable.llm.provenance import (
|
|
31
|
+
AttachmentRecord,
|
|
32
|
+
build_write_provenance,
|
|
33
|
+
write_attachment_record,
|
|
34
|
+
write_provenance_sidecar,
|
|
35
|
+
)
|
|
36
|
+
from modelable.llm.providers import build_provider
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_llm_commands(cli_group: click.Group) -> None:
|
|
42
|
+
cli_group.add_command(describe)
|
|
43
|
+
cli_group.add_command(generate)
|
|
44
|
+
cli_group.add_command(import_model)
|
|
45
|
+
cli_group.add_command(diff)
|
|
46
|
+
cli_group.add_command(update)
|
|
47
|
+
cli_group.add_command(attach)
|
|
48
|
+
cli_group.add_command(transform)
|
|
49
|
+
cli_group.add_command(suggest_projection_cmd)
|
|
50
|
+
cli_group.add_command(ask)
|
|
51
|
+
cli_group.add_command(recommend)
|
|
52
|
+
cli_group.add_command(explain)
|
|
53
|
+
cli_group.add_command(chat)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@click.command()
|
|
57
|
+
@click.argument("target", required=False)
|
|
58
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), default=None)
|
|
59
|
+
def describe(target: str | None, path: Path | None) -> None:
|
|
60
|
+
"""Describe a model, projection, or workspace."""
|
|
61
|
+
if target is None and path is None:
|
|
62
|
+
raise click.UsageError("provide a model ref or --path")
|
|
63
|
+
console.print(describe_path_or_ref(path=path, ref=target))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _detect_format(path: Path) -> str | None:
|
|
67
|
+
import json
|
|
68
|
+
|
|
69
|
+
import yaml
|
|
70
|
+
|
|
71
|
+
suffix = path.suffix.lower()
|
|
72
|
+
if suffix in {".yml", ".yaml"}:
|
|
73
|
+
try:
|
|
74
|
+
content = path.read_text(encoding="utf-8")
|
|
75
|
+
doc = yaml.safe_load(content)
|
|
76
|
+
if isinstance(doc, dict) and ("models" in doc or "sources" in doc):
|
|
77
|
+
return "dbt"
|
|
78
|
+
if isinstance(doc, dict) and (
|
|
79
|
+
doc.get("kind") == "DataContract"
|
|
80
|
+
or "schema" in doc
|
|
81
|
+
or "schemas" in doc
|
|
82
|
+
or "dataContractSpecification" in doc
|
|
83
|
+
):
|
|
84
|
+
return "odcs"
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
return None
|
|
88
|
+
if suffix == ".json":
|
|
89
|
+
try:
|
|
90
|
+
content = path.read_text(encoding="utf-8")
|
|
91
|
+
doc = json.loads(content)
|
|
92
|
+
if isinstance(doc, dict):
|
|
93
|
+
if doc.get("resourceType") == "StructureDefinition":
|
|
94
|
+
return "fhir"
|
|
95
|
+
if "nodes" in doc:
|
|
96
|
+
return "dbt"
|
|
97
|
+
if "title" in doc and "type" in doc:
|
|
98
|
+
return "json-schema"
|
|
99
|
+
if "openapi" in doc or "swagger" in doc:
|
|
100
|
+
return "openapi"
|
|
101
|
+
if isinstance(doc.get("type"), str) and "fields" in doc:
|
|
102
|
+
return "avro"
|
|
103
|
+
if isinstance(doc.get("type"), dict) and doc["type"].get("type") == "record":
|
|
104
|
+
return "avro"
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
if suffix == ".avsc":
|
|
108
|
+
try:
|
|
109
|
+
content = path.read_text(encoding="utf-8")
|
|
110
|
+
doc = json.loads(content)
|
|
111
|
+
if isinstance(doc, dict) and isinstance(doc.get("type"), str) and "fields" in doc:
|
|
112
|
+
return "avro"
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
if suffix == ".proto":
|
|
116
|
+
try:
|
|
117
|
+
content = path.read_text(encoding="utf-8")
|
|
118
|
+
if "message " in content:
|
|
119
|
+
return "protobuf"
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
if suffix in {".sql", ".ddl"}:
|
|
123
|
+
try:
|
|
124
|
+
content = path.read_text(encoding="utf-8")
|
|
125
|
+
upper = content.upper()
|
|
126
|
+
if "CREATE TABLE" in upper or "CREATE VIEW" in upper:
|
|
127
|
+
return "sql"
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@click.command()
|
|
134
|
+
@click.option("--from", "source", required=True, help="Natural language or source file path.")
|
|
135
|
+
@click.option("--format", "source_format", default=None, help="Source format for import paths.")
|
|
136
|
+
@click.option("--domain", "domain_name", default=None, help="Override the output domain.")
|
|
137
|
+
@click.option("--name", "model_name", default=None, help="Override the output model name.")
|
|
138
|
+
@click.option("--output", "output", type=click.Path(path_type=Path), default=None)
|
|
139
|
+
def generate(
|
|
140
|
+
source: str, source_format: str | None, domain_name: str | None, model_name: str | None, output: Path | None
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Generate a draft entity definition."""
|
|
143
|
+
source_path = Path(source)
|
|
144
|
+
source_descriptor = f"path={source_path}"
|
|
145
|
+
try:
|
|
146
|
+
if source_path.exists():
|
|
147
|
+
detected = source_format or _detect_format(source_path)
|
|
148
|
+
if detected is not None:
|
|
149
|
+
text = import_definition(source_path, detected, domain_name=domain_name, source_name=model_name)
|
|
150
|
+
source_format = detected
|
|
151
|
+
else:
|
|
152
|
+
text = source_path.read_text(encoding="utf-8")
|
|
153
|
+
else:
|
|
154
|
+
if source_format is not None:
|
|
155
|
+
text = import_definition(source, source_format, domain_name=domain_name, source_name=model_name)
|
|
156
|
+
source_descriptor = "inline"
|
|
157
|
+
else:
|
|
158
|
+
text = generate_entity_from_prompt(source, domain_name=domain_name, model_name=model_name)
|
|
159
|
+
source_descriptor = "prompt"
|
|
160
|
+
except ValueError as exc:
|
|
161
|
+
raise click.ClickException(str(exc)) from exc
|
|
162
|
+
except (UnicodeDecodeError, OSError) as exc:
|
|
163
|
+
raise click.ClickException(f"Could not read source: {exc}") from exc
|
|
164
|
+
|
|
165
|
+
_mdl, errors = validate_generated_text(text)
|
|
166
|
+
if errors:
|
|
167
|
+
for error in errors:
|
|
168
|
+
console.print(f"[red]ERROR[/red] {error}")
|
|
169
|
+
raise click.ClickException("generated output failed validation")
|
|
170
|
+
|
|
171
|
+
rendered = text if text.endswith("\n") else text + "\n"
|
|
172
|
+
if output is not None:
|
|
173
|
+
output.write_text(rendered, encoding="utf-8")
|
|
174
|
+
write_provenance_sidecar(
|
|
175
|
+
output,
|
|
176
|
+
build_write_provenance(
|
|
177
|
+
command="generate",
|
|
178
|
+
artifact_path=output,
|
|
179
|
+
provider="local",
|
|
180
|
+
model="modelable-local",
|
|
181
|
+
validation_status="passed",
|
|
182
|
+
diagnostics_repaired=0,
|
|
183
|
+
inputs={
|
|
184
|
+
"source": source_descriptor,
|
|
185
|
+
"format": source_format or "prompt",
|
|
186
|
+
},
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
console.print(f"[green]OK[/green] wrote {output}")
|
|
190
|
+
console.print(
|
|
191
|
+
render_write_audit_summary(
|
|
192
|
+
provider="local",
|
|
193
|
+
model="modelable-local",
|
|
194
|
+
validation_status="passed",
|
|
195
|
+
files_written=str(output),
|
|
196
|
+
inputs=f"{source_descriptor} format={source_format or 'prompt'}",
|
|
197
|
+
diagnostics_repaired=0,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
console.print(rendered.rstrip())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@click.command(name="import")
|
|
205
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
206
|
+
@click.option(
|
|
207
|
+
"--format", "source_format", required=True, help="json-schema, openapi, avro, protobuf, sql, dbt, fhir, odcs"
|
|
208
|
+
)
|
|
209
|
+
@click.option("--domain", "domain_name", default=None)
|
|
210
|
+
@click.option("--output", "output", type=click.Path(path_type=Path), default=None)
|
|
211
|
+
def import_model(source: Path, source_format: str, domain_name: str | None, output: Path | None) -> None:
|
|
212
|
+
"""Import a schema or DDL file into Modelable text."""
|
|
213
|
+
try:
|
|
214
|
+
text = import_definition(source, source_format, domain_name=domain_name)
|
|
215
|
+
except ValueError as exc:
|
|
216
|
+
raise click.ClickException(str(exc)) from exc
|
|
217
|
+
except (UnicodeDecodeError, OSError) as exc:
|
|
218
|
+
raise click.ClickException(f"Could not read source: {exc}") from exc
|
|
219
|
+
_mdl, errors = validate_generated_text(text)
|
|
220
|
+
if errors:
|
|
221
|
+
for error in errors:
|
|
222
|
+
console.print(f"[red]ERROR[/red] {error}")
|
|
223
|
+
raise click.ClickException("imported definition failed validation")
|
|
224
|
+
if output is not None:
|
|
225
|
+
output.write_text(text, encoding="utf-8")
|
|
226
|
+
write_provenance_sidecar(
|
|
227
|
+
output,
|
|
228
|
+
build_write_provenance(
|
|
229
|
+
command="import",
|
|
230
|
+
artifact_path=output,
|
|
231
|
+
provider="local",
|
|
232
|
+
model="modelable-local",
|
|
233
|
+
validation_status="passed",
|
|
234
|
+
diagnostics_repaired=0,
|
|
235
|
+
inputs={
|
|
236
|
+
"path": str(source),
|
|
237
|
+
"format": source_format,
|
|
238
|
+
"domain": domain_name or "",
|
|
239
|
+
},
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
console.print(f"[green]OK[/green] wrote {output}")
|
|
243
|
+
console.print(
|
|
244
|
+
render_write_audit_summary(
|
|
245
|
+
provider="local",
|
|
246
|
+
model="modelable-local",
|
|
247
|
+
validation_status="passed",
|
|
248
|
+
files_written=str(output),
|
|
249
|
+
inputs=f"path={source} format={source_format}",
|
|
250
|
+
diagnostics_repaired=0,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
console.print(text.rstrip())
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@click.command()
|
|
258
|
+
@click.argument("from_ref")
|
|
259
|
+
@click.argument("to_ref")
|
|
260
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
261
|
+
def diff(from_ref: str, to_ref: str, path: Path) -> None:
|
|
262
|
+
"""Compare two published model versions."""
|
|
263
|
+
run_diff(from_ref, to_ref, path)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@click.command()
|
|
267
|
+
@click.argument("ref")
|
|
268
|
+
@click.argument("instruction", nargs=-1)
|
|
269
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
270
|
+
@click.option("--output", "output", type=click.Path(path_type=Path), default=None)
|
|
271
|
+
@click.option("--preview", is_flag=True, help="Show the edit diff without writing changes.")
|
|
272
|
+
@click.option("--provider", "provider", default=None, help="Provider name, for example ollama or anthropic.")
|
|
273
|
+
@click.option("--model", "model", default=None, help="Model identifier.")
|
|
274
|
+
@click.option("--base-url", "base_url", default=None, help="Provider base URL.")
|
|
275
|
+
def update(
|
|
276
|
+
ref: str,
|
|
277
|
+
instruction: tuple[str, ...],
|
|
278
|
+
path: Path,
|
|
279
|
+
output: Path | None,
|
|
280
|
+
preview: bool,
|
|
281
|
+
provider: str | None,
|
|
282
|
+
model: str | None,
|
|
283
|
+
base_url: str | None,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Apply a natural-language update to an existing model version."""
|
|
286
|
+
workspace = load_workspace(path)
|
|
287
|
+
llm_config = resolve_llm_config(
|
|
288
|
+
flag_provider=provider,
|
|
289
|
+
flag_model=model,
|
|
290
|
+
flag_base_url=base_url,
|
|
291
|
+
workspace=workspace.mdl.workspace,
|
|
292
|
+
)
|
|
293
|
+
llm_provider = build_provider(llm_config.provider, model=llm_config.model, base_url=llm_config.base_url)
|
|
294
|
+
try:
|
|
295
|
+
result = update_definition(
|
|
296
|
+
path,
|
|
297
|
+
ref,
|
|
298
|
+
" ".join(instruction),
|
|
299
|
+
output=output,
|
|
300
|
+
write=not preview,
|
|
301
|
+
provider=llm_provider,
|
|
302
|
+
llm_config=llm_config,
|
|
303
|
+
)
|
|
304
|
+
except ValueError as exc:
|
|
305
|
+
raise click.ClickException(str(exc)) from exc
|
|
306
|
+
if preview:
|
|
307
|
+
from modelable.llm.chat import _render_update_preview
|
|
308
|
+
|
|
309
|
+
console.print(_render_update_preview(result))
|
|
310
|
+
else:
|
|
311
|
+
write_provenance_sidecar(
|
|
312
|
+
result.path,
|
|
313
|
+
build_write_provenance(
|
|
314
|
+
command="update",
|
|
315
|
+
artifact_path=result.path,
|
|
316
|
+
provider=result.provider,
|
|
317
|
+
model=result.model,
|
|
318
|
+
validation_status="passed",
|
|
319
|
+
diagnostics_repaired=result.diagnostics_repaired,
|
|
320
|
+
inputs={
|
|
321
|
+
"ref": result.ref,
|
|
322
|
+
"source_path": str(result.source_path),
|
|
323
|
+
},
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
console.print(result.content.rstrip())
|
|
327
|
+
for warning in result.warnings:
|
|
328
|
+
console.print(f"[yellow]WARN[/yellow] {warning}")
|
|
329
|
+
if not preview:
|
|
330
|
+
console.print(render_update_audit_summary(result))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@click.command()
|
|
334
|
+
@click.argument("ref")
|
|
335
|
+
@click.option(
|
|
336
|
+
"--source",
|
|
337
|
+
"source",
|
|
338
|
+
type=click.Path(exists=True, path_type=Path),
|
|
339
|
+
required=True,
|
|
340
|
+
help="External dbt schema.yml or FHIR StructureDefinition JSON file.",
|
|
341
|
+
)
|
|
342
|
+
@click.option("--source-format", "source_format", type=click.Choice(["dbt", "fhir", "odcs"]), required=True)
|
|
343
|
+
@click.option(
|
|
344
|
+
"--source-name",
|
|
345
|
+
"source_name",
|
|
346
|
+
default=None,
|
|
347
|
+
help="dbt model name or FHIR resource name to match, if the source defines multiple.",
|
|
348
|
+
)
|
|
349
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
350
|
+
@click.option("--output", "output", type=click.Path(path_type=Path), default=None)
|
|
351
|
+
@click.option("--preview", is_flag=True, help="Show the new version diff without writing changes.")
|
|
352
|
+
def attach(
|
|
353
|
+
ref: str,
|
|
354
|
+
source: Path,
|
|
355
|
+
source_format: str,
|
|
356
|
+
source_name: str | None,
|
|
357
|
+
path: Path,
|
|
358
|
+
output: Path | None,
|
|
359
|
+
preview: bool,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Attach a model version to an external dbt or FHIR source and record drift as a new version."""
|
|
362
|
+
try:
|
|
363
|
+
result = attach_external_version(
|
|
364
|
+
path, ref, source, source_format, source_name=source_name, output=output, write=not preview
|
|
365
|
+
)
|
|
366
|
+
except ValueError as exc:
|
|
367
|
+
raise click.ClickException(str(exc)) from exc
|
|
368
|
+
|
|
369
|
+
for warning in result.warnings:
|
|
370
|
+
console.print(f"[yellow]WARN[/yellow] {warning}")
|
|
371
|
+
|
|
372
|
+
if not result.attached:
|
|
373
|
+
console.print(
|
|
374
|
+
f"[green]OK[/green] {ref} already matches {result.source_descriptor} "
|
|
375
|
+
f"({result.source_format}); no new version created"
|
|
376
|
+
)
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
if preview:
|
|
380
|
+
from modelable.llm.chat import _render_update_preview
|
|
381
|
+
|
|
382
|
+
console.print(_render_update_preview(result))
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
write_attachment_record(
|
|
386
|
+
result.path,
|
|
387
|
+
AttachmentRecord(
|
|
388
|
+
ref=result.ref,
|
|
389
|
+
source_format=result.source_format,
|
|
390
|
+
source_name=result.source_name,
|
|
391
|
+
source_path=result.source_descriptor,
|
|
392
|
+
source_hash=result.source_hash,
|
|
393
|
+
from_version=result.from_version,
|
|
394
|
+
to_version=result.to_version,
|
|
395
|
+
change_kind=result.change_kind,
|
|
396
|
+
changes=[asdict(change) for change in result.changes],
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
console.print(result.content.rstrip())
|
|
400
|
+
console.print(
|
|
401
|
+
f"[green]OK[/green] attached {ref} to {result.source_descriptor} "
|
|
402
|
+
f"({result.source_format}); new version {result.to_version} ({result.change_kind})"
|
|
403
|
+
)
|
|
404
|
+
console.print(render_attach_audit_summary(result))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@click.command()
|
|
408
|
+
@click.argument("ref")
|
|
409
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
410
|
+
@click.option(
|
|
411
|
+
"--to",
|
|
412
|
+
"target",
|
|
413
|
+
type=click.Choice(["json-schema", "markdown", "typescript", "csharp", "java", "python", "rust", "go", "dbt-yaml"]),
|
|
414
|
+
required=True,
|
|
415
|
+
)
|
|
416
|
+
@click.option("--out", "output", type=click.Path(path_type=Path), default=None)
|
|
417
|
+
@click.option("--explain", is_flag=True, help="Show a mapping explanation alongside the emitted artifact.")
|
|
418
|
+
def transform(ref: str, path: Path, target: str, output: Path | None, explain: bool) -> None:
|
|
419
|
+
"""Transform a model or projection into another artifact format."""
|
|
420
|
+
result = transform_ref_to_target(path, ref, target)
|
|
421
|
+
if output is not None:
|
|
422
|
+
output.write_text(result.content, encoding="utf-8")
|
|
423
|
+
write_provenance_sidecar(
|
|
424
|
+
output,
|
|
425
|
+
build_write_provenance(
|
|
426
|
+
command="transform",
|
|
427
|
+
artifact_path=output,
|
|
428
|
+
provider="local",
|
|
429
|
+
model="modelable-local",
|
|
430
|
+
validation_status="passed",
|
|
431
|
+
diagnostics_repaired=0,
|
|
432
|
+
inputs={
|
|
433
|
+
"ref": ref,
|
|
434
|
+
"target": target,
|
|
435
|
+
"path": str(path),
|
|
436
|
+
},
|
|
437
|
+
),
|
|
438
|
+
)
|
|
439
|
+
console.print(f"[green]OK[/green] wrote {output}")
|
|
440
|
+
console.print(
|
|
441
|
+
render_write_audit_summary(
|
|
442
|
+
provider="local",
|
|
443
|
+
model="modelable-local",
|
|
444
|
+
validation_status="passed",
|
|
445
|
+
files_written=str(output),
|
|
446
|
+
inputs=f"ref={ref} target={target} path={path}",
|
|
447
|
+
diagnostics_repaired=0,
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
if explain and result.explanation is not None:
|
|
451
|
+
console.print(result.explanation)
|
|
452
|
+
console.print(result.content)
|
|
453
|
+
for warning in result.warnings:
|
|
454
|
+
console.print(f"[yellow]WARN[/yellow] {warning}")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@click.command(name="suggest-projection")
|
|
458
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
459
|
+
@click.option("--source", "source_ref", required=True)
|
|
460
|
+
@click.option("--consumer", "consumer_domain", required=True)
|
|
461
|
+
@click.option("--output", "output", type=click.Path(path_type=Path), default=None)
|
|
462
|
+
def suggest_projection_cmd(path: Path, source_ref: str, consumer_domain: str, output: Path | None) -> None:
|
|
463
|
+
"""Suggest a projection for a consuming domain."""
|
|
464
|
+
text = suggest_projection(path, source_ref, consumer_domain)
|
|
465
|
+
_mdl, errors = validate_generated_text(text)
|
|
466
|
+
if errors:
|
|
467
|
+
for error in errors:
|
|
468
|
+
console.print(f"[red]ERROR[/red] {error}")
|
|
469
|
+
raise click.ClickException("suggested projection failed validation")
|
|
470
|
+
if output is not None:
|
|
471
|
+
output.write_text(text, encoding="utf-8")
|
|
472
|
+
write_provenance_sidecar(
|
|
473
|
+
output,
|
|
474
|
+
build_write_provenance(
|
|
475
|
+
command="suggest-projection",
|
|
476
|
+
artifact_path=output,
|
|
477
|
+
provider="local",
|
|
478
|
+
model="modelable-local",
|
|
479
|
+
validation_status="passed",
|
|
480
|
+
diagnostics_repaired=0,
|
|
481
|
+
inputs={
|
|
482
|
+
"path": str(path),
|
|
483
|
+
"source": source_ref,
|
|
484
|
+
"consumer": consumer_domain,
|
|
485
|
+
},
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
console.print(f"[green]OK[/green] wrote {output}")
|
|
489
|
+
console.print(
|
|
490
|
+
render_write_audit_summary(
|
|
491
|
+
provider="local",
|
|
492
|
+
model="modelable-local",
|
|
493
|
+
validation_status="passed",
|
|
494
|
+
files_written=str(output),
|
|
495
|
+
inputs=f"path={path} source={source_ref} consumer={consumer_domain}",
|
|
496
|
+
diagnostics_repaired=0,
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
console.print(text.rstrip())
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@click.command()
|
|
504
|
+
@click.argument("question")
|
|
505
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
506
|
+
def ask(question: str, path: Path) -> None:
|
|
507
|
+
"""Answer a question about the current model workspace."""
|
|
508
|
+
console.print(answer_model_question_cli(path, question))
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@click.command()
|
|
512
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
513
|
+
@click.option("--ref", "ref", default=None)
|
|
514
|
+
@click.option("--consumer", "consumer", default=None)
|
|
515
|
+
def recommend(path: Path, ref: str | None, consumer: str | None) -> None:
|
|
516
|
+
"""Provide a recommendation for a model or projection."""
|
|
517
|
+
console.print(recommend_cli(path, ref=ref, consumer=consumer))
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@click.command()
|
|
521
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
522
|
+
def explain(path: Path) -> None:
|
|
523
|
+
"""Explain current validation errors."""
|
|
524
|
+
console.print(explain_validation(path))
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@click.command()
|
|
528
|
+
@click.option("--path", "path", type=click.Path(exists=True, path_type=Path), required=True)
|
|
529
|
+
@click.option("--ref", "ref", default=None, help="Optional model or projection ref to focus the chat.")
|
|
530
|
+
@click.option("--message", "message", default=None, help="Send a single message and exit.")
|
|
531
|
+
@click.option("--provider", "provider", default=None, help="Provider name, for example ollama or anthropic.")
|
|
532
|
+
@click.option("--model", "model", default=None, help="Model identifier.")
|
|
533
|
+
@click.option("--base-url", "base_url", default=None, help="Provider base URL.")
|
|
534
|
+
def chat(
|
|
535
|
+
path: Path, ref: str | None, message: str | None, provider: str | None, model: str | None, base_url: str | None
|
|
536
|
+
) -> None:
|
|
537
|
+
"""Chat with a model about the current workspace."""
|
|
538
|
+
workspace = load_workspace(path)
|
|
539
|
+
workspace_summary = build_workspace_summary(workspace)
|
|
540
|
+
config = resolve_llm_config(
|
|
541
|
+
flag_provider=provider,
|
|
542
|
+
flag_model=model,
|
|
543
|
+
flag_base_url=base_url,
|
|
544
|
+
workspace=workspace.mdl.workspace,
|
|
545
|
+
)
|
|
546
|
+
llm_provider = build_provider(config.provider, model=config.model, base_url=config.base_url)
|
|
547
|
+
state = ChatState(ref=ref, workspace_summary=workspace_summary)
|
|
548
|
+
|
|
549
|
+
if message is not None:
|
|
550
|
+
console.print(chat_turn(workspace, message, path=path, state=state, provider=llm_provider))
|
|
551
|
+
return
|
|
552
|
+
|
|
553
|
+
console.print("Modelable chat. Type /exit to quit.")
|
|
554
|
+
while True:
|
|
555
|
+
try:
|
|
556
|
+
user_message = click.prompt("you", prompt_suffix="> ", default="", show_default=False)
|
|
557
|
+
except EOFError, click.Abort:
|
|
558
|
+
break
|
|
559
|
+
if not user_message.strip():
|
|
560
|
+
continue
|
|
561
|
+
response = chat_turn(workspace, user_message, path=path, state=state, provider=llm_provider)
|
|
562
|
+
if response == "/exit":
|
|
563
|
+
break
|
|
564
|
+
console.print(f"assistant> {response}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from modelable.lsp.server import main as run_lsp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_lsp_commands(cli_group: click.Group) -> None:
|
|
9
|
+
cli_group.add_command(lsp)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
def lsp() -> None:
|
|
14
|
+
"""Start the Modelable language server over stdio."""
|
|
15
|
+
run_lsp()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from modelable.commands.common import console
|
|
9
|
+
from modelable.runtime.adapter import get_adapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_runtime_commands(cli_group: click.Group) -> None:
|
|
13
|
+
cli_group.add_command(runtime)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def runtime() -> None:
|
|
18
|
+
"""Manage runtime adapter operations."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime.command()
|
|
23
|
+
@click.option("--adapter", required=True, help="Adapter type (e.g., postgres)")
|
|
24
|
+
@click.option("--config", required=True, type=click.Path(exists=True), help="Path to adapter configuration JSON")
|
|
25
|
+
def bootstrap(adapter: str, config: str) -> None:
|
|
26
|
+
"""Initialize a runtime adapter environment."""
|
|
27
|
+
try:
|
|
28
|
+
adapter_instance = get_adapter(adapter)
|
|
29
|
+
config_path = Path(config)
|
|
30
|
+
with open(config_path) as f:
|
|
31
|
+
config_data = json.load(f)
|
|
32
|
+
|
|
33
|
+
adapter_instance.bootstrap(config_data)
|
|
34
|
+
console.print(f"[green]OK[/green] Bootstrapped {adapter}.")
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
console.print(f"[red]ERROR[/red] {exc}")
|
|
37
|
+
raise click.ClickException(str(exc)) from exc
|