Source code for lookbook.profiles

"""Subject profiles — declarative recipes loaded from YAML.

A profile is just a recipe with a name and a YAML file. The schema mirrors
the in-code `RECIPES` dict in `lookbook/__main__.py`:

    name: person
    description: short description shown in `lookbook list-recipes`
    scorers: [scorer_id, ...]
    embedders: [embedder_id, ...]   # optional
    filters: [filter_id, ...]       # may include [name, kwargs] entries
    selector: selector_id           # may be [name, kwargs]
    diagnose_clusters: 12           # optional
    constraints: { ... }            # default constraints (e.g. quotas)

Profiles ship in `lookbook/profiles/*.yaml`. Users can override or add
profiles by dropping YAML files into `<config_root>/profiles/` (resolved
via `config2py`).
"""

from __future__ import annotations

import os
from collections.abc import Mapping
from typing import Any, Optional

from lookbook._paths import default_config_root, subdir

# In-code path to shipped profiles.
_PROFILES_DIR = os.path.dirname(os.path.abspath(__file__))


def _user_profiles_dir() -> str:
    return subdir(default_config_root(), "profiles")


[docs] def list_profiles() -> list[str]: """All profile names available, shipped + user-defined. User profiles override shipped ones with the same name. """ names: dict[str, str] = {} for d in (_PROFILES_DIR, _user_profiles_dir()): if not os.path.isdir(d): continue for fn in os.listdir(d): if fn.endswith((".yaml", ".yml")): names[fn.rsplit(".", 1)[0]] = os.path.join(d, fn) return sorted(names)
def _profile_path(name: str) -> Optional[str]: """Locate a profile by name. User profiles win over shipped ones.""" for d in (_user_profiles_dir(), _PROFILES_DIR): for ext in (".yaml", ".yml"): p = os.path.join(d, name + ext) if os.path.isfile(p): return p return None
[docs] def load(name: str) -> dict: """Load a profile by name, returning a recipe-shaped dict. Raises `KeyError` if the profile is unknown. >>> spec = load("person") >>> "scorers" in spec True """ path = _profile_path(name) if path is None: raise KeyError(f"Unknown profile: {name!r}. Known: {list_profiles()}") try: import yaml # type: ignore except ImportError as e: raise ImportError( "Loading lookbook profiles requires `pyyaml`. " "`pip install pyyaml` or `pip install lookbook[profiles]`." ) from e with open(path, "r", encoding="utf-8") as f: data = yaml.safe_load(f) or {} return _normalize(data)
def _normalize(spec: Mapping[str, Any]) -> dict: """Coerce YAML-friendly forms into the runtime recipe shape. YAML naturally uses lists for `[name, kwargs]` overrides. The runtime recipe shape uses tuples for the same purpose; `lookbook.__main__` handles either. Here we just ensure the keys are present with safe defaults. """ return { "name": spec.get("name", ""), "description": spec.get("description", ""), "scorers": list(spec.get("scorers", [])), "embedders": list(spec.get("embedders", [])), "filters": list(spec.get("filters", [])), "selector": spec.get("selector", "top_k"), "diagnose_clusters": int(spec.get("diagnose_clusters", 0)), "constraints": dict(spec.get("constraints", {}) or {}), }