"""Scripture Knowledge Graph — Gradio app for Hugging Face Spaces. NLP entity extraction, NetworkX graph algorithms, an Ant Colony System swarm, and server-side streaming Claude insights, wired to a D3.js visualization injected via gr.HTML. """ import json import os import random import re from collections import defaultdict import anthropic import gradio as gr import networkx as nx # ═══════════════════════════════════════════════════════════════════════════ # Entity dictionaries # ═══════════════════════════════════════════════════════════════════════════ PEOPLE = { "Jesus", "Christ", "God", "Yahweh", "Jehovah", "Holy Spirit", "Spirit", "Lord", "Father", "Paul", "Peter", "John", "James", "Mary", "Moses", "Abraham", "David", "Solomon", "Isaiah", "Jeremiah", "Ezekiel", "Daniel", "Elijah", "Elisha", "Joshua", "Samuel", "Ruth", "Naomi", "Esther", "Mordecai", "Job", "Adam", "Eve", "Noah", "Jacob", "Israel", "Joseph", "Isaac", "Rebekah", "Rachel", "Leah", "Benjamin", "Judah", "Reuben", "Levi", "Saul", "Jonathan", "Barnabas", "Timothy", "Silas", "Luke", "Mark", "Matthew", "Stephen", "Philip", "Andrew", "Thomas", "Bartholomew", "Thaddaeus", "Simon", "Judas", "Iscariot", "Lazarus", "Martha", "Mary Magdalene", "Herod", "Pilate", "Satan", "Nicodemus", "Zacchaeus", "Cornelius", "Lydia", "Priscilla", "Aquila", "Apollos", "Titus", "Philemon", "Onesimus", "Gideon", "Deborah", "Barak", "Samson", "Delilah", "Boaz", "Hannah", "Eli", "Nathan", "Bathsheba", "Absalom", "Rehoboam", "Jeroboam", "Ahab", "Jezebel", "Elihu", "Amos", "Hosea", "Joel", "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", "Ezra", "Nehemiah", "Melchizedek", "Enoch", "Methuselah", "Lamech", "Cain", "Abel", "Seth", "Terah", "Lot", "Hagar", "Ishmael", "Laban", "Dinah", "Tamar", "Er", "Onan", "Perez", "Pharaoh", "Aaron", "Miriam", "Caleb", "Balaam", "Rahab", "Achan", "Othniel", "Ehud", "Shamgar", "Tola", "Jair", "Jephthah", "Ibzan", "Elon", "Abdon", "Uriah", "Joab", "Amnon", "Adonijah", "Hiram", "Asa", "Jehoshaphat", "Josiah", "Hezekiah", "Manasseh", "Amon", "Jehoiakim", "Zedekiah", "Nebuchadnezzar", "Cyrus", "Darius", "Shadrach", "Meshach", "Abednego", "Belshazzar", "Haman", "Vashti", "Tobiah", "Sanballat", "Simeon", "Anna", "Zechariah the priest", "Elizabeth", "Gabriel", "Michael", "Herod Antipas", "Herodias", "Salome", "Caiaphas", "Annas", "Barabbas", "Simon of Cyrene", "Joseph of Arimathea", "Nathanael", "Matthias", "Ananias", "Sapphira", "Gamaliel", "Dorcas", "Tabitha", "Rhoda", "Felix", "Festus", "Agrippa", "Sosthenes", "Crispus", "Erastus", "Gaius", "Tertius", "Phoebe", "Junia", "Andronicus", "Epaphras", "Epaphroditus", "Demas", "Tychicus", "Trophimus", "Archippus", "Clement", "Diotrephes", "Demetrius", "Cephas", } PLACES = { "Jerusalem", "Bethlehem", "Nazareth", "Galilee", "Judea", "Israel", "Egypt", "Babylon", "Rome", "Corinth", "Ephesus", "Antioch", "Athens", "Philippi", "Thessalonica", "Colossae", "Galatia", "Samaria", "Jordan", "Sinai", "Canaan", "Eden", "Zion", "Capernaum", "Bethsaida", "Jericho", "Hebron", "Bethany", "Gethsemane", "Calvary", "Golgotha", "Mount Sinai", "Mount Zion", "Mount Carmel", "Mount of Olives", "Dead Sea", "Red Sea", "Sea of Galilee", "Mediterranean", "Persia", "Assyria", "Syria", "Macedonia", "Crete", "Cyprus", "Malta", "Arabia", "Moab", "Edom", "Ammon", "Midian", "Philistia", "Gaza", "Ashkelon", "Ashdod", "Gath", "Ekron", "Shiloh", "Gilgal", "Shechem", "Bethel", "Dan", "Beersheba", "Tyre", "Sidon", "Damascus", "Nineveh", "Ur", "Haran", "Padan Aram", "Goshen", "Rameses", "Kadesh Barnea", "Mount Nebo", "Jabbok", "Peniel", "Ai", "Gibeon", "Ramah", "Mizpah", "Gilead", "Bashan", "Perga", "Lystra", "Derbe", "Iconium", "Troas", "Berea", "Cenchreae", "Miletus", "Patmos", "Laodicea", "Smyrna", "Pergamum", "Thyatira", "Sardis", "Philadelphia", "Caesarea", "Joppa", "Emmaus", "Cana", "Tiberias", "Decapolis", "Perea", "Idumea", "Susa", "Ecbatana", "Chaldea", } BOOK_ORDER = [ "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra", "Nehemiah", "Esther", "Job", "Psalms", "Proverbs", "Ecclesiastes", "Song of Songs", "Isaiah", "Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", "Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians", "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter", "1 John", "2 John", "3 John", "Jude", "Revelation", ] BOOK_ABBREVIATIONS = { "gen": "Genesis", "gn": "Genesis", "ex": "Exodus", "exod": "Exodus", "lev": "Leviticus", "lv": "Leviticus", "num": "Numbers", "nm": "Numbers", "deut": "Deuteronomy", "dt": "Deuteronomy", "josh": "Joshua", "judg": "Judges", "jdg": "Judges", "ruth": "Ruth", "1 sam": "1 Samuel", "2 sam": "2 Samuel", "1 kgs": "1 Kings", "2 kgs": "2 Kings", "1 chr": "1 Chronicles", "2 chr": "2 Chronicles", "ezra": "Ezra", "neh": "Nehemiah", "esth": "Esther", "job": "Job", "ps": "Psalms", "psa": "Psalms", "psalm": "Psalms", "prov": "Proverbs", "eccl": "Ecclesiastes", "eccles": "Ecclesiastes", "song": "Song of Songs", "isa": "Isaiah", "jer": "Jeremiah", "lam": "Lamentations", "ezek": "Ezekiel", "eze": "Ezekiel", "dan": "Daniel", "hos": "Hosea", "joel": "Joel", "amos": "Amos", "obad": "Obadiah", "jonah": "Jonah", "mic": "Micah", "nah": "Nahum", "hab": "Habakkuk", "zeph": "Zephaniah", "hag": "Haggai", "zech": "Zechariah", "mal": "Malachi", "matt": "Matthew", "mt": "Matthew", "mk": "Mark", "mrk": "Mark", "lk": "Luke", "luk": "Luke", "jn": "John", "jhn": "John", "acts": "Acts", "rom": "Romans", "1 cor": "1 Corinthians", "2 cor": "2 Corinthians", "gal": "Galatians", "eph": "Ephesians", "phil": "Philippians", "col": "Colossians", "1 thess": "1 Thessalonians", "2 thess": "2 Thessalonians", "1 tim": "1 Timothy", "2 tim": "2 Timothy", "titus": "Titus", "phlm": "Philemon", "heb": "Hebrews", "jas": "James", "1 pet": "1 Peter", "2 pet": "2 Peter", "1 jn": "1 John", "2 jn": "2 John", "3 jn": "3 John", "jude": "Jude", "rev": "Revelation", } BOOKS = set(BOOK_ORDER) | { "Samuel", "Kings", "Chronicles", "Corinthians", "Thessalonians", "Timothy", "Peter", "John", } THEMES = { "faith", "grace", "love", "salvation", "redemption", "covenant", "forgiveness", "mercy", "justice", "righteousness", "holiness", "prayer", "worship", "sin", "repentance", "hope", "peace", "joy", "obedience", "discipleship", "kingdom", "gospel", "resurrection", "eternal life", "sanctification", "justification", "atonement", "baptism", "communion", "prophecy", "wisdom", "truth", "light", "darkness", "healing", "miracles", "creation", "judgment", "reconciliation", "fellowship", "community", "sacrifice", "suffering", "perseverance", "humility", "trust", "guidance", "provision", "protection", "power", "glory", "praise", "servanthood", "stewardship", "generosity", "temptation", "victory", "identity", "adoption", "inheritance", "election", "predestination", "assurance", "confession", "intercession", "fasting", "tithing", "hospitality", "purity", "integrity", "patience", "gentleness", "self-control", "spiritual warfare", "discernment", "revelation", "blessing", "curse", "restoration", "renewal", "transformation", "surrender", "gratitude", "unity", "diversity", "witness", "mission", "evangelism", "compassion", "loyalty", } THEOLOGICAL_CONCEPTS = { "trinity", "incarnation", "eschatology", "hermeneutics", "typology", "soteriology", "ecclesiology", "pneumatology", "christology", "theodicy", "hypostatic union", "imputation", "propitiation", "expiation", "kenosis", "parousia", "millennialism", "dispensationalism", "covenant theology", "replacement theology", "supersessionism", "inerrancy", "sola scriptura", "sola fide", "total depravity", "common grace", } EVENTS = { "crucifixion", "resurrection", "ascension", "pentecost", "creation", "flood", "exodus", "last supper", "baptism of jesus", "transfiguration", "temptation", "birth", "nativity", "second coming", "rapture", "great commission", "sermon on the mount", "feeding of the 5000", "raising of lazarus", "wedding at cana", "tower of babel", "fall of man", "passover", "day of atonement", "tabernacle", "temple dedication", "burning bush", "parting of the red sea", "ten commandments", "golden calf", "fall of jericho", "anointing of david", "fall of jerusalem", "babylonian exile", "return from exile", "rebuilding of the temple", "annunciation", "triumphal entry", "last judgment", "great flood", "covenant with abraham", "binding of isaac", "jacob's ladder", "joseph sold into slavery", "plagues of egypt", "wilderness wandering", "conquest of canaan", "united monarchy", "divided kingdom", "fall of samaria", "council of jerusalem", "conversion of paul", "stoning of stephen", "damascus road", "road to emmaus", "doubting thomas", "great flood narrative", "tower of siloam", "cleansing of the temple", "washing of feet", "gethsemane prayer", "trial of jesus", "empty tomb", "road to calvary", "day of the lord", } TIME_PERIODS = { "old testament", "new testament", "early church", "exile", "wilderness", "intertestamental period", "apostolic age", "patriarchal age", "monarchy period", "judges period", "second temple period", "diaspora", } RELATION_PATTERNS = [ (re.compile(r"\b(\w[\w' ]*?)\s+said to\s+(\w[\w' ]*?)\b", re.I), "said to"), (re.compile(r"\b(\w[\w' ]*?)\s+spoke to\s+(\w[\w' ]*?)\b", re.I), "spoke to"), (re.compile(r"\b(\w[\w' ]*?)\s+wrote to\s+(\w[\w' ]*?)\b", re.I), "wrote to"), (re.compile(r"\b(\w[\w' ]*?)\s+taught\s+(\w[\w' ]*?)\b", re.I), "taught"), (re.compile(r"\b(\w[\w' ]*?)\s+healed\s+(\w[\w' ]*?)\b", re.I), "healed"), (re.compile(r"\b(\w[\w' ]*?)\s+baptized\s+(\w[\w' ]*?)\b", re.I), "baptized"), (re.compile(r"\b(\w[\w' ]*?)\s+killed\s+(\w[\w' ]*?)\b", re.I), "killed"), (re.compile(r"\b(\w[\w' ]*?)\s+anointed\s+(\w[\w' ]*?)\b", re.I), "anointed"), (re.compile(r"\b(\w[\w' ]*?)\s+sent\s+(\w[\w' ]*?)\b", re.I), "sent"), (re.compile(r"\b(\w[\w' ]*?)\s+called\s+(\w[\w' ]*?)\b", re.I), "called"), (re.compile(r"\b(\w[\w' ]*?)\s+appeared to\s+(\w[\w' ]*?)\b", re.I), "appeared to"), (re.compile(r"\b(\w[\w' ]*?)\s+rebuked\s+(\w[\w' ]*?)\b", re.I), "rebuked"), (re.compile(r"\b(\w[\w' ]*?)\s+blessed\s+(\w[\w' ]*?)\b", re.I), "blessed"), (re.compile(r"\b(\w[\w' ]*?)\s+betrayed\s+(\w[\w' ]*?)\b", re.I), "betrayed"), (re.compile(r"\b(\w[\w' ]*?)\s+forgave\s+(\w[\w' ]*?)\b", re.I), "forgave"), ] VERSE_RE = re.compile( r"\b([1-3]?\s?[A-Z][a-z]+\.?)\s+(\d{1,3}):(\d{1,3})(?:[-–](\d{1,3}))?\b" ) NODE_TYPE_ORDER = ["person", "place", "theme", "concept", "book", "verse", "event", "period"] def _canon_book(word_or_phrase): key = word_or_phrase.lower().strip().rstrip(".") if key in BOOK_ABBREVIATIONS: return BOOK_ABBREVIATIONS[key] for b in BOOK_ORDER: if b.lower() == key: return b return None def extract_entities(text): """Pure-Python NLP extraction: entities + co-occurrence/relationship edges.""" nodes = {} edge_weights = defaultdict(int) relation_edges = [] def add_node(key, ntype, label): key = key.lower().strip() if not key: return None if key not in nodes: nodes[key] = {"id": key, "label": label.strip(), "type": ntype, "weight": 1} else: nodes[key]["weight"] += 1 return key # Verse references for m in VERSE_RE.finditer(text): verse_label = m.group(0).strip() add_node(verse_label.lower(), "verse", verse_label) # Directed relationship extraction for pattern, relation in RELATION_PATTERNS: for m in pattern.finditer(text): a_raw, b_raw = m.group(1).strip(), m.group(2).strip() a_cap = a_raw.split()[-1].capitalize() if a_raw else "" b_cap = b_raw.split()[0].capitalize() if b_raw else "" if a_cap in PEOPLE and b_cap in PEOPLE: ka = add_node(a_cap.lower(), "person", a_cap) kb = add_node(b_cap.lower(), "person", b_cap) if ka and kb and ka != kb: relation_edges.append({"source": ka, "target": kb, "label": relation}) sentences = re.split(r"[.!?;\n]+", text) for sentence in sentences: if len(sentence.strip()) < 3: continue words = sentence.split() sentence_keys = [] for i, raw in enumerate(words): w = re.sub(r"[^a-zA-Z']", "", raw) if not w: continue w_cap = w[0].upper() + w[1:] if w else w w_low = w.lower() matched = False if w_cap in PEOPLE or w in PEOPLE: sentence_keys.append(add_node(w_low, "person", w_cap)) matched = True elif w_cap in PLACES: sentence_keys.append(add_node(w_low, "place", w_cap)) matched = True elif w_cap in BOOKS or w_low in BOOK_ABBREVIATIONS: canon = _canon_book(w_cap) or w_cap sentence_keys.append(add_node(canon.lower(), "book", canon)) matched = True elif w_low in THEMES: sentence_keys.append(add_node(w_low, "theme", w_low)) matched = True elif w_low in THEOLOGICAL_CONCEPTS: sentence_keys.append(add_node(w_low, "concept", w_low)) matched = True elif w_low in EVENTS: sentence_keys.append(add_node(w_low, "event", w_low)) matched = True if not matched and i + 1 < len(words): nxt = re.sub(r"[^a-zA-Z']", "", words[i + 1]).lower() bigram = f"{w_low} {nxt}" if bigram in EVENTS: sentence_keys.append(add_node(bigram, "event", bigram)) elif bigram in THEMES: sentence_keys.append(add_node(bigram, "theme", bigram)) elif bigram in THEOLOGICAL_CONCEPTS: sentence_keys.append(add_node(bigram, "concept", bigram)) elif bigram in TIME_PERIODS: sentence_keys.append(add_node(bigram, "period", bigram)) if not matched and i + 2 < len(words): w2 = re.sub(r"[^a-zA-Z']", "", words[i + 1]).lower() w3 = re.sub(r"[^a-zA-Z']", "", words[i + 2]).lower() trigram = f"{w_low} {w2} {w3}" if trigram in TIME_PERIODS: sentence_keys.append(add_node(trigram, "period", trigram)) elif trigram in EVENTS: sentence_keys.append(add_node(trigram, "event", trigram)) sentence_keys = [k for k in sentence_keys if k] for a in range(len(sentence_keys)): for b in range(a + 1, len(sentence_keys)): if sentence_keys[a] != sentence_keys[b]: k = tuple(sorted((sentence_keys[a], sentence_keys[b]))) edge_weights[k] += 1 edges = [{"source": s, "target": t, "weight": w} for (s, t), w in edge_weights.items()] node_list = [n for n in nodes.values() if len(n["label"]) > 1] node_ids = {n["id"] for n in node_list} edges = [e for e in edges if e["source"] in node_ids and e["target"] in node_ids] return {"nodes": node_list, "edges": edges, "relations": relation_edges} # ═══════════════════════════════════════════════════════════════════════════ # Graph algorithms (NetworkX) # ═══════════════════════════════════════════════════════════════════════════ def build_graph(data): G = nx.Graph() for n in data["nodes"]: G.add_node(n["id"], **n) for e in data["edges"]: G.add_edge(e["source"], e["target"], weight=e["weight"]) return G def run_algorithms(G): if G.number_of_nodes() == 0: return None pagerank = nx.pagerank(G, weight="weight") if G.number_of_edges() else {n: 1 / max(G.number_of_nodes(), 1) for n in G.nodes()} communities_raw = list(nx.community.greedy_modularity_communities(G, weight="weight")) if G.number_of_edges() else [{n} for n in G.nodes()] community_map = {} for idx, comm in enumerate(communities_raw): for node in comm: community_map[node] = idx betweenness = nx.betweenness_centrality(G, weight="weight") if G.number_of_nodes() > 2 else {n: 0 for n in G.nodes()} degree_centrality = nx.degree_centrality(G) try: eigenvector = nx.eigenvector_centrality(G, weight="weight", max_iter=500) except (nx.PowerIterationFailedConvergence, nx.AmbiguousSolution, ZeroDivisionError): eigenvector = {n: 0 for n in G.nodes()} clustering = nx.average_clustering(G, weight="weight") if G.number_of_edges() else 0.0 density = nx.density(G) if G.number_of_nodes() > 1 and nx.is_connected(G): diameter = nx.diameter(G) else: comps = list(nx.connected_components(G)) largest = max(comps, key=len) if comps else set() sub = G.subgraph(largest) diameter = nx.diameter(sub) if len(largest) > 1 else 0 bridges = list(nx.bridges(G)) if G.number_of_nodes() > 1 else [] articulation_pts = list(nx.articulation_points(G)) if G.number_of_nodes() > 2 else [] return { "pagerank": pagerank, "communities": [list(c) for c in communities_raw], "community_map": community_map, "betweenness": betweenness, "degree_centrality": degree_centrality, "eigenvector": eigenvector, "clustering": clustering, "density": density, "diameter": diameter, "bridges": bridges, "articulation_points": articulation_pts, } # ═══════════════════════════════════════════════════════════════════════════ # ACO Swarm — Ant Colony System (Dorigo & Gambardella, 1997) # ═══════════════════════════════════════════════════════════════════════════ class ACS: """Ant Colony System on the conceptual graph. Traces the strongest conceptual pathway between the two highest-PageRank nodes.""" def __init__(self, G, pagerank, alpha=1.0, beta=2.0, rho=0.1, q0=0.9, Q=1.0): self.G = G self.pr = pagerank self.alpha = alpha self.beta = beta self.rho = rho self.q0 = q0 self.Q = Q n_edges = max(G.number_of_edges(), 1) self.tau0 = 1.0 / n_edges self.tau = {frozenset(e): self.tau0 for e in G.edges()} def _key(self, a, b): return frozenset((a, b)) def _weight(self, a, b): return self.G[a][b].get("weight", 1) def _choose_next(self, current, visited): neighbors = [n for n in self.G.neighbors(current) if n not in visited] if not neighbors: return None scores = [] for n in neighbors: tau = self.tau.get(self._key(current, n), self.tau0) eta = self._weight(current, n) scores.append((tau ** self.alpha) * (eta ** self.beta)) total = sum(scores) if total <= 0: return random.choice(neighbors) if random.random() < self.q0: return neighbors[scores.index(max(scores))] r = random.random() * total acc = 0.0 for n, s in zip(neighbors, scores): acc += s if r <= acc: return n return neighbors[-1] def _local_update(self, a, b): k = self._key(a, b) self.tau[k] = (1 - self.rho) * self.tau.get(k, self.tau0) + self.rho * self.tau0 def _global_update(self, path, strength): deposit = self.Q * strength for i in range(len(path) - 1): k = self._key(path[i], path[i + 1]) self.tau[k] = (1 - self.rho) * self.tau.get(k, self.tau0) + self.rho * deposit def _path_strength(self, path, reached_target): if len(path) < 2: return 0.0 w = sum(self._weight(path[i], path[i + 1]) for i in range(len(path) - 1)) pr = sum(self.pr.get(n, 0) for n in path) score = (w / len(path)) * (1 + pr) return score if reached_target else score * 0.4 def run(self, n_ants=40, n_iter=30, source=None, target=None, max_len=10): nodes = list(self.G.nodes()) if not nodes: yield {"iteration": 0, "n_iter": n_iter, "best_path": [], "best_score": 0, "convergence": [], "tau": {}} return if source is None: source = max(nodes, key=lambda n: self.pr.get(n, 0)) if target is None: ranked = sorted(nodes, key=lambda n: -self.pr.get(n, 0)) target = next((n for n in ranked if n != source), source) best_path, best_score = [source], 0.0 convergence = [] for iteration in range(n_iter): iter_best_path, iter_best_score, iter_reached = None, -1.0, False for _ in range(n_ants): path = [source] visited = {source} current = source reached = False for _step in range(max_len): if current == target: reached = True break nxt = self._choose_next(current, visited) if nxt is None: break self._local_update(current, nxt) path.append(nxt) visited.add(nxt) current = nxt if current == target: reached = True score = self._path_strength(path, reached) if score > iter_best_score: iter_best_score, iter_best_path, iter_reached = score, path, reached for k in list(self.tau.keys()): self.tau[k] = max((1 - self.rho) * self.tau[k], 0.001) if iter_best_path and iter_best_score > best_score: best_score, best_path = iter_best_score, iter_best_path if best_path and len(best_path) > 1: self._global_update(best_path, best_score) convergence.append(round(best_score, 4)) yield { "iteration": iteration + 1, "n_iter": n_iter, "best_path": best_path, "best_score": round(best_score, 4), "convergence": convergence, "tau": {"|||".join(sorted(k)): round(v, 4) for k, v in self.tau.items()}, } def compute_swarm_insights(G, algo, swarm_result): degrees = dict(G.degree()) hub_concepts = sorted(degrees.items(), key=lambda x: -x[1])[:5] isolated = [n for n, d in degrees.items() if d <= 1] community_map = algo["community_map"] bridges_by_node = defaultdict(set) for u, v in G.edges(): cu, cv = community_map.get(u), community_map.get(v) if cu != cv: bridges_by_node[u].add(cv) bridges_by_node[v].add(cu) community_bridges = sorted( ((n, len(c)) for n, c in bridges_by_node.items() if len(c) >= 1), key=lambda x: -x[1], )[:5] def label(nid): return G.nodes[nid].get("label", nid) if nid in G.nodes else nid return { "hub_concepts": [f"{label(n)} (degree {d})" for n, d in hub_concepts], "isolated_concepts": [label(n) for n in isolated[:6]], "community_bridges": [f"{label(n)} bridges {c} communities" for n, c in community_bridges], "dominant_pathway": " → ".join(label(n) for n in swarm_result["best_path"]), } # ═══════════════════════════════════════════════════════════════════════════ # Anthropic streaming insights # ═══════════════════════════════════════════════════════════════════════════ _client = None def get_client(): global _client if _client is None: api_key = os.environ.get("ANTHROPIC_API_KEY") if not api_key: return None _client = anthropic.Anthropic(api_key=api_key) return _client def build_insights_prompt(notes, G, algo, swarm_state): top_nodes = sorted(algo["pagerank"].items(), key=lambda x: -x[1])[:10] num_communities = len(algo["communities"]) top_pr_id = top_nodes[0][0] if top_nodes else None top_pr_label = G.nodes[top_pr_id].get("label", top_pr_id) if top_pr_id else "unknown" bridge_id = max(algo["betweenness"].items(), key=lambda x: x[1])[0] if algo["betweenness"] else None bridge_label = G.nodes[bridge_id].get("label", bridge_id) if bridge_id else "unknown" top_lines = "\n".join( f" {i+1}. {G.nodes[n].get('label', n)} (PR={v:.4f})" for i, (n, v) in enumerate(top_nodes[:8]) ) swarm_state = swarm_state or {} insights = swarm_state.get("insights", {}) pathway = insights.get("dominant_pathway", "not yet computed") strength = swarm_state.get("best_score", 0) hub_concepts = ", ".join(insights.get("hub_concepts", [])[:5]) isolated = ", ".join(insights.get("isolated_concepts", [])[:3]) diameter = algo["diameter"] prompt = f"""You are a biblical scholar, theologian, and data scientist analyzing a scripture knowledge graph built from a student's Bible study notes using graph theory and swarm intelligence algorithms. STUDY NOTES (excerpt): {notes[:2500]} GRAPH TOPOLOGY: - Nodes: {G.number_of_nodes()} biblical entities - Edges: {G.number_of_edges()} conceptual relationships - Communities detected (greedy modularity): {num_communities} - Graph density: {algo['density']:.4f} - Average clustering coefficient: {algo['clustering']:.4f} - Graph diameter: {diameter} TOP ENTITIES BY PAGERANK (most theologically central): {top_lines} BETWEENNESS CENTRALITY (conceptual bridges): Bridge node: {bridge_label} ACO SWARM RESULTS: Dominant conceptual pathway: {pathway} Path strength: {strength} Hub concepts identified by swarm: {hub_concepts} Isolated concepts (weak connections): {isolated} Provide deep theological and structural analysis in exactly these sections: ## CENTRAL THEME What is the dominant theological theme? Why does PageRank converge on {top_pr_label}? (3-4 sentences, theologically substantive) ## GRAPH STRUCTURE REVELATION What does the clustering into {num_communities} communities reveal about how these biblical concepts relate? What surprised you about the graph topology? (3-4 sentences) ## SWARM INTELLIGENCE FINDING The ACO ants, operating with no global knowledge, organically traced: {pathway}. Why is this path theologically significant? What does it reveal that a linear reading of the notes might miss? (3-4 sentences) ## UNEXPECTED CONNECTION Identify one non-obvious connection between entities in the graph — ideally involving the bridge node ({bridge_label}) — that reveals something theologically significant. (2-3 sentences) ## CROSS-REFERENCES Suggest 3 specific passages that deepen this study, explaining the graph-theoretic reason each is relevant (e.g., "Hebrews 11 would add 15+ faith connections, increasing graph density in the salvation community"): 1. [Book Chapter:Verse] — reason 2. [Book Chapter:Verse] — reason 3. [Book Chapter:Verse] — reason ## STUDY PROMPT One penetrating question for deeper reflection, grounded in what the swarm and graph revealed.""" return prompt def generate_insights(notes, graph_state, algo_state, swarm_state): if not graph_state or not algo_state: yield "Build a knowledge graph first." return client = get_client() if client is None: yield ("**No Anthropic API key found.**\n\nSet the `ANTHROPIC_API_KEY` secret in this " "Space's settings (Settings → Variables and secrets) to enable AI insights.") return G = build_graph(graph_state) prompt = build_insights_prompt(notes or "", G, algo_state, swarm_state) accumulated = "" try: with client.messages.stream( model="claude-opus-4-8", max_tokens=2400, messages=[{"role": "user", "content": prompt}], ) as stream: for text in stream.text_stream: accumulated += text yield accumulated except Exception as e: # noqa: BLE001 — surface any API error to the UI accumulated += f"\n\n---\n**Error generating insights:** {e}" yield accumulated # ═══════════════════════════════════════════════════════════════════════════ # HTML fragments rendered server-side # ═══════════════════════════════════════════════════════════════════════════ EMPTY_STATS_HTML = '
Paste your notes · Extract entities · Run graph algorithms · Swarm intelligence · AI insights