| """Precompute the 2D UMAP layout of the tool embeddings for the background constellation. |
| |
| Run once locally (needs the model + HF auth + umap-learn): |
| |
| python build_layout.py |
| |
| Embeds every tool's routing_text with LFM2.5-Embedding-350M (the same text and prompt |
| the live search uses), projects to 2D with UMAP (cosine metric), normalises to [0,1] |
| with a small inset, and writes static/layout.json — a list of |
| {key: "domain|name", name, domain, x, y}. The frontend renders one dot per tool and |
| lights up the ones a query retrieves. The Space itself does not need umap-learn. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import pathlib |
|
|
| import numpy as np |
|
|
| import search as S |
| import toolset as T |
|
|
| OUT = pathlib.Path(__file__).resolve().parent / "static" / "layout.json" |
|
|
|
|
| def _normalize(xy: np.ndarray) -> np.ndarray: |
| lo, hi = xy.min(axis=0), xy.max(axis=0) |
| span = np.where(hi - lo == 0, 1.0, hi - lo) |
| return (xy - lo) / span |
|
|
|
|
| def main() -> None: |
| import umap |
|
|
| tools = T.load_catalog() |
| print(f"embedding {len(tools)} tools with {S.MODEL_ID} ...", flush=True) |
| emb = S._encode([t.routing_text for t in tools], prompt_name="document") |
|
|
| n_neighbors = min(15, max(2, len(tools) - 1)) |
| reducer = umap.UMAP( |
| n_components=2, metric="cosine", n_neighbors=n_neighbors, |
| min_dist=0.12, random_state=42, |
| ) |
| xy = _normalize(np.asarray(reducer.fit_transform(emb), dtype=np.float64)) |
|
|
| pts = [ |
| {"key": f"{t.domain}|{t.name}", "name": t.name, "domain": t.domain, |
| "x": round(float(xy[i, 0]), 4), "y": round(float(xy[i, 1]), 4)} |
| for i, t in enumerate(tools) |
| ] |
| OUT.write_text(json.dumps(pts), encoding="utf-8") |
| print(f"wrote {len(pts)} points to {OUT}", flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|