mindvisualizer-live / src /brain_state.py
Pixedar's picture
Deploy full MindVisualizer Python runtime
03e863f
Raw
History Blame Contribute Delete
37.2 kB
"""Brain region state database.
Stores the current cognitive/functional state of each brain region. States can be:
- Auto-initialized from a global brain state description via GPT
- Manually edited per-region
- Cleared and re-initialized at any time
- Persisted to disk as JSON
Used by the probe system to simulate information propagation: when a probe
enters a region, the state can be modified and propagated to downstream regions.
"""
import json
import os
import sys
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout
from pathlib import Path
from typing import Optional
def _ensure_ssl():
"""Fix SSL cert path for environments where Git overrides it.
Must run BEFORE importing openai/langchain so the httpx client
picks up the correct certificate.
"""
try:
import certifi
cert_file = certifi.where()
cur = os.environ.get("SSL_CERT_FILE", "")
if not cur or not os.path.isfile(cur):
os.environ["SSL_CERT_FILE"] = cert_file
cur2 = os.environ.get("REQUESTS_CA_BUNDLE", "")
if not cur2 or not os.path.isfile(cur2):
os.environ["REQUESTS_CA_BUNDLE"] = cert_file
except ImportError:
pass
# Fix SSL before any network library is imported
_ensure_ssl()
import openai
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
DEFAULT_STATE_FILE = Path(__file__).resolve().parent.parent / "data" / "brain_states.json"
DEFAULT_MODEL = "gpt-5.4-mini"
HQ_MODEL = "gpt-5.4"
class BrainStateDB:
"""Database of brain region states, persisted to JSON."""
def __init__(self, path: Path | str = DEFAULT_STATE_FILE, model: str = DEFAULT_MODEL,
debug: bool = False):
self.path = Path(path)
self.model = model
self.debug = debug
self.states: dict[str, str] = {} # region_name -> state description
self.global_state: str = "" # the overall brain state description
if self.path.exists():
self.load()
def _debug_prompt(self, method_name: str, prompt_text: str):
"""Print the full prompt if debug mode is enabled."""
if self.debug:
print(f"\n{'='*60}")
print(f"[DEBUG PROMPT] {method_name}")
print(f"{'='*60}")
print(prompt_text)
print(f"{'='*60}\n")
sys.stdout.flush()
# ---------- persistence ----------
def load(self):
"""Load states from JSON file."""
try:
data = json.loads(self.path.read_text(encoding="utf-8"))
self.states = data.get("states", {})
self.global_state = data.get("global_state", "")
print(f"[brain-state] loaded {len(self.states)} region states")
except Exception as e:
print(f"[brain-state] could not load {self.path}: {e}")
def save(self):
"""Save states to JSON file."""
data = {
"global_state": self.global_state,
"states": self.states,
}
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False),
encoding="utf-8")
print(f"[brain-state] saved {len(self.states)} region states to {self.path}")
def clear(self):
"""Clear all states."""
self.states.clear()
self.global_state = ""
if self.path.exists():
self.path.unlink()
print("[brain-state] cleared all states")
# ---------- access ----------
def get(self, region_name: str) -> str:
return self.states.get(region_name, "")
def set(self, region_name: str, state: str):
self.states[region_name] = state
def has_states(self) -> bool:
return bool(self.states)
# ---------- GPT initialization ----------
def initialize_from_global(self, global_state: str, region_names: list[str],
callback=None) -> str:
"""Use GPT to generate states for all regions from a global brain state.
Args:
global_state: e.g. "someone thinking about loved ones" or "" for GPT to choose
region_names: list of region names to generate states for
callback: optional fn(status_msg) for progress updates
Returns:
Summary of initialization.
"""
_ensure_ssl()
load_dotenv()
if not global_state:
global_state = "a resting state with spontaneous mind-wandering"
self.global_state = global_state
# Build region list (batch into groups of ~30 for efficiency)
all_states = {}
batch_size = 30
for i in range(0, len(region_names), batch_size):
batch = region_names[i:i + batch_size]
if callback:
callback(f"Generating states for regions {i+1}-{i+len(batch)}/{len(region_names)}...")
region_list = "\n".join(f"- {name}" for name in batch)
prompt = ChatPromptTemplate.from_template(
"""You are a neuroscience expert. The brain is currently in this overall state:
"{global_state}"
For each brain region below, write a SHORT (1-2 sentences) specific description of
what this region is likely doing right now given the overall brain state.
IMPORTANT RULES:
- Be SPECIFIC about the actual cognitive content, not generic function descriptions
- Don't force every region to match the global state — some regions may be doing
their own thing (e.g., sensory processing, homeostasis) independent of the global state
- First consider what the region generally does, then derive what it's specifically
doing in this context
- Focus on CURRENT ACTIVITY, not general capabilities
Regions:
{regions}
Reply as JSON object mapping region name to state description. Example:
{{"Amygdala (AMY)": "Low-level monitoring for threats; no active fear processing", ...}}
Only output valid JSON, nothing else."""
)
llm = ChatOpenAI(model=self.model, temperature=0.4, max_tokens=4000)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("initialize_from_global",
prompt.format(global_state=global_state, regions=region_list))
try:
result = chain.invoke({
"global_state": global_state,
"regions": region_list,
})
# Parse JSON from response
result = result.strip()
if result.startswith("```"):
result = result.split("\n", 1)[1].rsplit("```", 1)[0]
parsed = json.loads(result)
all_states.update(parsed)
# Show each region's state in real time
for rname, rstate in parsed.items():
if callback:
callback(f" {rname}: {rstate}")
except Exception as e:
print(f"[brain-state] GPT batch error: {e}")
for name in batch:
all_states[name] = f"(initialization failed: {e})"
self.states = all_states
self.save()
return f"Initialized {len(self.states)} region states for: {global_state}"
def validate_perturbation(self, region_name: str, perturbation: str) -> dict:
"""Check if a perturbation is appropriate for a brain region.
Returns:
dict with keys:
- valid (bool): True if perturbation is plausible
- region_function (str): what this region does
- suggestion (str): suggested alternative if not valid
- warning (str): warning message if perturbation is questionable
"""
_ensure_ssl()
load_dotenv()
current = self.states.get(region_name, "unknown state")
prompt = ChatPromptTemplate.from_template(
"""You are a neuroscience expert. Evaluate whether this perturbation makes sense
for the specified brain region.
Region: {region}
Current state: {current}
Requested perturbation: "{perturbation}"
Consider:
1. What does this brain region actually do? (its primary functions)
2. Is the requested perturbation something this region CAN process?
3. If not, what WOULD be an appropriate perturbation for this region?
Reply as JSON:
{{
"valid": true/false,
"region_function": "brief description of what this region does",
"warning": "warning if questionable (empty string if fine)",
"suggestion": "suggested alternative perturbation if invalid (empty string if valid)"
}}
Only output valid JSON."""
)
llm = ChatOpenAI(model=self.model, temperature=0.2, max_tokens=300)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("validate_perturbation",
prompt.format(region=region_name, current=current,
perturbation=perturbation))
try:
result = chain.invoke({
"region": region_name,
"current": current,
"perturbation": perturbation,
}).strip()
if result.startswith("```"):
result = result.split("\n", 1)[1].rsplit("```", 1)[0]
return json.loads(result)
except Exception as e:
return {"valid": True, "region_function": "unknown",
"warning": f"(validation failed: {e})", "suggestion": ""}
def propose_perturbations(self, region_name: str) -> list[str]:
"""Ask GPT to propose 4 plausible perturbations for a brain region.
Uses the openai SDK directly (not langchain) for reliability in
background threads.
Args:
region_name: the region to perturb
Returns:
List of 4 perturbation description strings.
"""
import sys
_defaults = [
"Heightened activity in this region",
"Suppressed activity in this region",
"Shift to an alternative processing mode",
"Disrupted connectivity with downstream regions",
]
_ensure_ssl()
load_dotenv()
current = self.states.get(region_name, "unknown state")
# Use a shorter name for prompt if the full name is very long
short_name = region_name
if len(short_name) > 80 and "(" in short_name:
short_name = short_name.split("(", 1)[1].rstrip(")")
api_key = os.environ.get("OPENAI_API_KEY", "")
if not api_key:
print("[brain-state] OPENAI_API_KEY not set, using defaults")
sys.stdout.flush()
return _defaults
print(f"[brain-state] propose_perturbations: calling OpenAI for '{short_name}'...")
sys.stdout.flush()
try:
client = openai.OpenAI(api_key=api_key, timeout=30.0)
msg_content = (
f"You are a neuroscience expert. A user wants to perturb a brain region "
f"in a resting-state simulation.\n\n"
f"Region: {short_name}\n"
f"Current state: \"{current}\"\n\n"
f"Based on the region's known functions and its current state, propose "
f"exactly 4 different plausible ways this region's state could change. "
f"Each should be specific to this region's actual function, a realistic "
f"state change, described in 1 short sentence, and diverse from each other.\n\n"
f"Reply as a JSON array of exactly 4 strings:\n"
f'["perturbation 1", "perturbation 2", "perturbation 3", "perturbation 4"]\n'
f"Only output valid JSON."
)
self._debug_prompt("propose_perturbations", msg_content)
print(f"[brain-state] propose_perturbations: sending API request...")
sys.stdout.flush()
def _call():
return client.chat.completions.create(
model=self.model,
temperature=0.5,
max_completion_tokens=400,
messages=[{"role": "user", "content": msg_content}],
)
with ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(_call)
try:
response = future.result(timeout=30)
except FutureTimeout:
print("[brain-state] propose_perturbations: TIMEOUT after 30s")
sys.stdout.flush()
return _defaults
result = response.choices[0].message.content.strip()
print(f"[brain-state] propose_perturbations: got response, parsing...")
sys.stdout.flush()
if result.startswith("```"):
result = result.split("\n", 1)[1].rsplit("```", 1)[0]
proposals = json.loads(result)
if isinstance(proposals, list) and len(proposals) >= 4:
return proposals[:4]
return proposals if isinstance(proposals, list) else _defaults
except Exception as e:
print(f"[brain-state] proposal error: {type(e).__name__}: {e}")
sys.stdout.flush()
import traceback
traceback.print_exc()
sys.stdout.flush()
return _defaults
def alter_region_state(self, region_name: str, modification: str,
callback=None, skip_validation=False) -> str:
"""Use GPT to alter a specific region's state.
Args:
region_name: the region to modify
modification: description of how to change it
callback: optional fn(status_msg) for progress
skip_validation: skip perturbation validation
Returns:
The new state description.
"""
_ensure_ssl()
load_dotenv()
# Validate perturbation first
if not skip_validation:
if callback:
callback(f"Validating perturbation for {region_name}...")
validation = self.validate_perturbation(region_name, modification)
if callback:
callback(f"Region function: {validation.get('region_function', '?')}")
if not validation.get("valid", True):
warning = validation.get("warning", "")
suggestion = validation.get("suggestion", "")
msg = f"[WARNING] Perturbation may not be appropriate for {region_name}."
if warning:
msg += f"\n Reason: {warning}"
if suggestion:
msg += f"\n Suggestion: {suggestion}"
if callback:
callback(msg)
print(msg)
# Still proceed but note the warning
elif validation.get("warning"):
if callback:
callback(f"Note: {validation['warning']}")
current = self.states.get(region_name, "unknown state")
prompt = ChatPromptTemplate.from_template(
"""You are a neuroscience expert. A brain region's state needs to be modified.
Region: {region}
Current state: {current}
Modification requested: {modification}
Write a new SHORT (1-2 sentences) specific state description that incorporates
the requested modification while staying neuroscientifically plausible.
Only output the new state description, nothing else."""
)
llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=200)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("alter_region_state",
prompt.format(region=region_name, current=current,
modification=modification))
try:
new_state = chain.invoke({
"region": region_name,
"current": current,
"modification": modification,
}).strip()
self.states[region_name] = new_state
self.save()
return new_state
except Exception as e:
return f"(alter failed: {e})"
def propagate_through_graph(self, source_region: str,
connections: list[dict],
A, # numpy ndarray (R x R)
roi_names: list[str],
callback=None) -> dict[str, str]:
"""Propagate perturbation through the connectivity graph, depth by depth.
For each affected region individually:
1. Determine if the incoming signal is strong or weak (relative to that
region's other connections)
2. Find additional un-perturbed regions strongly connected to this target
for context
3. Ask GPT: given region X's function, its previous state, the incoming
signal from Y (strong/weak), and contextual connections — what does
X's state change to?
Processes depth-1 targets first, then depth-2 using already-updated states,
so the signal cascades realistically.
Args:
source_region: name of the perturbed region
connections: list of dicts from get_strongest_connections, each with
target_idx, weight, abs_weight, depth, source_idx
A: full connectivity matrix (R x R numpy array)
roi_names: list of ROI names matching A's indices
callback: optional fn(status_msg)
Returns:
Dict of {region_name: new_state} for all updated regions.
"""
import numpy as np
_ensure_ssl()
load_dotenv()
llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=300)
# Group connections by depth
by_depth: dict[int, list[dict]] = {}
for c in connections:
by_depth.setdefault(c["depth"], []).append(c)
# Build name->index map
name_to_idx = {n: i for i, n in enumerate(roi_names)}
# Track before/after states and which regions were updated
before_states: dict[str, str] = {}
all_updates: dict[str, str] = {}
perturbed_idxs = set()
source_idx = name_to_idx.get(source_region)
if source_idx is not None:
perturbed_idxs.add(source_idx)
# Diagonal-zeroed matrix for connection lookups
Ac = np.array(A, dtype=np.float32, copy=True)
np.fill_diagonal(Ac, 0.0)
# Process depth by depth
max_depth = max(by_depth.keys()) if by_depth else 0
for d in range(1, max_depth + 1):
depth_conns = by_depth.get(d, [])
if not depth_conns:
continue
if callback:
callback(f"Propagating depth {d}: {len(depth_conns)} regions...")
for c in depth_conns:
ti = c["target_idx"]
target_name = roi_names[ti]
from_idx = c["source_idx"]
from_name = roi_names[from_idx]
# Skip if target has no state
target_prev_state = self.states.get(target_name, "")
if not target_prev_state:
target_prev_state = "unknown / not initialized"
before_states[target_name] = target_prev_state
# Get the incoming source's current state (may have been
# updated in a previous depth iteration)
from_state = all_updates.get(from_name,
self.states.get(from_name, "unknown"))
# ---- Determine signal strength and type ----
# Compare this connection weight to the target's other incoming
# connections (row ti of A)
incoming_weights = np.abs(Ac[ti, :])
incoming_weights[ti] = 0 # no self
median_incoming = float(np.median(incoming_weights[incoming_weights > 0])) \
if np.any(incoming_weights > 0) else 0.001
conn_abs_w = c["abs_weight"]
if conn_abs_w > median_incoming * 2.0:
strength_label = "STRONG (well above average)"
elif conn_abs_w > median_incoming * 0.8:
strength_label = "moderate"
else:
strength_label = "weak (below average)"
# Determine excitatory vs inhibitory based on connection sign
raw_weight = c.get("weight", conn_abs_w)
if raw_weight < 0:
sign_label = "INHIBITORY (negative connection weight)"
else:
sign_label = "EXCITATORY (positive connection weight)"
# ---- Find context: other strong un-perturbed connections ----
context_lines = []
top_others = np.argsort(incoming_weights)[::-1][:10]
for oi in top_others:
if int(oi) == from_idx or int(oi) in perturbed_idxs:
continue
oi = int(oi)
other_name = roi_names[oi]
other_state = self.states.get(other_name, "")
if not other_state:
continue
other_w = float(incoming_weights[oi])
if other_w < conn_abs_w * 0.3:
break # only include meaningfully strong ones
context_lines.append(
f"- {other_name} (connection weight {other_w:.4f}): "
f"state = \"{other_state}\""
)
if len(context_lines) >= 3:
break
context_text = ""
if context_lines:
context_text = (
"\n\nAdditional context — other strong connections to this region "
"(NOT perturbed, their states remain stable):\n"
+ "\n".join(context_lines)
)
# ---- GPT call for this single region ----
prompt = ChatPromptTemplate.from_template(
"""You are a computational neuroscientist analyzing intrinsic information flow in a resting-state brain network (rs-fMRI effective connectivity).
TARGET REGION: {target}
TARGET PREVIOUS STATE: {target_state}
INCOMING SIGNAL FROM: {source}
INCOMING SIGNAL STRENGTH: {strength} (connection weight: {weight:.4f})
CONNECTION TYPE: {sign}
SOURCE REGION'S CURRENT STATE: "{source_state}"
{context}
CRITICAL INSTRUCTIONS:
1. NO SEMANTIC ECHOING: Do not simply copy the semantic concept of the source region. You must TRANSLATE the incoming signal into the strict anatomical and functional domain of the TARGET REGION. If the source is about "visual beauty", the motor cortex should NOT start "appreciating beauty" — it should show changes in motor readiness or postural tone.
2. RESTING-STATE CONTEXT: The connectivity data reflects intrinsic resting-state dynamics. Information flow here represents spontaneous internal cognition or modulation of the target region's resting equilibrium.
3. INHIBITORY vs EXCITATORY: If the connection is INHIBITORY, the incoming signal SUPPRESSES or DAMPENS the target region's activity. If EXCITATORY, it AMPLIFIES or FACILITATES the target's function. This fundamentally changes the nature of the state change.
4. INTRINSIC DYNAMICS: Focus on how the target region's OWN function shifts, not on relaying the source's content.
5. Output ONLY the precise description of the target region's new state (1-2 sentences). No conversational filler.
Note: connection weights are directed influence estimates from resting-state fMRI — interpret propagation as the most likely influence pattern, not literal neural signal timing."""
)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("propagate_through_graph",
prompt.format(target=target_name,
target_state=target_prev_state,
strength=strength_label,
sign=sign_label,
source=from_name,
source_state=from_state,
weight=c["abs_weight"],
context=context_text))
try:
new_state = chain.invoke({
"target": target_name,
"target_state": target_prev_state,
"strength": strength_label,
"sign": sign_label,
"source": from_name,
"source_state": from_state,
"weight": c["abs_weight"],
"context": context_text,
}).strip()
all_updates[target_name] = new_state
perturbed_idxs.add(ti)
if callback:
callback(f" {target_name}: {new_state}")
except Exception as e:
print(f"[brain-state] propagation error for {target_name}: {e}")
# Record source before state too
before_states[source_region] = self.states.get(source_region, "unknown")
# Apply all updates
for name, new_state in all_updates.items():
self.states[name] = new_state
self.save()
# Attach before_states so summarize_changes can use them
self._last_before_states = before_states
return all_updates
def propagate_through_regions(self, source_region: str, affected_regions: list[str],
flow_strengths: dict[str, float] | None = None,
callback=None) -> dict[str, str]:
"""Simpler propagation for MDN flow mode (no connectivity matrix).
Uses flow strengths as a proxy for connection weights. Processes regions
in order of the flow path (which is already serial).
"""
_ensure_ssl()
load_dotenv()
llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=300)
source_state = self.states.get(source_region, "unknown")
all_updates = {}
before_states = {source_region: source_state}
# Process regions in flow order (serial propagation)
prev_name = source_region
prev_state = source_state
if flow_strengths and affected_regions:
all_strengths = [flow_strengths.get(n, 0.0) for n in affected_regions]
max_s = max(all_strengths) if all_strengths else 1.0
else:
max_s = 1.0
for name in affected_regions:
target_state = self.states.get(name, "unknown")
before_states[name] = target_state
strength = flow_strengths.get(name, 0.0) if flow_strengths else 0.0
if max_s > 0:
rel_strength = strength / max_s
if rel_strength > 0.6:
strength_label = "STRONG"
elif rel_strength > 0.3:
strength_label = "moderate"
else:
strength_label = "weak"
else:
strength_label = "moderate"
prompt = ChatPromptTemplate.from_template(
"""You are a computational neuroscientist analyzing intrinsic information flow in a resting-state brain network.
TARGET REGION: {target}
TARGET PREVIOUS STATE: {target_state}
INCOMING SIGNAL FROM: {source}
INCOMING SIGNAL STRENGTH: {strength}
SOURCE REGION'S CURRENT STATE: "{source_state}"
CRITICAL INSTRUCTIONS:
1. NO SEMANTIC ECHOING: Do not simply copy the semantic concept of the source region. You must TRANSLATE the incoming signal into the strict anatomical and functional domain of the TARGET REGION ({target}). For example, if the source is a visual area processing "edge detection" and the target is a motor area, do NOT say the motor area is doing "edge detection" — describe how the motor area's OWN function shifts in response.
2. RESTING-STATE CONTEXT: This is intrinsic resting-state dynamics, not task-driven activity. Describe subtle modulations, not dramatic activations.
3. INTRINSIC DYNAMICS: Focus on how {target}'s OWN function shifts given the incoming signal. The target region does what IT does, influenced by the source — not what the source does.
Note: signal strength here is a proxy from the flow field magnitude, not a measured biological quantity. Interpret as the most likely direction of influence, not neural propagation timing.
Output ONLY the precise description of {target}'s new state (1-2 sentences). No labels, no prefixes."""
)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("propagate_through_regions",
prompt.format(target=name, target_state=target_state,
strength=strength_label, source=prev_name,
source_state=prev_state))
try:
new_state = chain.invoke({
"target": name,
"target_state": target_state,
"strength": strength_label,
"source": prev_name,
"source_state": prev_state,
}).strip()
all_updates[name] = new_state
if callback:
callback(f" {name}: {new_state}")
# Next hop uses this updated state as source
prev_name = name
prev_state = new_state
except Exception as e:
print(f"[brain-state] propagation error for {name}: {e}")
# Apply
for name, new_state in all_updates.items():
self.states[name] = new_state
self.save()
self._last_before_states = before_states
return all_updates
def summarize_changes(self, updates: dict[str, str],
source_region: str) -> str:
"""Summarize what changed: show before/after states (without revealing the
perturbation) and ask GPT to build a coherent picture.
Args:
updates: dict of {region_name: new_state}
source_region: the region that was perturbed
Returns:
Human-readable coherent summary.
"""
_ensure_ssl()
load_dotenv()
# Get before states (saved by propagation methods)
before = getattr(self, '_last_before_states', {})
# Build separate before and after state maps
all_regions = [source_region] + [n for n in updates if n != source_region]
before_lines = []
after_lines = []
for name in all_regions:
prev = before.get(name, "unknown")
current = self.states.get(name, updates.get(name, "unknown"))
before_lines.append(f"- {name}: \"{prev}\"")
after_lines.append(f"- {name}: \"{current}\"")
llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=800)
prompt = ChatPromptTemplate.from_template(
"""You are a network neuroscientist analyzing a macroscopic shift in resting-state brain activity based on effective connectivity changes.
INITIAL BRAIN STATE MAP:
{comparisons_before}
POST-PROPAGATION BRAIN STATE MAP:
{comparisons_after}
CRITICAL INSTRUCTIONS:
1. DO NOT list the regions or compare them one by one.
2. This is a resting-state brain network — interpret changes as shifts in intrinsic functional organization, not external stimulus-response narratives.
3. FOCUS ON INTRINSIC STATES: Synthesize this data into ONE coherent paragraph explaining the overall shift in the subject's internal cognitive, emotional, or physiological baseline.
4. NETWORK LEVEL INTEGRATION: Identify the broad functional domains driving the new equilibrium and describe the holistic network-level transition based purely on the provided state changes.
Note: the underlying connectivity is directed effective connectivity from resting-state fMRI — read the overall shift as a change in inter-regional influence patterns, not a literal neural event cascade.
Provide your coherent resting-state network insight below:"""
)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("summarize_changes",
prompt.format(comparisons_before="\n".join(before_lines),
comparisons_after="\n".join(after_lines)))
try:
return chain.invoke({
"comparisons_before": "\n".join(before_lines),
"comparisons_after": "\n".join(after_lines),
}).strip()
except Exception as e:
return f"(summary failed: {e})"
def generate_flow_story(self, updates: dict[str, str],
source_region: str,
connections: list[dict] | None = None) -> str:
"""Generate a narrative story of how information flowed through the brain.
Unlike summarize_changes (which gives a holistic snapshot), this tells
the story of the signal's journey: where it started, how each region
processed and transformed it, and what the downstream effects were.
Args:
updates: dict of {region_name: new_state}
source_region: the origin region
connections: optional list of connection dicts (with depth, weight info)
Returns:
A narrative paragraph describing the information flow journey.
"""
_ensure_ssl()
load_dotenv()
before = getattr(self, '_last_before_states', {})
# Build ordered flow description
flow_steps = []
source_before = before.get(source_region, "unknown")
source_after = self.states.get(source_region,
updates.get(source_region, "unknown"))
flow_steps.append(
f"ORIGIN — {source_region}: \"{source_before}\" -> \"{source_after}\""
)
# Order by depth if connections available
if connections:
by_depth: dict[int, list] = {}
for c in connections:
name = c.get("target_name", "")
if not name and "target_idx" in c:
continue
by_depth.setdefault(c.get("depth", 1), []).append(c)
for d in sorted(by_depth.keys()):
for c in by_depth[d]:
name = c.get("target_name", "")
if name in updates:
prev = before.get(name, "unknown")
sign = "excitatory" if c.get("weight", 0) >= 0 else "inhibitory"
flow_steps.append(
f"DEPTH {d} ({sign}) — {name}: \"{prev}\" -> \"{updates[name]}\""
)
else:
for name, new_state in updates.items():
if name == source_region:
continue
prev = before.get(name, "unknown")
flow_steps.append(f"STEP — {name}: \"{prev}\" -> \"{new_state}\"")
flow_text = "\n".join(flow_steps)
llm = ChatOpenAI(model=self.model, temperature=0.4, max_tokens=800)
prompt = ChatPromptTemplate.from_template(
"""You are a science writer narrating how a signal traveled through a resting-state brain network.
SIGNAL FLOW PATH (in order of propagation):
{flow_path}
Write a SHORT narrative story (2-3 paragraphs) of how the information traveled through the brain:
- Start with where the signal originated and what it carried
- Describe how each region it reached processed and TRANSFORMED the signal according to its own function
- Highlight how the signal's meaning changed as it moved through different functional domains
- End with the overall effect on the brain's resting state
RULES:
- Do NOT list regions mechanically — weave them into a flowing narrative
- Use concrete, vivid language about what each region actually does
- Show how the signal was transformed at each hop, not just passed along
- Keep it grounded in neuroscience but accessible to a general audience"""
)
chain = prompt | llm | StrOutputParser()
self._debug_prompt("generate_flow_story",
prompt.format(flow_path=flow_text))
try:
return chain.invoke({"flow_path": flow_text}).strip()
except Exception as e:
return f"(story generation failed: {e})"