Jac-Zac commited on
Commit
a1b8512
·
1 Parent(s): 7460198

General cleanups

Browse files
Files changed (10) hide show
  1. app.py +22 -61
  2. pyproject.toml +1 -1
  3. tabs/probe.py +25 -30
  4. tests/test_probes.py +3 -39
  5. utils/contrast.py +3 -7
  6. utils/preload.py +9 -0
  7. utils/probe_trace.py +10 -16
  8. utils/probes.py +13 -53
  9. utils/runtime.py +6 -19
  10. uv.lock +34 -34
app.py CHANGED
@@ -5,7 +5,7 @@ import streamlit as st
5
  from dotenv import load_dotenv
6
 
7
  from utils.analysis_sources import DEFAULT_COMPARE_MODEL, DEFAULT_HUB_REPO, SOURCE_HUB
8
- from utils.helpers import DATASET_SOURCES, session_key, widget_key
9
  from utils.preload import preload_once
10
  from utils.runtime import configured_ndif_api_key, list_remote_models
11
  from utils.theme import active_base, install_catppuccin_theme
@@ -53,67 +53,28 @@ _TAB_PRELOAD_FUNCTIONS = {
53
  def _hub_metadata_preload_calls() -> tuple[
54
  tuple[str, tuple[str, str, str, str | None]], ...
55
  ]:
56
- calls: list[tuple[str, tuple[str, str, str, str | None]]] = []
57
-
58
- def add(repo: str, model: str, mask_strategy: str, variant: str | None) -> None:
59
- calls.append(
60
- (
61
- "utils.analysis_sources:prefetch_hub_metadata",
62
- (repo, model, mask_strategy, variant),
63
- )
64
- )
65
-
66
- shared_source = st.session_state.get("source:last_source", SOURCE_HUB)
67
- shared_mask_strategy = st.session_state.get(
68
- "source:last_mask_strategy", "answer_mean"
 
 
 
 
 
 
 
 
69
  )
70
-
71
- analysis_source = st.session_state.get("analysis:last_source", shared_source)
72
- if analysis_source == SOURCE_HUB:
73
- repo = st.session_state.get(
74
- "analysis:hub_repo",
75
- st.session_state.get("source:hub_repo", DEFAULT_HUB_REPO),
76
- )
77
- mask_strategy = st.session_state.get(
78
- "analysis:last_mask_strategy",
79
- shared_mask_strategy,
80
- )
81
- model = st.session_state.get(
82
- widget_key("load", "hub_model", repo, mask_strategy),
83
- st.session_state.get(
84
- "analysis:hub_model_fallback",
85
- st.session_state.get("source:hub_model", DEFAULT_COMPARE_MODEL),
86
- ),
87
- )
88
- variant = st.session_state.get(
89
- "analysis:last_projection_variant",
90
- st.session_state.get("analysis:last_similarity_variant"),
91
- )
92
- add(repo, model, mask_strategy, variant)
93
-
94
- probe_source = st.session_state.get(widget_key("probe", "source"), shared_source)
95
- if probe_source == SOURCE_HUB:
96
- repo = st.session_state.get(
97
- "probe:hub_repo",
98
- st.session_state.get("source:hub_repo", DEFAULT_HUB_REPO),
99
- )
100
- mask_strategy = st.session_state.get(
101
- "probe:last_mask_strategy",
102
- shared_mask_strategy,
103
- )
104
- model = st.session_state.get(
105
- widget_key("probe", "hub_model", repo, mask_strategy),
106
- st.session_state.get(
107
- "probe:hub_model_fallback",
108
- st.session_state.get("source:hub_model", DEFAULT_COMPARE_MODEL),
109
- ),
110
- )
111
- add(repo, model, mask_strategy, st.session_state.get("probe:variant"))
112
-
113
- deduped: dict[tuple[str, tuple[str, str, str, str | None]], None] = {}
114
- for call in calls:
115
- deduped[call] = None
116
- return tuple(deduped)
117
 
118
 
119
  @dataclass(frozen=True)
 
5
  from dotenv import load_dotenv
6
 
7
  from utils.analysis_sources import DEFAULT_COMPARE_MODEL, DEFAULT_HUB_REPO, SOURCE_HUB
8
+ from utils.helpers import DATASET_SOURCES, session_key
9
  from utils.preload import preload_once
10
  from utils.runtime import configured_ndif_api_key, list_remote_models
11
  from utils.theme import active_base, install_catppuccin_theme
 
53
  def _hub_metadata_preload_calls() -> tuple[
54
  tuple[str, tuple[str, str, str, str | None]], ...
55
  ]:
56
+ """Warm Hub metadata for the currently-selected source, if any.
57
+
58
+ Reads only the shared ``source:*`` keys (written by ``utils.source_controls``)
59
+ so this stays a small, single-purpose preload. Per-tab divergence used to
60
+ matter when each tab picked its own model; the shared keys are now the
61
+ source of truth.
62
+ """
63
+
64
+ if st.session_state.get("source:last_source", SOURCE_HUB) != SOURCE_HUB:
65
+ return ()
66
+ repo = st.session_state.get("source:hub_repo", DEFAULT_HUB_REPO)
67
+ model = st.session_state.get("source:hub_model", DEFAULT_COMPARE_MODEL)
68
+ mask_strategy = st.session_state.get("source:last_mask_strategy", "answer_mean")
69
+ variant = st.session_state.get("probe:variant") or st.session_state.get(
70
+ "analysis:last_projection_variant"
71
+ )
72
+ return (
73
+ (
74
+ "utils.analysis_sources:prefetch_hub_metadata",
75
+ (repo, model, mask_strategy, variant),
76
+ ),
77
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
 
80
  @dataclass(frozen=True)
pyproject.toml CHANGED
@@ -5,7 +5,7 @@ description = "Streamlit UI for persona-vectors"
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
8
- "persona-vectors>=0.8.6",
9
  "datasets>=4.8.5",
10
  "huggingface-hub>=1.14.0",
11
  "streamlit>=1.44.0",
 
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
8
+ "persona-vectors>=0.8.7",
9
  "datasets>=4.8.5",
10
  "huggingface-hub>=1.14.0",
11
  "streamlit>=1.44.0",
tabs/probe.py CHANGED
@@ -88,42 +88,32 @@ def _select_personas(
88
  if not all_ids:
89
  st.info("No personas found for this variant.")
90
  return []
91
- regular = all_ids
92
- if len(regular) < 2:
93
  st.info("At least two non-assistant personas are needed for probing.")
94
  return []
95
- min_count = min(10, len(regular))
96
- if min_count == len(regular):
97
- count = len(regular)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  st.warning(
99
  f"Only {count} non-assistant personas are available; using all of them."
100
  )
101
- st.session_state["probe:persona_count"] = count
102
- persona_ids = regular
103
- persona_names_cached(
104
- source,
105
- location,
106
- model_name,
107
- mask_strategy.value,
108
- (variant,),
109
- tuple(persona_ids),
110
- )
111
- st.caption(f"Probing {len(persona_ids)} non-assistant personas.")
112
- return persona_ids
113
 
114
- default_count = min(
115
- len(regular),
116
- max(min_count, st.session_state.get("probe:persona_count", len(regular))),
117
- )
118
- count = st.slider(
119
- "Personas",
120
- min_value=min_count,
121
- max_value=len(regular),
122
- value=default_count,
123
- key="probe:persona_count_slider",
124
- )
125
  st.session_state["probe:persona_count"] = count
126
- persona_ids = regular[:count]
127
  persona_names_cached(
128
  source,
129
  location,
@@ -132,7 +122,12 @@ def _select_personas(
132
  (variant,),
133
  tuple(persona_ids),
134
  )
135
- st.caption(f"Probing {len(persona_ids)} of {len(regular)} non-assistant personas.")
 
 
 
 
 
136
  return persona_ids
137
 
138
 
 
88
  if not all_ids:
89
  st.info("No personas found for this variant.")
90
  return []
91
+ if len(all_ids) < 2:
 
92
  st.info("At least two non-assistant personas are needed for probing.")
93
  return []
94
+
95
+ min_count = min(10, len(all_ids))
96
+ has_slider = min_count < len(all_ids)
97
+ if has_slider:
98
+ default_count = max(
99
+ min_count,
100
+ min(len(all_ids), st.session_state.get("probe:persona_count", len(all_ids))),
101
+ )
102
+ count = st.slider(
103
+ "Personas",
104
+ min_value=min_count,
105
+ max_value=len(all_ids),
106
+ value=default_count,
107
+ key="probe:persona_count_slider",
108
+ )
109
+ else:
110
+ count = len(all_ids)
111
  st.warning(
112
  f"Only {count} non-assistant personas are available; using all of them."
113
  )
 
 
 
 
 
 
 
 
 
 
 
 
114
 
 
 
 
 
 
 
 
 
 
 
 
115
  st.session_state["probe:persona_count"] = count
116
+ persona_ids = all_ids[:count]
117
  persona_names_cached(
118
  source,
119
  location,
 
122
  (variant,),
123
  tuple(persona_ids),
124
  )
125
+ if has_slider:
126
+ st.caption(
127
+ f"Probing {len(persona_ids)} of {len(all_ids)} non-assistant personas."
128
+ )
129
+ else:
130
+ st.caption(f"Probing {len(persona_ids)} non-assistant personas.")
131
  return persona_ids
132
 
133
 
tests/test_probes.py CHANGED
@@ -1,12 +1,10 @@
1
  """Regression tests for utils.probes.
2
 
3
  Covers the probe-artifact filename parser (both naming conventions) and the
4
- two correctness fixes:
5
 
6
  * ``_normalize_batch`` applies PCA independently of the scaler (previously the
7
  PCA branch was unreachable when no scaler was present).
8
- * ``LoadedProbe.run`` predicts class 1 for a single-output probe whose sigmoid
9
- score is >= 0.5 (previously it always predicted class 0).
10
  """
11
 
12
  import pytest
@@ -162,41 +160,6 @@ def test_normalize_batch_pca_shape_mismatch_raises():
162
  probe._normalize_batch(torch.zeros(1, 3))
163
 
164
 
165
- # --------------------------------------------------------------------------- #
166
- # LoadedProbe.run — single-output prediction
167
- # --------------------------------------------------------------------------- #
168
-
169
-
170
- def _single_output_probe(weight: list[float], bias: float) -> LoadedProbe:
171
- model = _LinearProbe(input_dim=len(weight), num_classes=1)
172
- with torch.no_grad():
173
- model.linear.weight.copy_(torch.tensor([weight]))
174
- model.linear.bias.copy_(torch.tensor([bias]))
175
- return LoadedProbe(
176
- model=model,
177
- input_dim=len(weight),
178
- labels=["neg", "pos"],
179
- model_type="linear",
180
- layer=0,
181
- location=None,
182
- )
183
-
184
-
185
- def test_run_single_output_predicts_positive_when_score_high():
186
- """Regression: single-output probe must predict class 1 when sigmoid >= 0.5."""
187
- probe = _single_output_probe(weight=[1.0, 1.0], bias=5.0)
188
- result = probe.run(torch.tensor([1.0, 1.0]))
189
- assert result.predicted_index == 1
190
- assert result.predicted_label == "pos"
191
-
192
-
193
- def test_run_single_output_predicts_negative_when_score_low():
194
- probe = _single_output_probe(weight=[1.0, 1.0], bias=-5.0)
195
- result = probe.run(torch.tensor([1.0, 1.0]))
196
- assert result.predicted_index == 0
197
- assert result.predicted_label == "neg"
198
-
199
-
200
  # --------------------------------------------------------------------------- #
201
  # canonical persona-vectors artifacts
202
  # --------------------------------------------------------------------------- #
@@ -224,4 +187,5 @@ def test_loaded_probe_from_canonical_artifact():
224
  )
225
  assert probe.labels == ["neg", "pos"]
226
  assert probe.layer == 3
227
- assert probe.run(torch.tensor([1.0, 0.0])).predicted_label == "pos"
 
 
1
  """Regression tests for utils.probes.
2
 
3
  Covers the probe-artifact filename parser (both naming conventions) and the
4
+ correctness fix:
5
 
6
  * ``_normalize_batch`` applies PCA independently of the scaler (previously the
7
  PCA branch was unreachable when no scaler was present).
 
 
8
  """
9
 
10
  import pytest
 
160
  probe._normalize_batch(torch.zeros(1, 3))
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  # --------------------------------------------------------------------------- #
164
  # canonical persona-vectors artifacts
165
  # --------------------------------------------------------------------------- #
 
187
  )
188
  assert probe.labels == ["neg", "pos"]
189
  assert probe.layer == 3
190
+ _, _, predicted = probe.run_batch(torch.tensor([[1.0, 0.0]]))
191
+ assert probe.labels[int(predicted[0])] == "pos"
utils/contrast.py CHANGED
@@ -49,12 +49,10 @@ def _strip_special_ids(
49
  ) -> tuple[torch.Tensor, torch.Tensor]:
50
  """Return display ids and a mask that excludes special tokens."""
51
  ids = ids.cpu()
52
- special_ids = set(getattr(tokenizer, "all_special_ids", []) or [])
53
  if not special_ids or ids.numel() == 0:
54
  return ids, torch.ones(ids.shape[0], dtype=torch.bool)
55
- keep = torch.tensor(
56
- [tid.item() not in special_ids for tid in ids], dtype=torch.bool
57
- )
58
  return ids[keep], keep
59
 
60
 
@@ -67,9 +65,7 @@ def _prepare_trace_input_ids(
67
  context_prompt, _ = format_generation_prompt(context_messages, tokenizer)
68
  context_ids = tokenizer(context_prompt, return_tensors="pt").input_ids[0]
69
  input_ids = torch.cat([context_ids.cpu(), response_ids.detach().cpu()])
70
- n_ctx = len(context_ids)
71
- n_resp = len(response_ids)
72
- return input_ids, n_ctx, n_resp
73
 
74
 
75
  def _build_contrast(
 
49
  ) -> tuple[torch.Tensor, torch.Tensor]:
50
  """Return display ids and a mask that excludes special tokens."""
51
  ids = ids.cpu()
52
+ special_ids = sorted(getattr(tokenizer, "all_special_ids", []) or [])
53
  if not special_ids or ids.numel() == 0:
54
  return ids, torch.ones(ids.shape[0], dtype=torch.bool)
55
+ keep = ~torch.isin(ids, torch.tensor(special_ids, dtype=ids.dtype))
 
 
56
  return ids[keep], keep
57
 
58
 
 
65
  context_prompt, _ = format_generation_prompt(context_messages, tokenizer)
66
  context_ids = tokenizer(context_prompt, return_tensors="pt").input_ids[0]
67
  input_ids = torch.cat([context_ids.cpu(), response_ids.detach().cpu()])
68
+ return input_ids, len(context_ids), len(response_ids)
 
 
69
 
70
 
71
  def _build_contrast(
utils/preload.py CHANGED
@@ -82,4 +82,13 @@ def preload_once(
82
  name=f"persona-ui-preload-{name}",
83
  daemon=True,
84
  )
 
 
 
 
 
 
 
 
 
85
  thread.start()
 
82
  name=f"persona-ui-preload-{name}",
83
  daemon=True,
84
  )
85
+ # Attach this Streamlit script's run context so calls into ``@st.cache_*``
86
+ # functions land in the right session instead of emitting a
87
+ # "missing ScriptRunContext" warning per call.
88
+ try:
89
+ from streamlit.runtime.scriptrunner import add_script_run_ctx
90
+
91
+ add_script_run_ctx(thread)
92
+ except Exception:
93
+ logger.debug("Could not attach script run context to preload thread")
94
  thread.start()
utils/probe_trace.py CHANGED
@@ -58,7 +58,6 @@ def trace_conversation(
58
  model.tokenizer,
59
  add_generation_prompt=False,
60
  )
61
- assistant_mask_seq = _compute_assistant_mask(model.tokenizer, messages)
62
  prompt_hash = hashlib.sha256(prompt_text.encode("utf-8")).hexdigest()
63
  cache_key = _trace_cache_key(
64
  model_name=model_name,
@@ -71,6 +70,8 @@ def trace_conversation(
71
  if cached is not None:
72
  return cached
73
 
 
 
74
  accessor = _select_accessor(model, location)
75
  if remote:
76
  from utils.runtime import remote_backend
@@ -199,19 +200,15 @@ def _drop_derived_results_for_trace(cache_key: str) -> None:
199
  f"probe_values::{cache_key}::",
200
  )
201
  tracked = st.session_state.get(_DERIVED_CACHE_TRACKER_KEY)
202
- if isinstance(tracked, list):
203
- kept: list[str] = []
204
- for key in tracked:
205
- if isinstance(key, str) and key.startswith(prefixes):
206
- st.session_state.pop(key, None)
207
- else:
208
- kept.append(key)
209
- st.session_state[_DERIVED_CACHE_TRACKER_KEY] = kept
210
  return
211
-
212
- for key in list(st.session_state):
213
  if isinstance(key, str) and key.startswith(prefixes):
214
  st.session_state.pop(key, None)
 
 
 
215
 
216
 
217
  def _compute_assistant_mask(
@@ -395,10 +392,7 @@ def _assistant_spans(
395
 
396
 
397
  def _special_token_mask(tokenizer: object, input_ids: torch.Tensor) -> torch.Tensor:
398
- special_ids = set(getattr(tokenizer, "all_special_ids", []) or [])
399
  if not special_ids:
400
  return torch.zeros(int(input_ids.shape[0]), dtype=torch.bool)
401
- return torch.tensor(
402
- [int(token_id) in special_ids for token_id in input_ids.tolist()],
403
- dtype=torch.bool,
404
- )
 
58
  model.tokenizer,
59
  add_generation_prompt=False,
60
  )
 
61
  prompt_hash = hashlib.sha256(prompt_text.encode("utf-8")).hexdigest()
62
  cache_key = _trace_cache_key(
63
  model_name=model_name,
 
70
  if cached is not None:
71
  return cached
72
 
73
+ assistant_mask_seq = _compute_assistant_mask(model.tokenizer, messages)
74
+
75
  accessor = _select_accessor(model, location)
76
  if remote:
77
  from utils.runtime import remote_backend
 
200
  f"probe_values::{cache_key}::",
201
  )
202
  tracked = st.session_state.get(_DERIVED_CACHE_TRACKER_KEY)
203
+ if not isinstance(tracked, list):
 
 
 
 
 
 
 
204
  return
205
+ kept: list[str] = []
206
+ for key in tracked:
207
  if isinstance(key, str) and key.startswith(prefixes):
208
  st.session_state.pop(key, None)
209
+ else:
210
+ kept.append(key)
211
+ st.session_state[_DERIVED_CACHE_TRACKER_KEY] = kept
212
 
213
 
214
  def _compute_assistant_mask(
 
392
 
393
 
394
  def _special_token_mask(tokenizer: object, input_ids: torch.Tensor) -> torch.Tensor:
395
+ special_ids = sorted(getattr(tokenizer, "all_special_ids", []) or [])
396
  if not special_ids:
397
  return torch.zeros(int(input_ids.shape[0]), dtype=torch.bool)
398
+ return torch.isin(input_ids.cpu(), torch.tensor(special_ids, dtype=input_ids.dtype))
 
 
 
utils/probes.py CHANGED
@@ -21,15 +21,6 @@ from utils.probe_files import (
21
  _PROBE_CACHE_ENTRIES = env_int("PERSONA_UI_PROBE_CACHE_ENTRIES", 8)
22
 
23
 
24
- @dataclass(frozen=True)
25
- class ProbeRunResult:
26
- input_dim: int
27
- logits: torch.Tensor
28
- probabilities: torch.Tensor
29
- predicted_index: int
30
- predicted_label: str | None
31
-
32
-
33
  class _LinearProbe(nn.Module):
34
  def __init__(self, input_dim: int, num_classes: int):
35
  super().__init__()
@@ -85,6 +76,15 @@ class LoadedProbe:
85
 
86
  def __post_init__(self) -> None:
87
  self.model.eval()
 
 
 
 
 
 
 
 
 
88
 
89
  @property
90
  def is_regression(self) -> bool:
@@ -113,43 +113,6 @@ class LoadedProbe:
113
  outputs = outputs.unsqueeze(-1)
114
  return outputs
115
 
116
- def run(self, vector: torch.Tensor) -> ProbeRunResult:
117
- if vector.ndim != 1:
118
- raise ValueError(
119
- f"Probe expects a 1D activation vector, got shape {tuple(vector.shape)}"
120
- )
121
- if vector.shape[0] != self.input_dim:
122
- raise ValueError(
123
- f"Probe expects input dim {self.input_dim}, got {vector.shape[0]}"
124
- )
125
-
126
- batch = vector.detach().to(dtype=torch.float32, device="cpu").unsqueeze(0)
127
- logits_batch, probs_batch = self._forward_batch(batch)
128
- logits = logits_batch.squeeze(0)
129
- probs = probs_batch.squeeze(0)
130
- if logits.ndim == 0:
131
- logits = logits.unsqueeze(0)
132
- if probs.ndim == 0:
133
- probs = probs.unsqueeze(0)
134
-
135
- predicted_index = (
136
- int(probs.item() >= 0.5)
137
- if probs.numel() == 1
138
- else int(torch.argmax(probs).item())
139
- )
140
- predicted_label = (
141
- self.labels[predicted_index]
142
- if 0 <= predicted_index < len(self.labels)
143
- else None
144
- )
145
- return ProbeRunResult(
146
- input_dim=int(vector.shape[0]),
147
- logits=logits,
148
- probabilities=probs,
149
- predicted_index=predicted_index,
150
- predicted_label=predicted_label,
151
- )
152
-
153
  def run_batch(
154
  self, activations: torch.Tensor
155
  ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
@@ -188,8 +151,7 @@ class LoadedProbe:
188
 
189
  def _normalize_batch(self, batch: torch.Tensor) -> torch.Tensor:
190
  if self.scaler_mean is not None and self.scaler_std is not None:
191
- mean = self.scaler_mean.to(dtype=torch.float32)
192
- std = self.scaler_std.to(dtype=torch.float32)
193
  if mean.ndim != 1 or std.ndim != 1 or mean.shape[0] != batch.shape[1]:
194
  raise ValueError(
195
  "Probe scaler shape does not match activation hidden size: "
@@ -199,14 +161,12 @@ class LoadedProbe:
199
  safe_std = torch.where(std == 0, torch.ones_like(std), std)
200
  batch = (batch - mean) / safe_std
201
  if self.pca_mean is not None and self.pca_components is not None:
202
- pca_mean = self.pca_mean.to(dtype=torch.float32)
203
- components = self.pca_components.to(dtype=torch.float32)
204
- if pca_mean.ndim != 1 or pca_mean.shape[0] != batch.shape[1]:
205
  raise ValueError(
206
  "Probe PCA mean shape does not match activation hidden size: "
207
- f"mean={tuple(pca_mean.shape)} batch={tuple(batch.shape)}"
208
  )
209
- batch = (batch - pca_mean) @ components.T
210
  return batch
211
 
212
 
 
21
  _PROBE_CACHE_ENTRIES = env_int("PERSONA_UI_PROBE_CACHE_ENTRIES", 8)
22
 
23
 
 
 
 
 
 
 
 
 
 
24
  class _LinearProbe(nn.Module):
25
  def __init__(self, input_dim: int, num_classes: int):
26
  super().__init__()
 
76
 
77
  def __post_init__(self) -> None:
78
  self.model.eval()
79
+ # Cast normalization tensors once so per-forward calls don't redo it.
80
+ if self.scaler_mean is not None:
81
+ self.scaler_mean = self.scaler_mean.to(dtype=torch.float32)
82
+ if self.scaler_std is not None:
83
+ self.scaler_std = self.scaler_std.to(dtype=torch.float32)
84
+ if self.pca_mean is not None:
85
+ self.pca_mean = self.pca_mean.to(dtype=torch.float32)
86
+ if self.pca_components is not None:
87
+ self.pca_components = self.pca_components.to(dtype=torch.float32)
88
 
89
  @property
90
  def is_regression(self) -> bool:
 
113
  outputs = outputs.unsqueeze(-1)
114
  return outputs
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  def run_batch(
117
  self, activations: torch.Tensor
118
  ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
 
151
 
152
  def _normalize_batch(self, batch: torch.Tensor) -> torch.Tensor:
153
  if self.scaler_mean is not None and self.scaler_std is not None:
154
+ mean, std = self.scaler_mean, self.scaler_std
 
155
  if mean.ndim != 1 or std.ndim != 1 or mean.shape[0] != batch.shape[1]:
156
  raise ValueError(
157
  "Probe scaler shape does not match activation hidden size: "
 
161
  safe_std = torch.where(std == 0, torch.ones_like(std), std)
162
  batch = (batch - mean) / safe_std
163
  if self.pca_mean is not None and self.pca_components is not None:
164
+ if self.pca_mean.ndim != 1 or self.pca_mean.shape[0] != batch.shape[1]:
 
 
165
  raise ValueError(
166
  "Probe PCA mean shape does not match activation hidden size: "
167
+ f"mean={tuple(self.pca_mean.shape)} batch={tuple(batch.shape)}"
168
  )
169
+ batch = (batch - self.pca_mean) @ self.pca_components.T
170
  return batch
171
 
172
 
utils/runtime.py CHANGED
@@ -113,7 +113,8 @@ def configured_ndif_api_key() -> str | None:
113
  def remote_backend(model: object, api_key: str | None = None, *, on_status=None):
114
  """Build an NDIF backend with credentials bound to one browser session."""
115
 
116
- from nnsight.intervention.backends.remote import JobStatusDisplay, RemoteBackend
 
117
 
118
  active_key = api_key or session_ndif_api_key() or configured_ndif_api_key()
119
  if not active_key:
@@ -121,24 +122,10 @@ def remote_backend(model: object, api_key: str | None = None, *, on_status=None)
121
 
122
  backend = RemoteBackend(model.to_model_key(), api_key=active_key)
123
  backend.CONNECT_TIMEOUT = 300.0
124
- if on_status is None:
125
- return backend
126
-
127
- class _CallbackJobStatusDisplay(JobStatusDisplay):
128
- def update(
129
- self,
130
- job_id: str = "",
131
- status_name: str = "",
132
- description: str = "",
133
- ):
134
- super().update(job_id, status_name, description)
135
- if status_name:
136
- on_status(job_id, status_name, description)
137
-
138
- backend.status_display = _CallbackJobStatusDisplay(
139
- enabled=True,
140
- verbose=backend.verbose,
141
- )
142
  return backend
143
 
144
 
 
113
  def remote_backend(model: object, api_key: str | None = None, *, on_status=None):
114
  """Build an NDIF backend with credentials bound to one browser session."""
115
 
116
+ from nnsight.intervention.backends.remote import RemoteBackend
117
+ from persona_vectors.activations import CallbackJobStatusDisplay
118
 
119
  active_key = api_key or session_ndif_api_key() or configured_ndif_api_key()
120
  if not active_key:
 
122
 
123
  backend = RemoteBackend(model.to_model_key(), api_key=active_key)
124
  backend.CONNECT_TIMEOUT = 300.0
125
+ if on_status is not None:
126
+ backend.status_display = CallbackJobStatusDisplay(
127
+ on_status, enabled=True, verbose=backend.verbose
128
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  return backend
130
 
131
 
uv.lock CHANGED
@@ -30,11 +30,11 @@ wheels = [
30
 
31
  [[package]]
32
  name = "aiohappyeyeballs"
33
- version = "2.6.1"
34
  source = { registry = "https://pypi.org/simple" }
35
- sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
36
  wheels = [
37
- { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
38
  ]
39
 
40
  [[package]]
@@ -229,11 +229,11 @@ wheels = [
229
 
230
  [[package]]
231
  name = "cachetools"
232
- version = "7.1.3"
233
  source = { registry = "https://pypi.org/simple" }
234
- sdist = { url = "https://files.pythonhosted.org/packages/8f/c1/67cfb86aa21144796ff51068326d467fbef8ee42f8d08a3a8a926106cf0c/cachetools-7.1.3.tar.gz", hash = "sha256:135cfe944bc3c1e805505f65dae0bef375a2f96261171ab66c79ef77d0bda39d", size = 45780, upload-time = "2026-05-18T18:21:03.819Z" }
235
  wheels = [
236
- { url = "https://files.pythonhosted.org/packages/68/52/8ff5c1a3b2e821ced9b2998fba3ee29aa4525c0bf51e5ee55dd6f61a4ed5/cachetools-7.1.3-py3-none-any.whl", hash = "sha256:9876787e2346e20584d5cca236cb5d49d04e7193de91646f230725b2e1e8b804", size = 16763, upload-time = "2026-05-18T18:21:02.386Z" },
237
  ]
238
 
239
  [[package]]
@@ -247,11 +247,11 @@ wheels = [
247
 
248
  [[package]]
249
  name = "certifi"
250
- version = "2026.4.22"
251
  source = { registry = "https://pypi.org/simple" }
252
- sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
253
  wheels = [
254
- { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
255
  ]
256
 
257
  [[package]]
@@ -343,14 +343,14 @@ wheels = [
343
 
344
  [[package]]
345
  name = "click"
346
- version = "8.4.0"
347
  source = { registry = "https://pypi.org/simple" }
348
  dependencies = [
349
  { name = "colorama", marker = "sys_platform == 'win32'" },
350
  ]
351
- sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" }
352
  wheels = [
353
- { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" },
354
  ]
355
 
356
  [[package]]
@@ -725,7 +725,7 @@ wheels = [
725
 
726
  [[package]]
727
  name = "huggingface-hub"
728
- version = "1.15.0"
729
  source = { registry = "https://pypi.org/simple" }
730
  dependencies = [
731
  { name = "filelock" },
@@ -738,18 +738,18 @@ dependencies = [
738
  { name = "typer" },
739
  { name = "typing-extensions" },
740
  ]
741
- sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" }
742
  wheels = [
743
- { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" },
744
  ]
745
 
746
  [[package]]
747
  name = "idna"
748
- version = "3.15"
749
  source = { registry = "https://pypi.org/simple" }
750
- sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
751
  wheels = [
752
- { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
753
  ]
754
 
755
  [[package]]
@@ -1608,7 +1608,7 @@ requires-dist = [
1608
  { name = "catppuccin", specifier = ">=2.5.0" },
1609
  { name = "datasets", specifier = ">=4.8.5" },
1610
  { name = "huggingface-hub", specifier = ">=1.14.0" },
1611
- { name = "persona-vectors", specifier = ">=0.8.6" },
1612
  { name = "plotly", specifier = ">=6.6.0" },
1613
  { name = "python-dotenv", specifier = ">=1.2.2" },
1614
  { name = "safetensors", specifier = ">=0.7.0" },
@@ -1620,7 +1620,7 @@ dev = [{ name = "pytest", specifier = ">=9.0.3" }]
1620
 
1621
  [[package]]
1622
  name = "persona-vectors"
1623
- version = "0.8.6"
1624
  source = { registry = "https://pypi.org/simple" }
1625
  dependencies = [
1626
  { name = "datasets" },
@@ -1639,9 +1639,9 @@ dependencies = [
1639
  { name = "transformers" },
1640
  { name = "umap-learn" },
1641
  ]
1642
- sdist = { url = "https://files.pythonhosted.org/packages/33/32/3f090e3c088ca11686e0fbe1a4686f040c347e3073fee4b9315a77ffb0a4/persona_vectors-0.8.6.tar.gz", hash = "sha256:6fcbc17d79e8d61e82df4f33f0b658b5816241c8fe7aabd9d0431bb89d163e84", size = 43651, upload-time = "2026-05-20T07:41:53.902Z" }
1643
  wheels = [
1644
- { url = "https://files.pythonhosted.org/packages/67/99/bd32e180eaf4a85897f1c6248dd79e86080d50743921ac5498133dbc291b/persona_vectors-0.8.6-py3-none-any.whl", hash = "sha256:60d7f34c30638814ed85029d6083059fb9f58727a6c110746c3f421623ce5e86", size = 53610, upload-time = "2026-05-20T07:41:54.989Z" },
1645
  ]
1646
 
1647
  [[package]]
@@ -2133,14 +2133,14 @@ wheels = [
2133
 
2134
  [[package]]
2135
  name = "python-engineio"
2136
- version = "4.13.1"
2137
  source = { registry = "https://pypi.org/simple" }
2138
  dependencies = [
2139
  { name = "simple-websocket" },
2140
  ]
2141
- sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" }
2142
  wheels = [
2143
- { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" },
2144
  ]
2145
 
2146
  [[package]]
@@ -2154,15 +2154,15 @@ wheels = [
2154
 
2155
  [[package]]
2156
  name = "python-socketio"
2157
- version = "5.16.1"
2158
  source = { registry = "https://pypi.org/simple" }
2159
  dependencies = [
2160
  { name = "bidict" },
2161
  { name = "python-engineio" },
2162
  ]
2163
- sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" }
2164
  wheels = [
2165
- { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" },
2166
  ]
2167
 
2168
  [package.optional-dependencies]
@@ -2672,15 +2672,15 @@ wheels = [
2672
 
2673
  [[package]]
2674
  name = "starlette"
2675
- version = "1.0.0"
2676
  source = { registry = "https://pypi.org/simple" }
2677
  dependencies = [
2678
  { name = "anyio" },
2679
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
2680
  ]
2681
- sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
2682
  wheels = [
2683
- { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
2684
  ]
2685
 
2686
  [[package]]
@@ -2882,7 +2882,7 @@ wheels = [
2882
 
2883
  [[package]]
2884
  name = "transformers"
2885
- version = "5.8.1"
2886
  source = { registry = "https://pypi.org/simple" }
2887
  dependencies = [
2888
  { name = "huggingface-hub" },
@@ -2895,9 +2895,9 @@ dependencies = [
2895
  { name = "tqdm" },
2896
  { name = "typer" },
2897
  ]
2898
- sdist = { url = "https://files.pythonhosted.org/packages/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz", hash = "sha256:4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", size = 8532019, upload-time = "2026-05-13T03:21:57.234Z" }
2899
  wheels = [
2900
- { url = "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl", hash = "sha256:5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", size = 10632882, upload-time = "2026-05-13T03:21:52.876Z" },
2901
  ]
2902
 
2903
  [[package]]
 
30
 
31
  [[package]]
32
  name = "aiohappyeyeballs"
33
+ version = "2.6.2"
34
  source = { registry = "https://pypi.org/simple" }
35
+ sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" }
36
  wheels = [
37
+ { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" },
38
  ]
39
 
40
  [[package]]
 
229
 
230
  [[package]]
231
  name = "cachetools"
232
+ version = "7.1.4"
233
  source = { registry = "https://pypi.org/simple" }
234
+ sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" }
235
  wheels = [
236
+ { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" },
237
  ]
238
 
239
  [[package]]
 
247
 
248
  [[package]]
249
  name = "certifi"
250
+ version = "2026.5.20"
251
  source = { registry = "https://pypi.org/simple" }
252
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
253
  wheels = [
254
+ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
255
  ]
256
 
257
  [[package]]
 
343
 
344
  [[package]]
345
  name = "click"
346
+ version = "8.4.1"
347
  source = { registry = "https://pypi.org/simple" }
348
  dependencies = [
349
  { name = "colorama", marker = "sys_platform == 'win32'" },
350
  ]
351
+ sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
352
  wheels = [
353
+ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
354
  ]
355
 
356
  [[package]]
 
725
 
726
  [[package]]
727
  name = "huggingface-hub"
728
+ version = "1.16.1"
729
  source = { registry = "https://pypi.org/simple" }
730
  dependencies = [
731
  { name = "filelock" },
 
738
  { name = "typer" },
739
  { name = "typing-extensions" },
740
  ]
741
+ sdist = { url = "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz", hash = "sha256:7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", size = 792534, upload-time = "2026-05-21T18:40:00.908Z" }
742
  wheels = [
743
+ { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" },
744
  ]
745
 
746
  [[package]]
747
  name = "idna"
748
+ version = "3.16"
749
  source = { registry = "https://pypi.org/simple" }
750
+ sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" }
751
  wheels = [
752
+ { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
753
  ]
754
 
755
  [[package]]
 
1608
  { name = "catppuccin", specifier = ">=2.5.0" },
1609
  { name = "datasets", specifier = ">=4.8.5" },
1610
  { name = "huggingface-hub", specifier = ">=1.14.0" },
1611
+ { name = "persona-vectors", specifier = ">=0.8.7" },
1612
  { name = "plotly", specifier = ">=6.6.0" },
1613
  { name = "python-dotenv", specifier = ">=1.2.2" },
1614
  { name = "safetensors", specifier = ">=0.7.0" },
 
1620
 
1621
  [[package]]
1622
  name = "persona-vectors"
1623
+ version = "0.8.7"
1624
  source = { registry = "https://pypi.org/simple" }
1625
  dependencies = [
1626
  { name = "datasets" },
 
1639
  { name = "transformers" },
1640
  { name = "umap-learn" },
1641
  ]
1642
+ sdist = { url = "https://files.pythonhosted.org/packages/25/92/2d8a021cabfb23cbc7fd79cc1140607e424283bae324177ee3c54c673a7d/persona_vectors-0.8.7.tar.gz", hash = "sha256:ffc92effdaecb38d06a107a6690c47ce0a891ae86fb3ffbfba390c9e2ca59714", size = 43667, upload-time = "2026-05-23T09:48:37.504Z" }
1643
  wheels = [
1644
+ { url = "https://files.pythonhosted.org/packages/29/51/976d72e4a98fce3cdd058862adb879d7400aff9459d2f4af9a9cffde8c33/persona_vectors-0.8.7-py3-none-any.whl", hash = "sha256:ba5a0fb3f81b2750b82960f07700c223f07c31bc215ae3ab67fbd1f9d200419f", size = 53636, upload-time = "2026-05-23T09:48:36.554Z" },
1645
  ]
1646
 
1647
  [[package]]
 
2133
 
2134
  [[package]]
2135
  name = "python-engineio"
2136
+ version = "4.13.2"
2137
  source = { registry = "https://pypi.org/simple" }
2138
  dependencies = [
2139
  { name = "simple-websocket" },
2140
  ]
2141
+ sdist = { url = "https://files.pythonhosted.org/packages/fa/6d/4384c2723adad93a3d6de4297e6d9c8b93be7f778a407f34f6ee0b2bea3e/python_engineio-4.13.2.tar.gz", hash = "sha256:a7732e99cfb7db6ed1aee31f18d7f73bbae086a92f31dee019bc646155d9684e", size = 79639, upload-time = "2026-05-21T21:45:07.578Z" }
2142
  wheels = [
2143
+ { url = "https://files.pythonhosted.org/packages/b7/28/180bfc5c95e83d40cb2abce512684ccad44e4819ec899fc36cb404a19061/python_engineio-4.13.2-py3-none-any.whl", hash = "sha256:8c101cd170e400dc4e970cd523325cde22df8fc25140953f379327055d701a6b", size = 59993, upload-time = "2026-05-21T21:45:06.162Z" },
2144
  ]
2145
 
2146
  [[package]]
 
2154
 
2155
  [[package]]
2156
  name = "python-socketio"
2157
+ version = "5.16.2"
2158
  source = { registry = "https://pypi.org/simple" }
2159
  dependencies = [
2160
  { name = "bidict" },
2161
  { name = "python-engineio" },
2162
  ]
2163
+ sdist = { url = "https://files.pythonhosted.org/packages/07/dd/6fd4112b941f7d39b8171b6ba17902609bd8fa2059c3812a3c29dade13e7/python_socketio-5.16.2.tar.gz", hash = "sha256:ad88c228d921646efa436c0a0df217e364ef30ec072df4041484e54d49c15989", size = 128011, upload-time = "2026-05-21T22:03:44.418Z" }
2164
  wheels = [
2165
+ { url = "https://files.pythonhosted.org/packages/72/dc/0decaf5da92a7a969374474025787102d811d42aed1d32191fa338620e15/python_socketio-5.16.2-py3-none-any.whl", hash = "sha256:bef2da3374fd533aed4297f57b4f6512b52aa51604cb0da2165f401291c5ca20", size = 82137, upload-time = "2026-05-21T22:03:42.616Z" },
2166
  ]
2167
 
2168
  [package.optional-dependencies]
 
2672
 
2673
  [[package]]
2674
  name = "starlette"
2675
+ version = "1.0.1"
2676
  source = { registry = "https://pypi.org/simple" }
2677
  dependencies = [
2678
  { name = "anyio" },
2679
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
2680
  ]
2681
+ sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" }
2682
  wheels = [
2683
+ { url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" },
2684
  ]
2685
 
2686
  [[package]]
 
2882
 
2883
  [[package]]
2884
  name = "transformers"
2885
+ version = "5.9.0"
2886
  source = { registry = "https://pypi.org/simple" }
2887
  dependencies = [
2888
  { name = "huggingface-hub" },
 
2895
  { name = "tqdm" },
2896
  { name = "typer" },
2897
  ]
2898
+ sdist = { url = "https://files.pythonhosted.org/packages/51/58/7f843608f2e8421f86bb97060b54649be6239ec612b82bf9d41e65c26c00/transformers-5.9.0.tar.gz", hash = "sha256:25997cb8fa6053533171634b6162d7df54346530ec2aa9b42bb834e63668c842", size = 8642240, upload-time = "2026-05-20T14:50:49.278Z" }
2899
  wheels = [
2900
+ { url = "https://files.pythonhosted.org/packages/02/ca/2eaa5359f2ccb8c2e1656bc26305ad0cf438aa392ce4b29ae67a315c186e/transformers-5.9.0-py3-none-any.whl", hash = "sha256:1d19509bcff7028ebc6b277d71caa712e8353778463d38764237d14b42b52788", size = 10787648, upload-time = "2026-05-20T14:50:45.337Z" },
2901
  ]
2902
 
2903
  [[package]]