Spaces:
Sleeping
Sleeping
Jac-Zac commited on
Commit ·
12cdb17
1
Parent(s): 0ba2e45
Switching to uniform colortheme catpuccin
Browse files- .streamlit/config.toml +9 -0
- app.py +5 -18
- pyproject.toml +1 -0
- state.py +1 -6
- tabs/chat.py +7 -3
- tabs/chat_ui.py +16 -6
- tabs/compare.py +39 -16
- tabs/compare_chat.py +6 -6
- tabs/extract.py +6 -2
- tabs/probe_ui.py +6 -1
- utils/chat.py +2 -12
- utils/datasets.py +10 -8
- utils/theme.py +67 -0
- uv.lock +11 -0
.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,
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
| 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" },
|