"""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})"