@@ -69,50 +69,75 @@ def resolve_dlm(dlm_path: Path) -> DlmHandle: |
| 69 | 69 | doc_text = "\n\n".join(s.content for s in sections) |
| 70 | 70 | |
| 71 | 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 | 74 | return DlmHandle( |
| 74 | 75 | dlm_id=fm.dlm_id, |
| 75 | | - base_model=fm.base_model, |
| 76 | + base_model=base_hf_id, |
| 76 | 77 | adapter_path=adapter_path, |
| 77 | 78 | sections=sections, |
| 78 | 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 | 105 | def _resolve_adapter_path(dlm_id: str) -> Path | None: |
| 83 | 106 | """Locate the current adapter directory for ``dlm_id``. |
| 84 | 107 | |
| 85 | | - Uses dlm's ``StorePath`` helper if available, else falls back to |
| 86 | | - the canonical ``~/.dlm/store/<dlm_id>/adapter/current.txt`` pointer. |
| 87 | | - Returns ``None`` if no adapter has been trained yet. |
| 108 | + Uses dlm's module-level ``for_dlm`` helper if available, else falls |
| 109 | + back to the canonical ``~/.dlm/store/<dlm_id>/adapter/current.txt`` |
| 110 | + pointer. Returns ``None`` if no adapter has been trained yet. |
| 88 | 111 | """ |
| 112 | + # Primary path: use dlm's own store-path helpers. |
| 89 | 113 | try: |
| 90 | | - from dlm.store.paths import StorePath |
| 91 | | - |
| 92 | | - _store_path_cls: object | None = StorePath |
| 114 | + from dlm.store.paths import for_dlm as _for_dlm |
| 93 | 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 | 119 | try: |
| 98 | | - store = _store_path_cls.for_dlm(dlm_id) # type: ignore[attr-defined] |
| 120 | + store = _for_dlm(dlm_id) |
| 99 | 121 | except Exception: # noqa: BLE001 — unknown dlm exception shapes |
| 100 | | - return None |
| 101 | | - try: |
| 102 | | - resolved = store.resolve_current_adapter() |
| 103 | | - except (AttributeError, FileNotFoundError): |
| 104 | | - resolved = None |
| 105 | | - if resolved is not None and resolved.exists(): |
| 106 | | - return Path(resolved) |
| 107 | | - |
| 108 | | - # Manual fallback in case the dlm API evolves. |
| 122 | + store = None |
| 123 | + if store is not None: |
| 124 | + try: |
| 125 | + resolved = store.resolve_current_adapter() |
| 126 | + except (AttributeError, FileNotFoundError): |
| 127 | + resolved = None |
| 128 | + if resolved is not None and Path(resolved).exists(): |
| 129 | + return Path(resolved) |
| 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 | 133 | import os |
| 110 | 134 | |
| 111 | 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 | 138 | if current_file.exists(): |
| 114 | 139 | pointer = current_file.read_text(encoding="utf-8").strip() |
| 115 | | - candidate = (current_file.parent / pointer).resolve() |
| 140 | + candidate = (store_root / pointer).resolve() |
| 116 | 141 | if candidate.exists(): |
| 117 | 142 | return candidate |
| 118 | 143 | return None |
@@ -121,12 +146,14 @@ def _resolve_adapter_path(dlm_id: str) -> Path | None: |
| 121 | 146 | def _translate_section(dlm_section: object) -> Section: |
| 122 | 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 |
| 125 | | - treat field access defensively so a minor dlm refactor can't silently |
| 126 | | - misread section content. |
| 149 | + dlm's Section dataclass uses the attribute name ``type`` (not |
| 150 | + ``kind``) and stores instruction/preference content as raw markdown |
| 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) |
| 129 | | - # dlm uses the attribute name "kind" on its Section dataclass. |
| 155 | + # dlm's current attribute is ``type``; older revisions used ``kind``. |
| 156 | + kind_raw = getattr(dlm_section, "type", getattr(dlm_section, "kind", None)) |
| 130 | 157 | kind = _normalize_kind(kind_raw) |
| 131 | 158 | content = str(getattr(dlm_section, "content", "")) |
| 132 | 159 | section_id = str( |
@@ -139,9 +166,9 @@ def _translate_section(dlm_section: object) -> Section: |
| 139 | 166 | probes: tuple[SectionProbe, ...] = () |
| 140 | 167 | preferences: tuple[SectionPreference, ...] = () |
| 141 | 168 | if kind == "instruction": |
| 142 | | - probes = tuple(_extract_instruction_probes(dlm_section)) |
| 169 | + probes = tuple(_parse_instruction(content, section_id=section_id)) |
| 143 | 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 | 173 | return Section( |
| 147 | 174 | id=section_id, |
@@ -168,35 +195,45 @@ def _normalize_kind(raw: object) -> SectionKind: |
| 168 | 195 | return "prose" |
| 169 | 196 | |
| 170 | 197 | |
| 171 | | -def _extract_instruction_probes(dlm_section: object) -> list[SectionProbe]: |
| 172 | | - """Pull (Q, A) pairs out of a dlm INSTRUCTION section. |
| 198 | +def _parse_instruction(content: str, *, section_id: str) -> list[SectionProbe]: |
| 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 |
| 175 | | - on version. We read the first non-empty one and build |
| 176 | | - :class:`SectionProbe` records defensively. |
| 201 | + Delegates to dlm's own ``parse_instruction_body`` so syntax additions |
| 202 | + land in sway without code changes here. Falls back to an empty list |
| 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) |
| 179 | | - if not raw_probes: |
| 205 | + try: |
| 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 | 212 | return [] |
| 181 | 213 | out: list[SectionProbe] = [] |
| 182 | | - for rp in raw_probes: |
| 183 | | - q = str(getattr(rp, "prompt", getattr(rp, "question", ""))) |
| 184 | | - a = str(getattr(rp, "gold", getattr(rp, "answer", ""))) |
| 214 | + for p in pairs: |
| 215 | + q = getattr(p, "question", getattr(p, "prompt", "")) |
| 216 | + a = getattr(p, "answer", getattr(p, "gold", "")) |
| 185 | 217 | if q and a: |
| 186 | | - out.append(SectionProbe(prompt=q, gold=a)) |
| 218 | + out.append(SectionProbe(prompt=str(q), gold=str(a))) |
| 187 | 219 | return out |
| 188 | 220 | |
| 189 | 221 | |
| 190 | | -def _extract_preference_triples(dlm_section: object) -> list[SectionPreference]: |
| 191 | | - """Pull (prompt, chosen, rejected) triples out of a dlm PREFERENCE section.""" |
| 192 | | - raw = getattr(dlm_section, "preferences", None) or getattr(dlm_section, "triples", None) |
| 193 | | - if not raw: |
| 222 | +def _parse_preference(content: str, *, section_id: str) -> list[SectionPreference]: |
| 223 | + """Pull (prompt, chosen, rejected) triples out of a PREFERENCE body.""" |
| 224 | + try: |
| 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 | 231 | return [] |
| 195 | 232 | out: list[SectionPreference] = [] |
| 196 | | - for r in raw: |
| 197 | | - p = str(getattr(r, "prompt", "")) |
| 198 | | - c = str(getattr(r, "chosen", "")) |
| 199 | | - rej = str(getattr(r, "rejected", "")) |
| 233 | + for t in triples: |
| 234 | + p = str(getattr(t, "prompt", "")) |
| 235 | + c = str(getattr(t, "chosen", "")) |
| 236 | + rej = str(getattr(t, "rejected", "")) |
| 200 | 237 | if p and c and rej: |
| 201 | 238 | out.append(SectionPreference(prompt=p, chosen=c, rejected=rej)) |
| 202 | 239 | return out |