@@ -69,50 +69,75 @@ def resolve_dlm(dlm_path: Path) -> DlmHandle: |
| 69 | doc_text = "\n\n".join(s.content for s in sections) | 69 | doc_text = "\n\n".join(s.content for s in sections) |
| 70 | | 70 | |
| 71 | adapter_path = _resolve_adapter_path(fm.dlm_id) | 71 | adapter_path = _resolve_adapter_path(fm.dlm_id) |
| | 72 | + base_hf_id = _resolve_base_model_to_hf_id(fm.base_model) |
| 72 | | 73 | |
| 73 | return DlmHandle( | 74 | return DlmHandle( |
| 74 | dlm_id=fm.dlm_id, | 75 | dlm_id=fm.dlm_id, |
| 75 | - base_model=fm.base_model, | 76 | + base_model=base_hf_id, |
| 76 | adapter_path=adapter_path, | 77 | adapter_path=adapter_path, |
| 77 | sections=sections, | 78 | sections=sections, |
| 78 | doc_text=doc_text, | 79 | doc_text=doc_text, |
| 79 | ) | 80 | ) |
| 80 | | 81 | |
| 81 | | 82 | |
| | 83 | +def _resolve_base_model_to_hf_id(base_model: str) -> str: |
| | 84 | + """Translate dlm's base-model *key* to a HuggingFace repo id. |
| | 85 | + |
| | 86 | + dlm's frontmatter stores registry keys like ``smollm2-135m`` which |
| | 87 | + resolve to ``HuggingFaceTB/SmolLM2-135M-Instruct``. sway's backends |
| | 88 | + call ``AutoModelForCausalLM.from_pretrained`` directly and need the |
| | 89 | + HF id. The ``hf:org/name`` escape hatch passes through unchanged. |
| | 90 | + """ |
| | 91 | + if base_model.startswith("hf:"): |
| | 92 | + return base_model[len("hf:") :] |
| | 93 | + try: |
| | 94 | + from dlm.base_models import resolve as resolve_base |
| | 95 | + except ImportError: |
| | 96 | + return base_model |
| | 97 | + try: |
| | 98 | + spec = resolve_base(base_model) |
| | 99 | + except Exception: # noqa: BLE001 — unknown dlm errors |
| | 100 | + return base_model |
| | 101 | + hf_id = getattr(spec, "hf_id", None) |
| | 102 | + return str(hf_id) if hf_id else base_model |
| | 103 | + |
| | 104 | + |
| 82 | def _resolve_adapter_path(dlm_id: str) -> Path | None: | 105 | def _resolve_adapter_path(dlm_id: str) -> Path | None: |
| 83 | """Locate the current adapter directory for ``dlm_id``. | 106 | """Locate the current adapter directory for ``dlm_id``. |
| 84 | | 107 | |
| 85 | - Uses dlm's ``StorePath`` helper if available, else falls back to | 108 | + Uses dlm's module-level ``for_dlm`` helper if available, else falls |
| 86 | - the canonical ``~/.dlm/store/<dlm_id>/adapter/current.txt`` pointer. | 109 | + back to the canonical ``~/.dlm/store/<dlm_id>/adapter/current.txt`` |
| 87 | - Returns ``None`` if no adapter has been trained yet. | 110 | + pointer. Returns ``None`` if no adapter has been trained yet. |
| 88 | """ | 111 | """ |
| | 112 | + # Primary path: use dlm's own store-path helpers. |
| 89 | try: | 113 | try: |
| 90 | - from dlm.store.paths import StorePath | 114 | + from dlm.store.paths import for_dlm as _for_dlm |
| 91 | - | | |
| 92 | - _store_path_cls: object | None = StorePath | | |
| 93 | except ImportError: | 115 | except ImportError: |
| 94 | - _store_path_cls = None | 116 | + _for_dlm = None |
| 95 | | 117 | |
| 96 | - if _store_path_cls is not None: | 118 | + if _for_dlm is not None: |
| 97 | try: | 119 | try: |
| 98 | - store = _store_path_cls.for_dlm(dlm_id) # type: ignore[attr-defined] | 120 | + store = _for_dlm(dlm_id) |
| 99 | except Exception: # noqa: BLE001 — unknown dlm exception shapes | 121 | except Exception: # noqa: BLE001 — unknown dlm exception shapes |
| 100 | - return None | 122 | + store = None |
| 101 | - try: | 123 | + if store is not None: |
| 102 | - resolved = store.resolve_current_adapter() | 124 | + try: |
| 103 | - except (AttributeError, FileNotFoundError): | 125 | + resolved = store.resolve_current_adapter() |
| 104 | - resolved = None | 126 | + except (AttributeError, FileNotFoundError): |
| 105 | - if resolved is not None and resolved.exists(): | 127 | + resolved = None |
| 106 | - return Path(resolved) | 128 | + if resolved is not None and Path(resolved).exists(): |
| 107 | - | 129 | + return Path(resolved) |
| 108 | - # Manual fallback in case the dlm API evolves. | 130 | + |
| | 131 | + # Manual fallback. The ``current.txt`` pointer is relative to the |
| | 132 | + # **store root**, not to current.txt's parent dir — so go up one level. |
| 109 | import os | 133 | import os |
| 110 | | 134 | |
| 111 | home = Path(os.environ.get("DLM_HOME", "~/.dlm")).expanduser() | 135 | home = Path(os.environ.get("DLM_HOME", "~/.dlm")).expanduser() |
| 112 | - current_file = home / "store" / dlm_id / "adapter" / "current.txt" | 136 | + store_root = home / "store" / dlm_id |
| | 137 | + current_file = store_root / "adapter" / "current.txt" |
| 113 | if current_file.exists(): | 138 | if current_file.exists(): |
| 114 | pointer = current_file.read_text(encoding="utf-8").strip() | 139 | pointer = current_file.read_text(encoding="utf-8").strip() |
| 115 | - candidate = (current_file.parent / pointer).resolve() | 140 | + candidate = (store_root / pointer).resolve() |
| 116 | if candidate.exists(): | 141 | if candidate.exists(): |
| 117 | return candidate | 142 | return candidate |
| 118 | return None | 143 | return None |
@@ -121,12 +146,14 @@ def _resolve_adapter_path(dlm_id: str) -> Path | None: |
| 121 | def _translate_section(dlm_section: object) -> Section: | 146 | def _translate_section(dlm_section: object) -> Section: |
| 122 | """Adapt a ``dlm.doc.sections.Section`` to sway's section type. | 147 | """Adapt a ``dlm.doc.sections.Section`` to sway's section type. |
| 123 | | 148 | |
| 124 | - The shape dlm uses has been stable through the v0.x series but we | 149 | + dlm's Section dataclass uses the attribute name ``type`` (not |
| 125 | - treat field access defensively so a minor dlm refactor can't silently | 150 | + ``kind``) and stores instruction/preference content as raw markdown |
| 126 | - misread section content. | 151 | + — dlm ships dedicated parsers (``parse_instruction_body``, |
| | 152 | + ``parse_preference_body``) that we reuse here so any future dlm |
| | 153 | + syntax additions land in sway for free. |
| 127 | """ | 154 | """ |
| 128 | - kind_raw = getattr(dlm_section, "kind", None) | 155 | + # dlm's current attribute is ``type``; older revisions used ``kind``. |
| 129 | - # dlm uses the attribute name "kind" on its Section dataclass. | 156 | + kind_raw = getattr(dlm_section, "type", getattr(dlm_section, "kind", None)) |
| 130 | kind = _normalize_kind(kind_raw) | 157 | kind = _normalize_kind(kind_raw) |
| 131 | content = str(getattr(dlm_section, "content", "")) | 158 | content = str(getattr(dlm_section, "content", "")) |
| 132 | section_id = str( | 159 | section_id = str( |
@@ -139,9 +166,9 @@ def _translate_section(dlm_section: object) -> Section: |
| 139 | probes: tuple[SectionProbe, ...] = () | 166 | probes: tuple[SectionProbe, ...] = () |
| 140 | preferences: tuple[SectionPreference, ...] = () | 167 | preferences: tuple[SectionPreference, ...] = () |
| 141 | if kind == "instruction": | 168 | if kind == "instruction": |
| 142 | - probes = tuple(_extract_instruction_probes(dlm_section)) | 169 | + probes = tuple(_parse_instruction(content, section_id=section_id)) |
| 143 | elif kind == "preference": | 170 | elif kind == "preference": |
| 144 | - preferences = tuple(_extract_preference_triples(dlm_section)) | 171 | + preferences = tuple(_parse_preference(content, section_id=section_id)) |
| 145 | | 172 | |
| 146 | return Section( | 173 | return Section( |
| 147 | id=section_id, | 174 | id=section_id, |
@@ -168,35 +195,45 @@ def _normalize_kind(raw: object) -> SectionKind: |
| 168 | return "prose" | 195 | return "prose" |
| 169 | | 196 | |
| 170 | | 197 | |
| 171 | -def _extract_instruction_probes(dlm_section: object) -> list[SectionProbe]: | 198 | +def _parse_instruction(content: str, *, section_id: str) -> list[SectionProbe]: |
| 172 | - """Pull (Q, A) pairs out of a dlm INSTRUCTION section. | 199 | + """Pull (Q, A) pairs out of a dlm INSTRUCTION section body. |
| 173 | | 200 | |
| 174 | - dlm's Section carries its parsed Q/A as ``probes`` or ``qa`` depending | 201 | + Delegates to dlm's own ``parse_instruction_body`` so syntax additions |
| 175 | - on version. We read the first non-empty one and build | 202 | + land in sway without code changes here. Falls back to an empty list |
| 176 | - :class:`SectionProbe` records defensively. | 203 | + on parse errors — the probe will fail gracefully. |
| 177 | """ | 204 | """ |
| 178 | - raw_probes = getattr(dlm_section, "probes", None) or getattr(dlm_section, "qa", None) | 205 | + try: |
| 179 | - if not raw_probes: | 206 | + from dlm.data.instruction_parser import parse_instruction_body |
| | 207 | + except ImportError: |
| | 208 | + return [] |
| | 209 | + try: |
| | 210 | + pairs = parse_instruction_body(content, section_id=section_id) |
| | 211 | + except Exception: # noqa: BLE001 — dlm raises InstructionParseError |
| 180 | return [] | 212 | return [] |
| 181 | out: list[SectionProbe] = [] | 213 | out: list[SectionProbe] = [] |
| 182 | - for rp in raw_probes: | 214 | + for p in pairs: |
| 183 | - q = str(getattr(rp, "prompt", getattr(rp, "question", ""))) | 215 | + q = getattr(p, "question", getattr(p, "prompt", "")) |
| 184 | - a = str(getattr(rp, "gold", getattr(rp, "answer", ""))) | 216 | + a = getattr(p, "answer", getattr(p, "gold", "")) |
| 185 | if q and a: | 217 | if q and a: |
| 186 | - out.append(SectionProbe(prompt=q, gold=a)) | 218 | + out.append(SectionProbe(prompt=str(q), gold=str(a))) |
| 187 | return out | 219 | return out |
| 188 | | 220 | |
| 189 | | 221 | |
| 190 | -def _extract_preference_triples(dlm_section: object) -> list[SectionPreference]: | 222 | +def _parse_preference(content: str, *, section_id: str) -> list[SectionPreference]: |
| 191 | - """Pull (prompt, chosen, rejected) triples out of a dlm PREFERENCE section.""" | 223 | + """Pull (prompt, chosen, rejected) triples out of a PREFERENCE body.""" |
| 192 | - raw = getattr(dlm_section, "preferences", None) or getattr(dlm_section, "triples", None) | 224 | + try: |
| 193 | - if not raw: | 225 | + from dlm.data.preference_parser import parse_preference_body |
| | 226 | + except ImportError: |
| | 227 | + return [] |
| | 228 | + try: |
| | 229 | + triples = parse_preference_body(content, section_id=section_id) |
| | 230 | + except Exception: # noqa: BLE001 — dlm raises PreferenceParseError |
| 194 | return [] | 231 | return [] |
| 195 | out: list[SectionPreference] = [] | 232 | out: list[SectionPreference] = [] |
| 196 | - for r in raw: | 233 | + for t in triples: |
| 197 | - p = str(getattr(r, "prompt", "")) | 234 | + p = str(getattr(t, "prompt", "")) |
| 198 | - c = str(getattr(r, "chosen", "")) | 235 | + c = str(getattr(t, "chosen", "")) |
| 199 | - rej = str(getattr(r, "rejected", "")) | 236 | + rej = str(getattr(t, "rejected", "")) |
| 200 | if p and c and rej: | 237 | if p and c and rej: |
| 201 | out.append(SectionPreference(prompt=p, chosen=c, rejected=rej)) | 238 | out.append(SectionPreference(prompt=p, chosen=c, rejected=rej)) |
| 202 | return out | 239 | return out |