Jac-Zac commited on
Commit
12cdb17
·
1 Parent(s): 0ba2e45

Switching to uniform colortheme catpuccin

Browse files
.streamlit/config.toml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Catppuccin Mocha theme. Switch base to "light" and swap the four colors
2
+ # below to the Latte equivalents (see utils/theme.py) for the light flavor.
3
+ [theme]
4
+ base = "dark"
5
+ primaryColor = "#89b4fa" # Mocha blue
6
+ backgroundColor = "#1e1e2e" # base
7
+ secondaryBackgroundColor = "#313244" # surface0
8
+ textColor = "#cdd6f4" # text
9
+ font = "sans serif"
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import os
2
- import threading
3
 
4
  import streamlit as st
5
  from dotenv import load_dotenv
@@ -17,22 +16,6 @@ _TABS = ["Chat", "Compare", "Extract"]
17
  _TAB_ICONS = [":material/chat:", ":material/search:", ":material/tune:"]
18
 
19
 
20
- def _preload_default_model() -> None:
21
- """Background-warm the default local model so the first chat is instant."""
22
- try:
23
- import torch
24
-
25
- torch.set_grad_enabled(False)
26
- from utils.runtime import cached_model
27
-
28
- cached_model(DEFAULT_MODEL)
29
- except Exception:
30
- pass
31
-
32
-
33
- threading.Thread(target=_preload_default_model, daemon=True).start()
34
-
35
-
36
  def _remote_model_input(remote_models: list[str]) -> str:
37
  """Return the active remote model id, picking from running NDIF deployments or a custom value."""
38
 
@@ -142,13 +125,17 @@ def _sidebar_controls() -> tuple[bool, str, str, str]:
142
  def main() -> None:
143
  """Run the Streamlit app."""
144
 
 
 
 
 
 
145
  # Deferred: importing torch is slow; keep it after dotenv load (done at
146
  # module level above) so the Streamlit page config renders immediately.
147
  import torch
148
 
149
  torch.set_grad_enabled(False)
150
 
151
- st.set_page_config(page_title="Persona UI", layout="wide")
152
  remote, model_name, dataset_source, active_tab = _sidebar_controls()
153
 
154
  if active_tab == "Extract":
 
1
  import os
 
2
 
3
  import streamlit as st
4
  from dotenv import load_dotenv
 
16
  _TAB_ICONS = [":material/chat:", ":material/search:", ":material/tune:"]
17
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def _remote_model_input(remote_models: list[str]) -> str:
20
  """Return the active remote model id, picking from running NDIF deployments or a custom value."""
21
 
 
125
  def main() -> None:
126
  """Run the Streamlit app."""
127
 
128
+ st.set_page_config(page_title="Persona UI", layout="wide")
129
+ from utils.theme import install_catppuccin_theme
130
+
131
+ install_catppuccin_theme(st.get_option("theme.base"))
132
+
133
  # Deferred: importing torch is slow; keep it after dotenv load (done at
134
  # module level above) so the Streamlit page config renders immediately.
135
  import torch
136
 
137
  torch.set_grad_enabled(False)
138
 
 
139
  remote, model_name, dataset_source, active_tab = _sidebar_controls()
140
 
141
  if active_tab == "Extract":
pyproject.toml CHANGED
@@ -10,6 +10,7 @@ dependencies = [
10
  "streamlit>=1.44.0",
11
  "plotly>=6.6.0",
12
  "python-dotenv>=1.2.2",
 
13
  ]
14
 
15
  # Local development:
 
10
  "streamlit>=1.44.0",
11
  "plotly>=6.6.0",
12
  "python-dotenv>=1.2.2",
13
+ "catppuccin>=2.5.0",
14
  ]
15
 
16
  # Local development:
state.py CHANGED
@@ -17,7 +17,6 @@ class ChatState(TypedDict):
17
  messages: list[ChatMessage]
18
  persona_id: str | None
19
  prompt_mode: str
20
- past_key_values: object | None
21
 
22
 
23
  def chat_session_key(model_name: str, dataset_source: str) -> str:
@@ -31,7 +30,6 @@ def default_chat_state() -> ChatState:
31
  "messages": [],
32
  "persona_id": None,
33
  "prompt_mode": "templated",
34
- "past_key_values": None,
35
  }
36
 
37
 
@@ -44,18 +42,15 @@ def reset_chat_context_state(
44
  """Reset one chat context and clear any related widget state."""
45
 
46
  state["messages"] = []
47
- state["past_key_values"] = None
48
  state["persona_id"] = persona_id
49
  state["prompt_mode"] = prompt_mode
50
  for key in ui_keys:
51
  st.session_state.pop(key, None)
52
 
53
 
54
- def get_chat_state(model_name: str, remote: bool, dataset_source: str) -> ChatState:
55
  """Return the mutable chat state for the active context."""
56
 
57
  key = chat_session_key(model_name, dataset_source)
58
  state = st.session_state.setdefault(key, default_chat_state())
59
- if remote and state.get("past_key_values") is not None:
60
- state["past_key_values"] = None
61
  return state
 
17
  messages: list[ChatMessage]
18
  persona_id: str | None
19
  prompt_mode: str
 
20
 
21
 
22
  def chat_session_key(model_name: str, dataset_source: str) -> str:
 
30
  "messages": [],
31
  "persona_id": None,
32
  "prompt_mode": "templated",
 
33
  }
34
 
35
 
 
42
  """Reset one chat context and clear any related widget state."""
43
 
44
  state["messages"] = []
 
45
  state["persona_id"] = persona_id
46
  state["prompt_mode"] = prompt_mode
47
  for key in ui_keys:
48
  st.session_state.pop(key, None)
49
 
50
 
51
+ def get_chat_state(model_name: str, _remote: bool, dataset_source: str) -> ChatState:
52
  """Return the mutable chat state for the active context."""
53
 
54
  key = chat_session_key(model_name, dataset_source)
55
  state = st.session_state.setdefault(key, default_chat_state())
 
 
56
  return state
tabs/chat.py CHANGED
@@ -113,7 +113,6 @@ def _handle_single_chat_generation(
113
  model=model,
114
  messages=messages,
115
  remote=remote,
116
- past_key_values=chat_state["past_key_values"],
117
  **generation.to_generate_kwargs(),
118
  )
119
  except Exception as exc:
@@ -125,7 +124,6 @@ def _handle_single_chat_generation(
125
  return
126
 
127
  chat_state["messages"].append({"role": "assistant", "content": reply.text})
128
- chat_state["past_key_values"] = reply.past_key_values if not remote else None
129
  st.rerun()
130
 
131
 
@@ -218,6 +216,13 @@ def render_chat_tab(remote: bool, model_name: str, dataset_source: str) -> None:
218
  prompt_key,
219
  prompt_mode,
220
  active_system_prompt,
 
 
 
 
 
 
 
221
  )
222
 
223
  render_probe_inspector(
@@ -232,7 +237,6 @@ def render_chat_tab(remote: bool, model_name: str, dataset_source: str) -> None:
232
  render_chat_window(
233
  chat_log=chat_log,
234
  messages=chat_state["messages"],
235
- chat_state=chat_state,
236
  edit_key=edit_key,
237
  pending_key=pending_key,
238
  )
 
113
  model=model,
114
  messages=messages,
115
  remote=remote,
 
116
  **generation.to_generate_kwargs(),
117
  )
118
  except Exception as exc:
 
124
  return
125
 
126
  chat_state["messages"].append({"role": "assistant", "content": reply.text})
 
127
  st.rerun()
128
 
129
 
 
216
  prompt_key,
217
  prompt_mode,
218
  active_system_prompt,
219
+ on_save=lambda: reset_chat_context_state(
220
+ chat_state,
221
+ selected_persona.id,
222
+ prompt_mode,
223
+ chat_input_key,
224
+ pending_key,
225
+ ),
226
  )
227
 
228
  render_probe_inspector(
 
237
  render_chat_window(
238
  chat_log=chat_log,
239
  messages=chat_state["messages"],
 
240
  edit_key=edit_key,
241
  pending_key=pending_key,
242
  )
tabs/chat_ui.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from dataclasses import asdict, dataclass
2
  from typing import Any
3
 
@@ -74,7 +75,6 @@ def _open_edit_dialog(
74
  *,
75
  msg_index: int,
76
  messages: list[dict[str, str]],
77
- chat_state: dict[str, object],
78
  pending_key: str,
79
  ) -> None:
80
  message = messages[msg_index]
@@ -102,7 +102,6 @@ def _open_edit_dialog(
102
  if role == "assistant":
103
  messages[msg_index]["_needs_contrast"] = True
104
  del messages[msg_index + 1 :]
105
- chat_state["past_key_values"] = None
106
  if role == "user":
107
  st.session_state[pending_key] = "regenerate_after_edit"
108
  st.rerun()
@@ -112,7 +111,12 @@ def _open_edit_dialog(
112
 
113
 
114
  @st.dialog("Edit system prompt", width="large")
115
- def _open_system_prompt_dialog(*, prompt_key: str, current_value: str) -> None:
 
 
 
 
 
116
  new_value = st.text_area(
117
  "System prompt",
118
  value=current_value,
@@ -123,6 +127,8 @@ def _open_system_prompt_dialog(*, prompt_key: str, current_value: str) -> None:
123
  with save_col:
124
  if st.button("Save", type="primary", use_container_width=True):
125
  st.session_state[prompt_key] = new_value
 
 
126
  st.rerun()
127
  with cancel_col:
128
  if st.button("Cancel", use_container_width=True):
@@ -329,7 +335,6 @@ def render_chat_window(
329
  *,
330
  chat_log: Any,
331
  messages: list[dict[str, str]],
332
- chat_state: dict[str, object],
333
  edit_key: str,
334
  pending_key: str,
335
  show_contrast: bool = False,
@@ -354,7 +359,6 @@ def render_chat_window(
354
  _open_edit_dialog(
355
  msg_index=i,
356
  messages=messages,
357
- chat_state=chat_state,
358
  pending_key=pending_key,
359
  )
360
 
@@ -372,6 +376,8 @@ def render_system_prompt(
372
  prompt_key: str,
373
  prompt_mode: str,
374
  active_system_prompt: str | None,
 
 
375
  ) -> str | None:
376
  if prompt_key not in st.session_state:
377
  st.session_state[prompt_key] = active_system_prompt or ""
@@ -381,7 +387,11 @@ def render_system_prompt(
381
  if prompt_mode != "empty" and st.button(
382
  "Edit", icon=":material/edit:", key=f"{prompt_key}_edit"
383
  ):
384
- _open_system_prompt_dialog(prompt_key=prompt_key, current_value=current)
 
 
 
 
385
  return st.session_state.get(prompt_key) or None
386
 
387
 
 
1
+ from collections.abc import Callable
2
  from dataclasses import asdict, dataclass
3
  from typing import Any
4
 
 
75
  *,
76
  msg_index: int,
77
  messages: list[dict[str, str]],
 
78
  pending_key: str,
79
  ) -> None:
80
  message = messages[msg_index]
 
102
  if role == "assistant":
103
  messages[msg_index]["_needs_contrast"] = True
104
  del messages[msg_index + 1 :]
 
105
  if role == "user":
106
  st.session_state[pending_key] = "regenerate_after_edit"
107
  st.rerun()
 
111
 
112
 
113
  @st.dialog("Edit system prompt", width="large")
114
+ def _open_system_prompt_dialog(
115
+ *,
116
+ prompt_key: str,
117
+ current_value: str,
118
+ on_save: Callable[[], None] | None = None,
119
+ ) -> None:
120
  new_value = st.text_area(
121
  "System prompt",
122
  value=current_value,
 
127
  with save_col:
128
  if st.button("Save", type="primary", use_container_width=True):
129
  st.session_state[prompt_key] = new_value
130
+ if on_save is not None:
131
+ on_save()
132
  st.rerun()
133
  with cancel_col:
134
  if st.button("Cancel", use_container_width=True):
 
335
  *,
336
  chat_log: Any,
337
  messages: list[dict[str, str]],
 
338
  edit_key: str,
339
  pending_key: str,
340
  show_contrast: bool = False,
 
359
  _open_edit_dialog(
360
  msg_index=i,
361
  messages=messages,
 
362
  pending_key=pending_key,
363
  )
364
 
 
376
  prompt_key: str,
377
  prompt_mode: str,
378
  active_system_prompt: str | None,
379
+ *,
380
+ on_save: Callable[[], None] | None = None,
381
  ) -> str | None:
382
  if prompt_key not in st.session_state:
383
  st.session_state[prompt_key] = active_system_prompt or ""
 
387
  if prompt_mode != "empty" and st.button(
388
  "Edit", icon=":material/edit:", key=f"{prompt_key}_edit"
389
  ):
390
+ _open_system_prompt_dialog(
391
+ prompt_key=prompt_key,
392
+ current_value=current,
393
+ on_save=on_save,
394
+ )
395
  return st.session_state.get(prompt_key) or None
396
 
397
 
tabs/compare.py CHANGED
@@ -42,6 +42,24 @@ def _filename(*parts: str) -> str:
42
 
43
  _list_layers_cached = st.cache_data(show_spinner=False)(list_local_layers)
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  # Keep compare-tab selection state separate so projection defaults do not
46
  # overwrite cosine similarity defaults.
47
  _LAST_COSINE_PERSONAS_KEY = "compare:last_personas:cosine"
@@ -75,8 +93,13 @@ def _layers_for_variant(
75
  if isinstance(store, HFActivationStore):
76
  if not persona_ids:
77
  return []
78
- sample = store.load(variant, persona_ids[0])
79
- return list(range(int(sample.shape[0])))
 
 
 
 
 
80
  return _list_layers_cached(
81
  str(store.root_dir),
82
  store.model_name,
@@ -234,12 +257,20 @@ def _build_cosine_figures(
234
  store: Store,
235
  selection: CosineSelection,
236
  ) -> tuple[object, object | None, int, int] | None:
 
 
 
 
 
 
 
 
 
 
 
 
237
  try:
238
- variant_samples = load_variant_vectors(
239
- store,
240
- [selection.variant_a, selection.variant_b],
241
- persona_ids=selection.persona_ids,
242
- )
243
  except Exception as exc:
244
  st.error(f"Could not load vectors: {exc}")
245
  return None
@@ -266,15 +297,7 @@ def _build_cosine_figures(
266
  pair_errors = []
267
  for left, right in combinations(selection.variants, 2):
268
  try:
269
- pair_samples = (
270
- variant_samples
271
- if {left, right} == {selection.variant_a, selection.variant_b}
272
- else load_variant_vectors(
273
- store,
274
- [left, right],
275
- persona_ids=selection.persona_ids,
276
- )
277
- )
278
  pair_traces.append(
279
  (
280
  f"{prompt_variant_label(left)} vs {prompt_variant_label(right)}",
 
42
 
43
  _list_layers_cached = st.cache_data(show_spinner=False)(list_local_layers)
44
 
45
+
46
+ @st.cache_data(show_spinner=False)
47
+ def _hub_layers_cached(
48
+ repo_id: str,
49
+ model_name: str,
50
+ mask_strategy_value: str,
51
+ variant: str,
52
+ persona_id: str,
53
+ ) -> list[int]:
54
+ store = HFActivationStore(
55
+ repo_id,
56
+ model_name,
57
+ mask_strategy=MaskStrategy(mask_strategy_value),
58
+ )
59
+ sample = store.load(variant, persona_id)
60
+ return list(range(int(sample.shape[0])))
61
+
62
+
63
  # Keep compare-tab selection state separate so projection defaults do not
64
  # overwrite cosine similarity defaults.
65
  _LAST_COSINE_PERSONAS_KEY = "compare:last_personas:cosine"
 
93
  if isinstance(store, HFActivationStore):
94
  if not persona_ids:
95
  return []
96
+ return _hub_layers_cached(
97
+ store.repo_id,
98
+ store.model_name,
99
+ mask_strategy.value,
100
+ variant,
101
+ persona_ids[0],
102
+ )
103
  return _list_layers_cached(
104
  str(store.root_dir),
105
  store.model_name,
 
257
  store: Store,
258
  selection: CosineSelection,
259
  ) -> tuple[object, object | None, int, int] | None:
260
+ variant_sample_cache = {}
261
+
262
+ def _load_pair(left: str, right: str):
263
+ key = tuple(sorted((left, right)))
264
+ if key not in variant_sample_cache:
265
+ variant_sample_cache[key] = load_variant_vectors(
266
+ store,
267
+ [left, right],
268
+ persona_ids=selection.persona_ids,
269
+ )
270
+ return variant_sample_cache[key]
271
+
272
  try:
273
+ variant_samples = _load_pair(selection.variant_a, selection.variant_b)
 
 
 
 
274
  except Exception as exc:
275
  st.error(f"Could not load vectors: {exc}")
276
  return None
 
297
  pair_errors = []
298
  for left, right in combinations(selection.variants, 2):
299
  try:
300
+ pair_samples = _load_pair(left, right)
 
 
 
 
 
 
 
 
301
  pair_traces.append(
302
  (
303
  f"{prompt_variant_label(left)} vs {prompt_variant_label(right)}",
tabs/compare_chat.py CHANGED
@@ -105,6 +105,12 @@ def _render_compare_panel(
105
  prompt_key,
106
  prompt_mode,
107
  active_system_prompt,
 
 
 
 
 
 
108
  )
109
 
110
  return ComparePanel(
@@ -138,7 +144,6 @@ def _generate_panels(
138
  panel.prompt, panel.state["messages"]
139
  ),
140
  remote=remote,
141
- past_key_values=panel.state["past_key_values"],
142
  **generation.to_generate_kwargs(),
143
  )
144
  )
@@ -151,7 +156,6 @@ def _apply_panel_results(
151
  *,
152
  panels: list[ComparePanel],
153
  results: list[ChatReply | Exception],
154
- remote: bool,
155
  rollback_user_on_error: bool,
156
  ) -> list[ChatReply | None]:
157
  valid_results: list[ChatReply | None] = []
@@ -165,7 +169,6 @@ def _apply_panel_results(
165
  continue
166
 
167
  panel.state["messages"].append({"role": "assistant", "content": result.text})
168
- panel.state["past_key_values"] = result.past_key_values if not remote else None
169
  valid_results.append(result)
170
  return valid_results
171
 
@@ -242,7 +245,6 @@ def _render_compare_history(
242
  render_chat_window(
243
  chat_log=panel.log,
244
  messages=panel.state["messages"],
245
- chat_state=panel.state,
246
  edit_key=panel.edit_key,
247
  pending_key=panel.pending_key,
248
  show_contrast=contrast_enabled,
@@ -409,7 +411,6 @@ def render_compare_mode(
409
  _apply_panel_results(
410
  panels=regen_panels,
411
  results=results,
412
- remote=remote,
413
  rollback_user_on_error=False,
414
  )
415
  st.rerun()
@@ -452,7 +453,6 @@ def render_compare_mode(
452
  valid_results = _apply_panel_results(
453
  panels=panels,
454
  results=results,
455
- remote=remote,
456
  rollback_user_on_error=True,
457
  )
458
  if contrast_enabled:
 
105
  prompt_key,
106
  prompt_mode,
107
  active_system_prompt,
108
+ on_save=lambda: reset_chat_context_state(
109
+ state,
110
+ selected_persona.id,
111
+ prompt_mode,
112
+ pending_key,
113
+ ),
114
  )
115
 
116
  return ComparePanel(
 
144
  panel.prompt, panel.state["messages"]
145
  ),
146
  remote=remote,
 
147
  **generation.to_generate_kwargs(),
148
  )
149
  )
 
156
  *,
157
  panels: list[ComparePanel],
158
  results: list[ChatReply | Exception],
 
159
  rollback_user_on_error: bool,
160
  ) -> list[ChatReply | None]:
161
  valid_results: list[ChatReply | None] = []
 
169
  continue
170
 
171
  panel.state["messages"].append({"role": "assistant", "content": result.text})
 
172
  valid_results.append(result)
173
  return valid_results
174
 
 
245
  render_chat_window(
246
  chat_log=panel.log,
247
  messages=panel.state["messages"],
 
248
  edit_key=panel.edit_key,
249
  pending_key=panel.pending_key,
250
  show_contrast=contrast_enabled,
 
411
  _apply_panel_results(
412
  panels=regen_panels,
413
  results=results,
 
414
  rollback_user_on_error=False,
415
  )
416
  st.rerun()
 
453
  valid_results = _apply_panel_results(
454
  panels=panels,
455
  results=results,
 
456
  rollback_user_on_error=True,
457
  )
458
  if contrast_enabled:
tabs/extract.py CHANGED
@@ -12,7 +12,7 @@ from persona_vectors.extraction import (
12
  )
13
  from persona_vectors.preview import TokenSegment, preview_token_segments
14
 
15
- from utils.datasets import load_dataset
16
  from utils.helpers import (
17
  NDIF_STATUS_ICONS,
18
  persona_label,
@@ -111,6 +111,11 @@ def _load_qa_dataset_personas(
111
  personas_file=st.session_state.get("extract__personas_file"),
112
  qa_file=st.session_state.get("extract__qa_file"),
113
  )
 
 
 
 
 
114
  st.caption(dataset_status)
115
  except Exception as exc:
116
  st.error(f"Could not load data: {exc}")
@@ -123,7 +128,6 @@ def _load_qa_dataset_personas(
123
  st.info("This dataset is persona-only for now. Use Chat to browse personas.")
124
  return None
125
 
126
- personas = list(dataset)
127
  if not personas:
128
  st.warning("No personas found in the selected dataset.")
129
  st.info(
 
12
  )
13
  from persona_vectors.preview import TokenSegment, preview_token_segments
14
 
15
+ from utils.datasets import load_dataset, load_persona_list
16
  from utils.helpers import (
17
  NDIF_STATUS_ICONS,
18
  persona_label,
 
111
  personas_file=st.session_state.get("extract__personas_file"),
112
  qa_file=st.session_state.get("extract__qa_file"),
113
  )
114
+ personas, _ = load_persona_list(
115
+ dataset_source,
116
+ personas_file=st.session_state.get("extract__personas_file"),
117
+ qa_file=st.session_state.get("extract__qa_file"),
118
+ )
119
  st.caption(dataset_status)
120
  except Exception as exc:
121
  st.error(f"Could not load data: {exc}")
 
128
  st.info("This dataset is persona-only for now. Use Chat to browse personas.")
129
  return None
130
 
 
131
  if not personas:
132
  st.warning("No personas found in the selected dataset.")
133
  st.info(
tabs/probe_ui.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import streamlit as st
2
  import torch
3
 
@@ -103,7 +105,10 @@ def _render_token_picker(trace: ConversationTrace, context_key: str) -> int:
103
  parts: list[str] = []
104
  for i in range(start, end):
105
  token_repr = trace.tokens[i].encode("unicode_escape").decode("ascii") or "·"
106
- parts.append(f"**[{token_repr}]**" if i == selected else token_repr)
 
 
 
107
  st.markdown(
108
  f"<div style='font-family:ui-monospace,monospace;font-size:0.85em;"
109
  f"line-height:1.6;background:rgba(127,127,127,0.08);padding:6px 10px;"
 
1
+ import html
2
+
3
  import streamlit as st
4
  import torch
5
 
 
105
  parts: list[str] = []
106
  for i in range(start, end):
107
  token_repr = trace.tokens[i].encode("unicode_escape").decode("ascii") or "·"
108
+ token_repr = html.escape(token_repr)
109
+ parts.append(
110
+ f"<strong>[{token_repr}]</strong>" if i == selected else token_repr
111
+ )
112
  st.markdown(
113
  f"<div style='font-family:ui-monospace,monospace;font-size:0.85em;"
114
  f"line-height:1.6;background:rgba(127,127,127,0.08);padding:6px 10px;"
utils/chat.py CHANGED
@@ -15,7 +15,6 @@ SystemPromptMode = Literal["empty", "templated", "biography", "custom"]
15
  @dataclass
16
  class ChatReply:
17
  text: str
18
- past_key_values: object | None
19
  generated_ids: torch.Tensor | None = None
20
 
21
 
@@ -171,7 +170,6 @@ def generate_chat_reply(
171
  model: StandardizedTransformer,
172
  messages: list[dict[str, str]],
173
  remote: bool,
174
- past_key_values: object | None = None,
175
  max_new_tokens: int = 256,
176
  do_sample: bool = False,
177
  temperature: float = 1.0,
@@ -183,14 +181,12 @@ def generate_chat_reply(
183
  """Generate one assistant reply from a full chat history.
184
 
185
  The helper uses ``model.generate`` so it works with both local and NDIF-backed
186
- nnsight models. The full conversation is re-rendered each turn and the cache from
187
- the previous turn is reused when available.
188
 
189
  Args:
190
  model: Loaded standardized nnterp model.
191
  messages: Full chat history, including any system prompt as the first message.
192
  remote: Whether to execute the generation on NDIF.
193
- past_key_values: Cache returned by the previous generation step.
194
  max_new_tokens: Maximum number of assistant tokens to generate.
195
  do_sample: Whether to sample from the model distribution.
196
  temperature: Sampling temperature, used only when sampling is enabled.
@@ -200,7 +196,7 @@ def generate_chat_reply(
200
  seed: Optional local RNG seed for sampled generation.
201
 
202
  Returns:
203
- ChatReply with generated text and the updated cache.
204
  """
205
 
206
  tokenizer = model.tokenizer
@@ -220,9 +216,6 @@ def generate_chat_reply(
220
  generation_kwargs["top_k"] = top_k
221
  if repetition_penalty != 1.0:
222
  generation_kwargs["repetition_penalty"] = repetition_penalty
223
- if past_key_values is not None and not remote:
224
- generation_kwargs["past_key_values"] = past_key_values
225
-
226
  # `remote` is captured by nnsight's RemoteableMixin.trace() and is NOT
227
  # forwarded to the underlying model's generate
228
  with _seeded_rng(seed if do_sample and not remote else None):
@@ -240,8 +233,5 @@ def generate_chat_reply(
240
  text = tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
241
  return ChatReply(
242
  text=text,
243
- past_key_values=(
244
- getattr(generated, "past_key_values", None) if not remote else None
245
- ),
246
  generated_ids=generated_ids.detach().cpu(),
247
  )
 
15
  @dataclass
16
  class ChatReply:
17
  text: str
 
18
  generated_ids: torch.Tensor | None = None
19
 
20
 
 
170
  model: StandardizedTransformer,
171
  messages: list[dict[str, str]],
172
  remote: bool,
 
173
  max_new_tokens: int = 256,
174
  do_sample: bool = False,
175
  temperature: float = 1.0,
 
181
  """Generate one assistant reply from a full chat history.
182
 
183
  The helper uses ``model.generate`` so it works with both local and NDIF-backed
184
+ nnsight models. The full conversation is re-rendered each turn.
 
185
 
186
  Args:
187
  model: Loaded standardized nnterp model.
188
  messages: Full chat history, including any system prompt as the first message.
189
  remote: Whether to execute the generation on NDIF.
 
190
  max_new_tokens: Maximum number of assistant tokens to generate.
191
  do_sample: Whether to sample from the model distribution.
192
  temperature: Sampling temperature, used only when sampling is enabled.
 
196
  seed: Optional local RNG seed for sampled generation.
197
 
198
  Returns:
199
+ ChatReply with generated text and token ids.
200
  """
201
 
202
  tokenizer = model.tokenizer
 
216
  generation_kwargs["top_k"] = top_k
217
  if repetition_penalty != 1.0:
218
  generation_kwargs["repetition_penalty"] = repetition_penalty
 
 
 
219
  # `remote` is captured by nnsight's RemoteableMixin.trace() and is NOT
220
  # forwarded to the underlying model's generate
221
  with _seeded_rng(seed if do_sample and not remote else None):
 
233
  text = tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
234
  return ChatReply(
235
  text=text,
 
 
 
236
  generated_ids=generated_ids.detach().cpu(),
237
  )
utils/datasets.py CHANGED
@@ -23,6 +23,13 @@ def _cached_dataset(cls: type) -> Any:
23
  return cls()
24
 
25
 
 
 
 
 
 
 
 
26
  def _upload_cache_dir() -> Path:
27
  cache_dir = st.session_state.get("_upload_cache_dir")
28
  if cache_dir is None:
@@ -35,14 +42,12 @@ def _upload_cache_dir() -> Path:
35
 
36
  def _uploaded_file_to_temp_path(uploaded_file: Any, stem: str) -> Path:
37
  suffix = Path(uploaded_file.name).suffix or ".jsonl"
38
- temp_path = _upload_cache_dir() / f"{stem}{suffix}"
39
- hash_path = temp_path.with_suffix(temp_path.suffix + ".sha256")
40
  data = uploaded_file.getvalue()
41
  digest = hashlib.sha256(data).hexdigest()
42
- if temp_path.exists() and hash_path.exists() and hash_path.read_text() == digest:
 
43
  return temp_path
44
  temp_path.write_bytes(data)
45
- hash_path.write_text(digest)
46
  return temp_path
47
 
48
 
@@ -95,7 +100,4 @@ def load_dataset(
95
 
96
  personas_path = _uploaded_file_to_temp_path(personas_file, stem="personas")
97
  qa_path = _uploaded_file_to_temp_path(qa_file, stem="qa")
98
- return (
99
- LocalPersonaDataset(personas_path=personas_path, qa_path=qa_path),
100
- "Local upload",
101
- )
 
23
  return cls()
24
 
25
 
26
+ @st.cache_resource(show_spinner=False)
27
+ def _cached_local_dataset(personas_path: str, qa_path: str) -> LocalPersonaDataset:
28
+ """Instantiate and cache a local upload dataset for stable temp paths."""
29
+
30
+ return LocalPersonaDataset(personas_path=Path(personas_path), qa_path=Path(qa_path))
31
+
32
+
33
  def _upload_cache_dir() -> Path:
34
  cache_dir = st.session_state.get("_upload_cache_dir")
35
  if cache_dir is None:
 
42
 
43
  def _uploaded_file_to_temp_path(uploaded_file: Any, stem: str) -> Path:
44
  suffix = Path(uploaded_file.name).suffix or ".jsonl"
 
 
45
  data = uploaded_file.getvalue()
46
  digest = hashlib.sha256(data).hexdigest()
47
+ temp_path = _upload_cache_dir() / f"{stem}_{digest[:16]}{suffix}"
48
+ if temp_path.exists():
49
  return temp_path
50
  temp_path.write_bytes(data)
 
51
  return temp_path
52
 
53
 
 
100
 
101
  personas_path = _uploaded_file_to_temp_path(personas_file, stem="personas")
102
  qa_path = _uploaded_file_to_temp_path(qa_file, stem="qa")
103
+ return _cached_local_dataset(str(personas_path), str(qa_path)), "Local upload"
 
 
 
utils/theme.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Catppuccin Plotly template installer."""
2
+
3
+ import plotly.graph_objects as go
4
+ import plotly.io as pio
5
+ from catppuccin import PALETTE
6
+
7
+
8
+ def install_catppuccin_theme(base: str | None = None) -> None:
9
+ """Register a Catppuccin template and alias it as ``plotly_white``.
10
+
11
+ Call once at startup. Persona-vectors pins ``template="plotly_white"`` on
12
+ every figure, so replacing that entry themes all plots without any
13
+ per-figure code.
14
+ """
15
+ flavor = PALETTE.latte if base == "light" else PALETTE.mocha
16
+ c = flavor.colors
17
+ bg, surface, line = c.base.hex, c.surface0.hex, c.surface1.hex
18
+ text, subtext = c.text.hex, c.subtext1.hex
19
+
20
+ axis = dict(
21
+ gridcolor=line,
22
+ zerolinecolor=line,
23
+ linecolor=line,
24
+ tickcolor=line,
25
+ tickfont=dict(color=subtext),
26
+ title=dict(font=dict(color=text)),
27
+ )
28
+ scene_axis = dict(
29
+ backgroundcolor=bg,
30
+ gridcolor=line,
31
+ zerolinecolor=line,
32
+ showbackground=True,
33
+ color=text,
34
+ tickfont=dict(color=subtext),
35
+ )
36
+
37
+ template = go.layout.Template(
38
+ layout=dict(
39
+ paper_bgcolor=bg,
40
+ plot_bgcolor=bg,
41
+ font=dict(color=text),
42
+ colorway=[
43
+ c.blue.hex,
44
+ c.mauve.hex,
45
+ c.green.hex,
46
+ c.peach.hex,
47
+ c.teal.hex,
48
+ c.pink.hex,
49
+ c.yellow.hex,
50
+ c.sapphire.hex,
51
+ c.lavender.hex,
52
+ c.red.hex,
53
+ c.sky.hex,
54
+ c.maroon.hex,
55
+ ],
56
+ xaxis=axis,
57
+ yaxis=axis,
58
+ scene=dict(xaxis=scene_axis, yaxis=scene_axis, zaxis=scene_axis),
59
+ legend=dict(bgcolor=surface, bordercolor=line, font=dict(color=text)),
60
+ colorscale=dict(
61
+ diverging=[[0.0, c.blue.hex], [0.5, surface], [1.0, c.red.hex]],
62
+ ),
63
+ )
64
+ )
65
+ pio.templates["catppuccin"] = template
66
+ pio.templates["plotly_white"] = template
67
+ pio.templates.default = "catppuccin"
uv.lock CHANGED
@@ -236,6 +236,15 @@ wheels = [
236
  { url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" },
237
  ]
238
 
 
 
 
 
 
 
 
 
 
239
  [[package]]
240
  name = "certifi"
241
  version = "2026.4.22"
@@ -1569,6 +1578,7 @@ name = "persona-ui"
1569
  version = "0.3.0"
1570
  source = { virtual = "." }
1571
  dependencies = [
 
1572
  { name = "persona-data" },
1573
  { name = "persona-vectors" },
1574
  { name = "plotly" },
@@ -1578,6 +1588,7 @@ dependencies = [
1578
 
1579
  [package.metadata]
1580
  requires-dist = [
 
1581
  { name = "persona-data", specifier = ">=0.4.2" },
1582
  { name = "persona-vectors", specifier = ">=0.6.4" },
1583
  { name = "plotly", specifier = ">=6.6.0" },
 
236
  { url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" },
237
  ]
238
 
239
+ [[package]]
240
+ name = "catppuccin"
241
+ version = "2.5.0"
242
+ source = { registry = "https://pypi.org/simple" }
243
+ sdist = { url = "https://files.pythonhosted.org/packages/b6/31/87c3ca25d29678e076e1f0d151ef8792f0557b26a8dc865035b5f1fa96c4/catppuccin-2.5.0.tar.gz", hash = "sha256:3035f3bf35bc2369d1cb4c754272a494708592a1e62d42655a371863ac7c6834", size = 1918017, upload-time = "2025-08-03T20:06:40.089Z" }
244
+ wheels = [
245
+ { url = "https://files.pythonhosted.org/packages/c8/9c/7908f34009eec72884fe9448a62188423a3621f04ee58e2abaec379c9cc0/catppuccin-2.5.0-py3-none-any.whl", hash = "sha256:74a1f1db79d527905225f1afce1c8858d06e2dd1231e4300a72a812797cdb572", size = 19600, upload-time = "2025-08-03T20:06:38.912Z" },
246
+ ]
247
+
248
  [[package]]
249
  name = "certifi"
250
  version = "2026.4.22"
 
1578
  version = "0.3.0"
1579
  source = { virtual = "." }
1580
  dependencies = [
1581
+ { name = "catppuccin" },
1582
  { name = "persona-data" },
1583
  { name = "persona-vectors" },
1584
  { name = "plotly" },
 
1588
 
1589
  [package.metadata]
1590
  requires-dist = [
1591
+ { name = "catppuccin", specifier = ">=2.5.0" },
1592
  { name = "persona-data", specifier = ">=0.4.2" },
1593
  { name = "persona-vectors", specifier = ">=0.6.4" },
1594
  { name = "plotly", specifier = ">=6.6.0" },