Skip to content

Commit 29fee30

Browse files
erwardenaarclaude
andcommitted
Add docs/code_walkthrough.md: demo preparation reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1bf16db commit 29fee30

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

docs/code_walkthrough.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# openllm-selector — Code Walkthrough
2+
3+
Quick reference for the demo. One section per feature: file + function, how it works, how to extend it.
4+
5+
---
6+
7+
## App entry point
8+
9+
**`app/app.py`**
10+
11+
The entire app is ~20 lines. On every rerun Streamlit calls this top to bottom: set page config → render sidebar (returns filter state) → compute filtered list → render scatter + grid side-by-side in two columns → if `st.session_state.selected_model` is set, render the profile card below.
12+
13+
**To extend:** add a new tab or view by wrapping `render_scatter`/`render_grid` in `st.tabs(...)`.
14+
15+
---
16+
17+
## 1. Loading models
18+
19+
**`src/openllm_selector/database.py``load_models()`**
20+
21+
Opens `data/models.json` (41 records), iterates the list, and calls `compute_openness_score()` on each record, injecting the result as an `openness_score` key before returning. The score is never stored in JSON — always derived at load time.
22+
23+
`compute_openness_score()` sums five boolean criteria: `open_weights`, `open_training_data`, `intermediate_checkpoints`, `open_code`, and permissive license (`"Apache"` or `"MIT"` in the `license` string). Result is 0–5.
24+
25+
**To extend:** add a new scoring criterion by adding another `bool(model["some_field"])` term to the sum in `compute_openness_score()`, and add that field to every record in `models.json`.
26+
27+
The app never calls `load_models()` directly — it goes through `app/utils.py → cached_load_models()`, which wraps it in `@st.cache_data` so the JSON is only read once per session.
28+
29+
---
30+
31+
## 2. Filtering models
32+
33+
**`src/openllm_selector/database.py``filter_models()`**
34+
35+
A single function with ~30 keyword-only parameters, all optional. Calls `load_models()` internally, then walks every model through a cascade of `if … continue` guards — one guard per parameter. Only models that pass every active guard are appended to `results`. String comparisons are case-insensitive; multivalue fields like `languages` use `any(...)`.
36+
37+
The app never calls `filter_models()` directly. It goes through `app/utils.py → get_filtered_models()`, which handles three things that `filter_models()` can't do in one call: OR-logic multiselects (family, org, architecture, etc.), exclusion lists, and free-text search — all applied as list comprehensions on top of the `filter_models()` output.
38+
39+
**To extend:** add a new filterable field by adding a parameter to `filter_models()` (with the guard in the loop) and a corresponding widget in `render_sidebar()`. The sidebar returns the value in `filter_args` and `get_filtered_models()` unpacks it automatically.
40+
41+
---
42+
43+
## 3. Ranking by openness
44+
45+
**`src/openllm_selector/database.py``rank_by_openness()`**
46+
47+
`sorted(models, key=lambda m: m["openness_score"], reverse=descending)`. Called at the end of `get_filtered_models()` so the results grid always shows the most open models first by default.
48+
49+
**To extend:** add a secondary sort key (e.g. `training_tokens_b` as a tiebreaker) by changing the lambda: `key=lambda m: (m["openness_score"], m["training_tokens_b"] or 0)`.
50+
51+
---
52+
53+
## 4. Sidebar filters
54+
55+
**`app/components/sidebar.py``render_sidebar()`**
56+
57+
Calls `cached_load_models()` to derive slider bounds dynamically (min/max size, year, tokens; unique context window values for the `select_slider`). Renders five sections inside `st.sidebar`: openness checkboxes, model characteristic multiselects, exclusion filters expander, range sliders, and a reset button.
58+
59+
The reset button works by iterating `_SIDEBAR_KEYS` — a flat list of every widget key — and popping them from `st.session_state`, then calling `st.rerun()`.
60+
61+
Returns three dicts: `filter_args` (unpacked into `filter_models()`), `multiselect_filters` (OR logic, handled in `get_filtered_models()`), and `exclude_filters` (exclusion logic, also in `get_filtered_models()`). Only actively-set filters are included in `filter_args` — unchecked checkboxes are simply absent from the dict.
62+
63+
**To extend:** add a new widget, give it an `sb_` key, add that key to `_SIDEBAR_KEYS`, and include its value in whichever return dict fits (boolean → `filter_args`, categorical → `multiselect_filters` or `exclude_filters`).
64+
65+
---
66+
67+
## 5. Results grid
68+
69+
**`app/components/grid.py``render_grid()`**
70+
71+
Builds a `pd.DataFrame` from the filtered list, keeping only the eight display columns defined in `_GRID_COLUMNS`. Formats `training_tokens_b` as `"N/A"` when `None`. Renders a `st.dataframe` with `selection_mode="single-row"` and `on_select="rerun"`.
72+
73+
When a row is selected, writes the model name to `st.session_state.selected_model` and sets `st.session_state.selection_source = "grid"`. The guarded `else` branch only clears `selected_model` when the grid itself was the selection source — so clicking a scatter bubble (which sets `selection_source = "scatter"`) doesn't get clobbered on the next rerun when the grid has no highlighted row.
74+
75+
**To extend:** add a column by appending its name to `_GRID_COLUMNS` and adding a `st.column_config` entry in `_COLUMN_CONFIG`.
76+
77+
---
78+
79+
## 6. Scatter plot
80+
81+
**`app/components/scatter.py``render_scatter()` and `_build_figure()`**
82+
83+
`render_scatter()` renders two axis selectboxes, builds the figure, handles the click event, then renders the chart.
84+
85+
`_build_figure()` constructs a Plotly Express scatter. Bubble colour encodes `openness_score` (Viridis 1–5). Bubble area encodes `size_b` capped at 100 B so outliers like BLOOM 176B and DeepSeek-R1 don't dwarf everything. Log scale is applied when `context_window`, `training_tokens_b`, or `num_languages` is on an axis — these span multiple orders of magnitude. Release year gets ±0.15 jitter (seeded at 42) to separate overlapping bubbles. A highlight ring is drawn as a separate `go.Scatter` trace for the selected model, since Plotly's built-in selection state resets on every rerun.
86+
87+
**Bubble click** works in two rerun cycles. `on_select="rerun"` triggers an immediate full rerun the moment a bubble is clicked, storing the selection in `st.session_state["scatter"]`. In the *next* rerun, `render_scatter()` reads `st.session_state.get("scatter")` *before* calling `st.plotly_chart()`, extracts the model name from `point["customdata"][0]` (set via `custom_data=["name"]`), writes it to `selected_model`, sets `selection_source = "scatter"`, and calls `st.rerun()` to open the profile card. The guard `name != st.session_state.get("selected_model")` prevents an infinite rerun loop.
88+
89+
**To extend:** add a new axis option by appending to `_AXIS_OPTIONS` and `_AXIS_LABELS`, and adding the field to the `log_x`/`log_y` condition if it spans orders of magnitude.
90+
91+
---
92+
93+
## 7. Model profile card
94+
95+
**`app/components/profile.py``render_profile()`**
96+
97+
Called from `app.py` when `st.session_state.selected_model` is set. Looks up the model from `cached_load_models()` (no disk hit), renders a bordered container with: header (name, org, country, year) + close button; left column (family, architecture, license, languages, paper and HuggingFace links); right column (size, context window, training tokens metrics, instruct/think availability); openness badge row (five ✅/❌ badges); and a "Recent arXiv papers" expander.
98+
99+
The close button calls `_close()`, which clears `selected_model`, pops the `grid`, `scatter`, and `selection_source` keys from session state, and reruns.
100+
101+
**To extend:** add a new field to the card by adding a `st.metric()` or `st.markdown()` line in `render_profile()`. No changes needed elsewhere.
102+
103+
---
104+
105+
## 8. arXiv paper fetch
106+
107+
**`src/openllm_selector/database.py``fetch_recent_papers()`**
108+
109+
Queries the arXiv Atom API at `http://arxiv.org/api/query` with `search_query=all:"<model_name>"`, sorted by submission date descending. Parses the Atom XML with `xml.etree.ElementTree` and returns a list of dicts with `title`, `authors`, `summary`, `published`, and `arxiv_url`.
110+
111+
In the app, all calls go through `app/utils.py → cached_fetch_recent_papers()`, which wraps `fetch_recent_papers()` in `@st.cache_data(ttl=3600)` — results are cached for one hour per model name, so switching between models in a session doesn't hammer the API. A 429 response is caught in `render_profile()` and surfaced as a user-visible warning rather than an uncaught exception.
112+
113+
**To extend:** change `max_results` (default 3) to show more papers, or switch `search_query` from `all:` to `ti:` to restrict to title-only matches (reduces false positives for short or common model names).
114+
115+
---
116+
117+
## File map
118+
119+
```
120+
src/openllm_selector/
121+
database.py load_models, filter_models, rank_by_openness,
122+
fetch_recent_papers, compute_openness_score
123+
data/models.json 41 model records (no openness_score stored here)
124+
125+
app/
126+
app.py entry point — wires sidebar → filter → scatter/grid → profile
127+
utils.py @st.cache_data wrappers + get_filtered_models()
128+
components/
129+
sidebar.py render_sidebar() — all filter widgets
130+
grid.py render_grid() — sortable results table
131+
scatter.py render_scatter(), _build_figure()
132+
profile.py render_profile(), _close()
133+
```

0 commit comments

Comments
 (0)