tenseleyflow/documentlanguagemodel / 2096ea7

Browse files

export/sway_json: cross-repo bridge for dlm export --emit-sway-json (S26 X1-P2)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
2096ea73066750104a65fdd71de48a3cde43929c
Parents
ef62b57
Tree
dbea2a0

1 changed file

StatusFile+-
A src/dlm/export/sway_json.py 134 0
src/dlm/export/sway_json.pyadded
@@ -0,0 +1,134 @@
1
+"""Cross-repo bridge: emit a ready-to-run ``sway.yaml`` next to a dlm export.
2
+
3
+Sprint 26 X1. Closes Audit 03's "users who train via dlm then evaluate
4
+via sway have to run two separate commands" gap. With ``dlm export
5
+--emit-sway-json``, the user runs::
6
+
7
+    dlm export myadapter.dlm --target ollama --emit-sway-json
8
+
9
+and finds both the GGUF/Modelfile *and* a ``sway.yaml`` at
10
+``<export-dir>/sway.yaml`` ready for ``sway run`` against any
11
+HF/MLX/HTTP backend.
12
+
13
+## Why this lives in dlm and not sway
14
+
15
+The autogen logic — translating ``.dlm`` sections to a sway suite spec
16
+— belongs to sway and stays there: importing
17
+``dlm_sway.integrations.dlm.autogen.build_spec_dict``. dlm's job is
18
+just "after a successful export, also run that autogen and write the
19
+result to disk." Keeping the orchestration on the dlm side means
20
+``dlm export`` is the one CLI a user touches; sway is a runtime
21
+collaborator, not a separate phase the user has to remember.
22
+
23
+## Optional-dep posture
24
+
25
+``dlm-sway`` is in dlm's ``[sway]`` optional extra. Without it
26
+installed, the import below raises ``ImportError`` and we surface a
27
+typed :class:`SwayJsonExportError` whose message tells the user
28
+exactly what to install. dlm's ``[sway]`` extra pulls plain
29
+``dlm-sway`` (NOT ``dlm-sway[dlm]``) — sway already optionally
30
+depends on dlm via its own ``[dlm]`` extra, so pulling the round-trip
31
+extra would create a resolver cycle. Plain ``dlm-sway`` is enough:
32
+``build_spec_dict`` lives in ``integrations/dlm/`` but doesn't import
33
+``dlm`` to do its work; it operates on the parsed ``.dlm`` file we
34
+hand it.
35
+"""
36
+
37
+from __future__ import annotations
38
+
39
+import logging
40
+from pathlib import Path
41
+
42
+from dlm.export.errors import ExportError
43
+
44
+logger = logging.getLogger(__name__)
45
+
46
+
47
+class SwayJsonExportError(ExportError):
48
+    """Raised when ``--emit-sway-json`` can't produce a sway.yaml.
49
+
50
+    Two common cases:
51
+
52
+    - ``dlm-sway`` not installed: the user passed ``--emit-sway-json``
53
+      but didn't ``pip install 'dlm[sway]'`` (or installed dlm without
54
+      the sway extra). The error message tells them exactly what to
55
+      install.
56
+    - ``build_spec_dict`` raised: typically a parse failure on the
57
+      ``.dlm`` itself, surfaced through ``SwayError``. We re-wrap so
58
+      the dlm CLI's exception handler sees a familiar ``ExportError``
59
+      subclass and exits cleanly.
60
+    """
61
+
62
+
63
+def write_sway_json(dlm_path: Path, export_dir: Path) -> Path:
64
+    """Generate ``<export_dir>/sway.yaml`` from ``dlm_path``.
65
+
66
+    Parameters
67
+    ----------
68
+    dlm_path:
69
+        The source ``.dlm`` document being exported. Used as the
70
+        autogen input and recorded as ``dlm_source`` in the emitted
71
+        spec for downstream traceability.
72
+    export_dir:
73
+        Directory the GGUF artifacts already wrote into. The new
74
+        ``sway.yaml`` lands at ``export_dir / "sway.yaml"``.
75
+
76
+    Returns
77
+    -------
78
+    Path
79
+        Absolute path to the written ``sway.yaml``.
80
+
81
+    Raises
82
+    ------
83
+    SwayJsonExportError
84
+        ``dlm-sway`` extra not installed, or the autogen call failed.
85
+    """
86
+    dlm_path = Path(dlm_path).expanduser().resolve()
87
+    export_dir = Path(export_dir).expanduser().resolve()
88
+
89
+    try:
90
+        # Both imports are required: ``resolve_dlm`` parses the .dlm
91
+        # into the DlmHandle that ``build_spec_dict`` consumes.
92
+        # ``# type: ignore[import-not-found]`` because dlm-sway is in
93
+        # the optional ``[sway]`` extra; mypy strict shouldn't fail
94
+        # type-checking on a deliberately-optional import.
95
+        from dlm_sway.integrations.dlm.autogen import (  # type: ignore[import-not-found]
96
+            build_spec_dict,
97
+        )
98
+        from dlm_sway.integrations.dlm.resolver import (  # type: ignore[import-not-found]
99
+            resolve_dlm,
100
+        )
101
+    except ImportError as exc:  # pragma: no cover — env-dep branch
102
+        raise SwayJsonExportError(
103
+            "--emit-sway-json requires the dlm-sway integration. Install "
104
+            "with: pip install 'dlm[sway]' (or `pip install dlm-sway` if "
105
+            "dlm is editable). dlm-sway must be on PyPI as `dlm-sway>=0.1.0`."
106
+        ) from exc
107
+
108
+    try:
109
+        import yaml
110
+    except ImportError as exc:  # pragma: no cover — pyyaml is in dlm core
111
+        raise SwayJsonExportError(
112
+            "PyYAML missing; this should never happen — it's a core dlm dep."
113
+        ) from exc
114
+
115
+    try:
116
+        handle = resolve_dlm(dlm_path)
117
+        spec_dict = build_spec_dict(handle, dlm_source=str(dlm_path))
118
+    except Exception as exc:
119
+        # Catch broadly so any sway-side parse / format error surfaces
120
+        # as a typed dlm error the CLI's existing handler can render.
121
+        raise SwayJsonExportError(
122
+            f"sway autogen failed for {dlm_path}: {type(exc).__name__}: {exc}"
123
+        ) from exc
124
+
125
+    out_path = export_dir / "sway.yaml"
126
+    out_path.parent.mkdir(parents=True, exist_ok=True)
127
+
128
+    # ``sort_keys=False`` preserves the dict insertion order
129
+    # ``build_spec_dict`` produced (version → models → defaults →
130
+    # suite). Looks like a hand-authored sway.yaml.
131
+    yaml_text = yaml.safe_dump(spec_dict, sort_keys=False)
132
+    out_path.write_text(yaml_text, encoding="utf-8")
133
+    logger.debug("wrote %s (%d bytes)", out_path, len(yaml_text))
134
+    return out_path