Spaces:
Paused
Paused
File size: 37,164 Bytes
03e863f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 | """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})"
|