Spaces:
Sleeping
Sleeping
| """Pure helpers shared across API handlers. | |
| Ported from miru-tracer's ``ui/helpers.py`` and ``ui/lens_common.py`` minus | |
| everything Gradio-specific. The frontend sends internal keys directly | |
| ("completion"/"chat"/"raw", "auto"/"off"/"prefill", "logit"/"jacobian"/"diff", | |
| "adjusted"/"raw", "text"/"id") so no label mapping happens server-side. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from miru_tracer.core.interventions import Intervention | |
| from miru_tracer.core.sampling import SamplingParams | |
| LENS_MODES = ("logit", "jacobian", "diff") | |
| GENERATION_MODES = ("completion", "chat", "raw") | |
| THINKING_MODES = ("auto", "off", "prefill") | |
| def ui_sampling_params(strategy, temperature, top_k, top_p) -> SamplingParams: | |
| """Build SamplingParams from raw widget values (clamped, not raising).""" | |
| return SamplingParams( | |
| strategy=strategy, | |
| temperature=float(temperature), | |
| top_k=int(top_k or 0), | |
| top_p=min(max(float(top_p), 1e-3), 1.0), | |
| ) | |
| class ChatValidationError(ValueError): | |
| """The chat JSON the user entered is not a valid message list.""" | |
| def parse_chat_messages(text: str) -> list[dict[str, str]]: | |
| """Parse and validate the chat-messages JSON from the UI. | |
| Raises: | |
| ChatValidationError: with a user-presentable message. | |
| """ | |
| try: | |
| messages = json.loads(text) | |
| except json.JSONDecodeError as e: | |
| raise ChatValidationError(f"Invalid JSON: {e}") from e | |
| if not isinstance(messages, list) or not messages: | |
| raise ChatValidationError("Chat messages must be a non-empty JSON array") | |
| for message in messages: | |
| if ( | |
| not isinstance(message, dict) | |
| or "role" not in message | |
| or "content" not in message | |
| ): | |
| raise ChatValidationError( | |
| "Each message must have 'role' and 'content' fields" | |
| ) | |
| return messages | |
| def token_ref_to_id(ref: str, tokenizer, mode: str) -> int: | |
| """Resolve a user token reference under an explicit interpretation mode. | |
| ``mode == "id"``: parse a numeric token id; surrounding whitespace is | |
| tolerated and non-numeric input is rejected. ``mode == "text"``: encode | |
| the text verbatim — leading/trailing whitespace is significant (" Paris" | |
| and "Paris" are different BPE tokens) and digits are NEVER treated as an | |
| id — returning the first token id. | |
| Raises: | |
| ValueError: empty ref, unencodable text, non-numeric id, or an | |
| out-of-range id. | |
| """ | |
| if mode == "id": | |
| stripped = ref.strip() | |
| if not stripped: | |
| raise ValueError("Empty token reference") | |
| if not stripped.lstrip("-").isdigit(): | |
| raise ValueError(f"Not a numeric token id: {ref!r}") | |
| token_id = int(stripped) | |
| if not 0 <= token_id < len(tokenizer): | |
| raise ValueError( | |
| f"Token id {token_id} out of range (vocab size {len(tokenizer)})" | |
| ) | |
| return token_id | |
| if not ref.strip(): | |
| raise ValueError("Empty token reference") | |
| encoded = tokenizer.encode(ref, add_special_tokens=False) | |
| if not encoded: | |
| raise ValueError(f"Could not tokenize {ref!r}") | |
| return int(encoded[0]) | |
| def parse_layer_refs(text: str) -> list[int]: | |
| """Comma-separated layers and inclusive ranges -> unique sorted layer list. | |
| E.g. ``"11, 12-15, 18"`` -> ``[11, 12, 13, 14, 15, 18]``. | |
| Raises: | |
| ValueError: empty input, malformed entry, or a descending range. | |
| """ | |
| layers: list[int] = [] | |
| for part in (str(text) if text is not None else "").split(","): | |
| part = part.strip() | |
| if not part: | |
| continue | |
| lo, sep, hi = part.partition("-") | |
| lo, hi = lo.strip(), hi.strip() | |
| if not lo.isdigit() or (sep and not hi.isdigit()): | |
| raise ValueError( | |
| f"Bad layer reference {part!r}: use a number or range like 12-15" | |
| ) | |
| start, end = int(lo), int(hi) if sep else int(lo) | |
| if end < start: | |
| raise ValueError(f"Descending layer range {part!r}") | |
| layers.extend(range(start, end + 1)) | |
| if not layers: | |
| raise ValueError("Empty layer reference") | |
| return sorted(set(layers)) | |
| def layer_selection(n_layers: int, start, end, stride) -> list[int]: | |
| """Resolve UI layer-range inputs into a concrete layer list. | |
| ``end`` is inclusive; -1 (or blank) means the final layer. The final | |
| selected layer is always included even if the stride skips it. | |
| """ | |
| start = int(start) if start is not None else 0 | |
| end = int(end) if end is not None else -1 | |
| stride = max(int(stride) if stride else 1, 1) | |
| if end < 0: | |
| end = n_layers - 1 | |
| start = max(0, min(start, n_layers - 1)) | |
| end = max(start, min(end, n_layers - 1)) | |
| layers = list(range(start, end + 1, stride)) | |
| if layers[-1] != end: | |
| layers.append(end) | |
| return layers | |
| # ------------------------------------------------------------- interventions | |
| def intervention_signature(iv: Intervention) -> tuple: | |
| """Stable key for deciding whether two UI interventions are duplicates.""" | |
| if iv.kind == "steer": | |
| return (iv.kind, iv.layer, iv.token_id, float(iv.strength), iv.basis) | |
| if iv.kind == "swap": | |
| return (iv.kind, iv.layer, iv.token_id, iv.token_id_to, iv.basis) | |
| return (iv.kind, iv.layer, iv.token_id, iv.basis) | |
| def add_unique_intervention_rows(rows: list, candidates: list) -> tuple[list, list, int]: | |
| """Append candidates whose effective intervention parameters are new.""" | |
| updated = list(rows or []) | |
| seen = {intervention_signature(row["intervention"]) for row in updated} | |
| added = [] | |
| skipped = 0 | |
| for row in candidates: | |
| signature = intervention_signature(row["intervention"]) | |
| if signature in seen: | |
| skipped += 1 | |
| continue | |
| updated.append(row) | |
| added.append(row) | |
| seen.add(signature) | |
| return updated, added, skipped | |
| def enabled_interventions(rows: list) -> list[Intervention]: | |
| return [row["intervention"] for row in rows or [] if row.get("enabled", True)] | |
| def describe_with_basis(iv: Intervention, tokenizer=None) -> str: | |
| """Human description of an intervention with its basis appended.""" | |
| return f"{iv.describe(tokenizer)} ({iv.basis})" | |
| def intervened_layer_titles( | |
| interventions: list[Intervention], tokenizer=None | |
| ) -> dict[int, str]: | |
| """Map each edited layer to a ``'; '``-joined description of its edits.""" | |
| titles: dict[int, str] = {} | |
| for iv in interventions: | |
| desc = describe_with_basis(iv, tokenizer) | |
| titles[iv.layer] = f"{titles[iv.layer]}; {desc}" if iv.layer in titles else desc | |
| return titles | |
| def interventions_summary( | |
| interventions: list[Intervention], tokenizer=None, *, limit: int = 4 | |
| ) -> str: | |
| """One-line summary of the active interventions for the status area.""" | |
| parts = [describe_with_basis(iv, tokenizer) for iv in interventions[:limit]] | |
| if len(interventions) > limit: | |
| parts.append(f"+{len(interventions) - limit} more") | |
| return "; ".join(parts) | |
| def intervention_visibility_warning( | |
| interventions: list[Intervention], mode: str, n_layers: int, tokenizer=None | |
| ) -> str | None: | |
| """Warn when an edit's basis differs from the current lens view mode. | |
| A jacobian-basis edit moves the residual along pre-transport directions | |
| (``J_ℓ v = û_t``), visible under the Jacobian lens but nearly invisible | |
| under the Logit lens — and vice versa. The final layer is basis-independent | |
| (both bases use ``û_t`` directly), and Diff renders both readouts, so | |
| neither triggers a warning. For a fixed ``mode`` at most one basis can | |
| mismatch, so this returns a single line (or None). | |
| """ | |
| if mode == "diff": | |
| return None | |
| final = n_layers - 1 | |
| mismatched = [iv for iv in interventions if iv.layer != final and iv.basis != mode] | |
| if not mismatched: | |
| return None | |
| basis = mismatched[0].basis # only one basis can mismatch a given mode | |
| shown = "; ".join(iv.describe(tokenizer) for iv in mismatched[:3]) | |
| if len(mismatched) > 3: | |
| shown += f" (+{len(mismatched) - 3} more)" | |
| verb = "uses" if len(mismatched) == 1 else "use" | |
| pronoun = "its" if len(mismatched) == 1 else "their" | |
| want = "Jacobian" if basis == "jacobian" else "Logit" | |
| return ( | |
| f"⚠ {shown} {verb} {basis} basis — switch Lens to {want} (or Diff) " | |
| f"to see {pronoun} effect in the readouts." | |
| ) | |