// UNDER THE HOOD
Each persona is a Flask RAG API paired with a Discord bot. Both are grounded in a real body of primary sources. This page shows the full pipeline. We verified each number against the running system on July 12, 2026. We verified the counts again on July 27, 2026, the day the eighth mind joined.
The stack
Ask a persona a question, and the same seven-stage path runs each time. The path starts with the source text a real person wrote. It ends with a grounded answer that cites its sources.
The real person's own words, gathered as raw material.
books · letters · transcripts · poemsLong texts are split into passage-sized pieces so each idea can be found on its own.
persona-tuned splittingEach passage becomes a vector: a list of numbers capturing its meaning.
text-embedding-3-small · 1,536-dimThe vector database. Stores every passage as a point and finds the ones closest in meaning to a question.
cosine · HNSW · per-personaThe handful of passages most relevant to the question are pulled out as grounding.
top 5–6 chunksReads those passages plus the persona's prompt and writes the reply in that voice.
persona prompt + contextA grounded answer that names the passages it drew from.
sources · confidence · correlation IDThe system embeds a question into a 1,536-dimensional vector with OpenAI's text-embedding-3-small. It matches the vector against the persona's ChromaDB collection, with cosine similarity over an HNSW index. The top five or six passages become the grounding context. The system gives those passages, a persona system prompt, and a curated worldview to Claude Sonnet. Claude Sonnet writes the reply, with source citations, a confidence score, and a correlation ID for tracing. The model does not invent the answer. The answer is assembled from material the real person produced.
Fine-tuning creates a model that thinks it knows things. RAG creates a model that checks what it knows. This distinction matters for education and research. You want Franklin to quote his actual Autobiography, not a plausible passage that never existed. With RAG, you can update the corpus without retraining. Each response is traceable to specific passages. The model can also report when a question falls outside its sources. Fine-tuning gives you none of that.
Seven personas use direct RAG: embed once, retrieve once, generate once. Decode cannot. Its corpus is too large and too mixed for a single similarity query, so it decides what to search first. A third pattern, tested on Russell, handles the questions that neither pattern answers well.
Direct RAG · Gabriella 2.0, Franklin, Leonardo, Sagan, Emily, Attenborough, Russell
q_vec = embed(question) # text-embedding-3-small
chunks = collection.query(
q_vec, n_results=6) # ChromaDB · cosine · HNSW
prompt = persona_prompt + chunks + question
answer = claude("sonnet", prompt) # one generation
return answer, sources, confidence, correlation_id
One embedding, one search, one call to Claude. For a small corpus (Franklin's 173 chunks, Sagan's 402), a single top-6 retrieval gives enough context for a grounded answer. Retrieval and embedding finish in less than 500 ms. Generation takes most of the time.
Agentic two-pass · Decode (46,912 chunks)
reply = claude("claude-sonnet-4-6",
system + question)
while "```search" in reply: # Claude asks to search
query = parse_search_block(reply)
hits = collection.query(
embed(query), n_results=6)
reply = claude("claude-sonnet-4-6",
system + question + hits)
# Claude may search several times, then synthesizes
return reply, sources, confidence, correlation_id
Decode indexes 46,912 chunks: 83 courses, 1,030 academic papers, and 815 case studies. In a corpus this large, a raw question rarely maps to one similarity cluster. Thus Claude first sends a search block. The bot runs the search against ChromaDB and returns the results. Claude can query again before it commits to an answer. Retrieval becomes a short reasoning loop instead of one lookup.
The third pattern
"What are the top ten things I should know?" is a reasonable question. It is also the question shape that similarity search answers worst. Russell's pipeline carries the prototype repair, live since July 27, 2026.
A survey question retrieves chunks whose text resembles the question: passages about knowing, learning, and importance. It cannot retrieve "the corpus's most important ideas." Importance across the full corpus is not a property of any single chunk. Broad questions have no local answer in vector space. The answer is a property of the collection, not of a passage in it.
The layer holds thirteen curated study-guide documents: the ten essentials, a biography, a study path built from the Home Study Course's 48 lesson titles, a glossary of his coined vocabulary, his positions against accepted science, and one overview per book. We verified each quote in them against the corpus, word for word. The digests live in their own ChromaDB collection, so corpus reindexes do not touch them. A breadth detector in the API decides per question. Survey questions blend the top three digests with three primary chunks, and they get a larger word budget. Specific questions run the unchanged direct path.
the same pipeline, two questions · live retrieval labels, port 5057
"What are the top ten things I should know?" (broad → digests blend in)
Study Guide — The Essentials: the ten central teachings
Study Guide — A Study Path: where to begin
Study Guide — Key Terms: his vocabulary explained
+ The Universal One · Divine Iliad · Home Study Course chunks
"What did you teach about the power of prayer?" (specific → unchanged)
Home Study Course, Unit 2 — Lesson 5: The Power of Prayer (×4)
Home Study Course, Unit 5 · Unit 2
The persona is told that the study guides are reliable reference notes, not his own prose. It must paraphrase them in his voice, and it must not mention them. Asked the top-ten question, Russell now answers with an organized, numbered survey of his ten pillars. Before the digest layer, the same question returned a response built from whichever six chunks mentioned "knowing."
Russell's API expands queries with HyDE. A small model writes a hypothetical answer in his 1920s register, and the system embeds the question plus that passage. This connects modern phrasing to century-old prose. The digests broke that assumption. They are written in the same modern register as the question. Thus the system searches the digests with the raw question embedding, and it searches the primary chunks with the HyDE embedding. Two searches, two registers, one embedding space. The digests are plain markdown files. Edit one, run the indexer again, and the persona's summary knowledge updates in seconds. If the pattern proves out, the same three pieces (digest folder, indexer, breadth detector) port to the other seven minds directly.
Retrieval quality sets persona quality, and retrieval starts with how you cut the source. A strategy tuned for prose essays would destroy poetry. A strategy tuned for narrative would miss the breaks in a fragmented notebook. Thus each corpus is chunked on its own terms.
| Strategy | Personas | Target / Max / Overlap | Why |
|---|---|---|---|
| Standard prose | Gabriella 2.0, Franklin, Sagan | 750 / 1,000 / 100 tokens | Sentence-boundary splitting (tiktoken cl100k_base). Overlap keeps ideas that span a boundary from being lost. |
| Short fragments | Leonardo | 600 / 800 / 75 tokens | His notebooks jump between water, anatomy, and flight on a single page. Smaller chunks respect those fragment boundaries. |
| Whole document | Emily Dickinson | never chunked | Each of 629 poems is one indexed document. The semantic unit is the complete poem. |
| TopicAwareChunker | Attenborough | topic-boundary, zero overlap | A custom chunker cuts at documentary topic shifts (scene, location, and temporal transitions) instead of fixed-token windows. |
| Genre-aware + contextual headers | Walter Russell | 700 / 1,000 / 100 tokens | Books, course lessons, and verse each get their own splitting rules, and every chunk is embedded with a one-sentence situating header written by a small model, so retrieval knows where a passage sits in a 664,758-word corpus. |
A Dickinson poem builds its meaning across its full length. If you divide "Because I could not stop for Death" at line 8, you cut the journey from its destination. Emily does not generate verse. Her API retrieves her actual poems and quotes them as blockquotes, almost word for word. She is the clearest case of the idea: the persona is the source material, so the source material must stay whole.
A persona can retrieve correct context and still drift into generic AI prose. Thus we do not trust groundedness. We score it on each response, before the answer is returned.
The most mature implementation lives in Gabriella 2.0's API. It combines three signals. Keyword overlap checks that important terms from the top-ranked chunks appear in the answer. If the retrieved passages discuss "spiral evolution" and "zero point," and the reply mentions neither, the model probably used its training data instead of the corpus. Top-chunk similarity measures how close the best match was. A cosine score of 0.92 means strong source material exists. A score of 0.4 means the corpus covers the question poorly. A hedging scan looks for the language of padding: "in general," "broadly speaking," "from my understanding."
The combined confidence starts at 1.0. Each weakness subtracts a penalty. When the score falls below 0.5, the system flags the response rag_grounded: false. The flag does not remove the answer. It warns that the persona has moved away from its sources, and it lets the operators see how often each mind is grounded. For education and research, a persona that invents quotes is worse than no persona. The score keeps that honest.
POST /api/query · 127.0.0.1:5050
{ "question": "What does the ego do to perception?" }
200 OK
{
"answer": "The ego builds perceptual blinders...",
"sources": ["session-07.txt", "falling-up-ch12.txt"],
"rag_grounded": true,
"rag_confidence": 0.82,
"correlation_id": "gabi-8f3a1c",
"elapsed_ms": 4120
}
The system does not prompt disagreement. The source materials contain different epistemologies. This section shows the machinery that turns those differences into a stance before a persona speaks.
Each persona carries a curated worldview document at /opt/salon/worldviews/{name}.json. It holds a display_name, a rhetorical_style, a list of core_principles, and a beliefs dictionary keyed by topic. Each belief names a stance adjective, and it states a view in the persona's own frame. A person writes this file. It is not generated. The work is to read the sources deeply and to record what the person believed.
/opt/salon/worldviews/sagan.json (excerpt)
{
"display_name": "Carl Sagan",
"rhetorical_style": "poetic wonder grounded in rigorous skepticism",
"core_principles": [
"Extraordinary claims require extraordinary evidence",
"We are a way for the cosmos to know itself"
],
"beliefs": {
"wealth": { "stance": "indifferent",
"view": "The real treasures are not material..." },
"cosmos": { "stance": "reverent",
"view": "We are made of star stuff..." },
"truth": { "stance": "devoted",
"view": "It is far better to grasp the universe as it
really is than to persist in delusion." }
}
}
Before a persona takes a turn in a dialogue, the system extracts the topic and looks up the persona's beliefs. It then maps about 40 named stance adjectives (SKEPTICAL, ENTHUSIASTIC, RELUCTANT, CRITICAL, and the rest) to one of four canonical stances: AGREE, PARTIAL, DISAGREE, or CURIOUS. The canonical stance shapes the reply, and it creates the tension you see in the transcripts. Sagan's complete worldview file, and the JSON schema each persona follows, are published in the public repo's examples.
The fast path reads the worldview JSON directly, with no network call. It resolves a stance when a matching belief exists. When nothing in the document matches, the slow path starts. Claude Haiku determines a stance from beliefs retrieved from the persona's own corpus. Most turns stay on the fast path.
Each dialogue prompt carries explicit constraints, so exchanges do not collapse into polite echoes. A persona must not open with agreement words ("Exactly," "Precisely," "Indeed"). It must not repeat points already made in the thread. It must stay under about 60 words per turn. The last six turns go into the prompt as memory, so a persona knows what was said.
Service architecture
Each persona is a pair of systemd units: a Flask RAG API and a Discord bot. The bot waits for its API with an After= dependency. Discord is the home interface. The API is the brain behind it.
Each Flask API binds to 127.0.0.1 only. There is no DNS, no TLS, no routing overhead, and no external attack surface. The bot and its API speak over the loopback interface. Generation does not call the Anthropic API directly. It runs the locally installed Claude Code CLI. Seven of the eight personas use the rolling --model sonnet alias. Decode pins claude-sonnet-4-6, so its agentic loop stays reproducible. The active ensemble runs 21 systemd services: 8 persona APIs, 8 Discord bots, 4 real-time voice services, and 1 roundtable scheduler.
Flask API port map · localhost only
5050 Gabriella 2.0 430 chunks
5051 Franklin 173 chunks
5052 Leonardo 1,634 chunks
5053 Sagan 402 chunks
5054 Decode 46,912 chunks (agentic two-pass)
5055 Emily 629 poems (indexed whole)
5056 Attenborough 1,170 chunks (local embeddings + F5-TTS)
5057 Russell 1,371 chunks (contextual headers)
The latency budget shows where the time goes. Retrieval is fast. Claude generation holds the intelligence, and it takes 80–90% of the clock. That is an acceptable cost. A POST /api/query with {"question": ...} returns the grounded answer, its sources, the confidence fields, and a correlation ID. The Discord bot renders the same shape back into the channel.
Each persona can answer a voice message. Four can hold a live spoken conversation. One shows that the stack does not depend on a single vendor.
You speak; the raw audio comes in.
user audio inSpeech is transcribed to text so the pipeline can read it.
whisper-1 transcriptionThe exact same grounded text pipeline runs: retrieve, then answer.
shared with text chatThe reply is spoken back in the persona's cloned voice.
TTS reply outThe async path above works for all personas. Whisper (whisper-1) transcribes the voice message. The message runs through the same RAG query pipeline as text. ElevenLabs TTS speaks the reply back. The real-time path (voice-v2, built on py-cord) is a live spoken conversation. It detects 1.5 seconds of silence, streams speech-to-text into Claude, and streams ElevenLabs back out, at about 2 seconds of latency. It is live for Gabriella 2.0, Sagan, Decode, and Walter Russell. Sagan's channel mixes background music under his voice. Russell's voice is cloned from a 1953 lecture tape recorded at Swannanoa.
Voice calibration is manual work. We tuned Gabriella 2.0's ElevenLabs settings by ear across several sessions (stability 0.75, similarity 1.0, style 0.20, speed 0.88) until the voice sounded correct. There is no formula. You listen, and you adjust.
Attenborough uses the same pipeline with different backends. His embeddings come from a local nomic-embed-text-v1.5 model instead of OpenAI. His voice is F5-TTS on a rented Vast.ai GPU, reached through an SSH tunnel. There is no ElevenLabs in his stack. The grounding and retrieval are the same, and the inference is fully self-hosted. This shows that the system is not locked to one vendor.
None of these lessons were in the design document. They are the bugs that do not announce themselves, and the trade-offs you feel only after the system is live.
Each collection was first created with L2 (Euclidean) distance. But OpenAI's embeddings are normalized for cosine similarity. The mismatch did not cause an error. It ranked retrieval incorrectly. The correct passages were in the database, and the wrong ones came back first. The personas became more generic for weeks. Then we traced the problem to the distance metric, and we migrated each collection to cosine. The lesson: retrieval bugs do not crash. They quietly make personas more generic.
Attenborough's vendor-free stack is a real advantage, until the rented GPU is unreachable. When the Vast.ai machine is down, or the SSH tunnel drops, he does not report an error. He answers without retrieval. That is the ungrounded behavior the system exists to prevent. Vendor independence is real, but it has an operations cost. Self-hosted inference is one more thing that must stay up, and its failure mode is silent.
Both lessons above have the same root: the failures were invisible. Nothing crashed, and no log complained. The personas became blander. This is why the confidence score and the rag_grounded flag exist. Without measurement, a persona drifts back toward a generic language model, and no one notices until a transcript sounds like no one.
This page assumes that the source text is what the author wrote. The newest corpus showed the cost of that assumption. 18 of its 21 sources arrived as damaged OCR, and a grounded persona quotes damage as easily as it quotes doctrine. The intake discipline that repaired it covers provenance forensics, junk-rate measurement, re-OCR, and independent validation gates. It has a page of its own.
Each subject on this page has a markdown counterpart in the public repo: the pipeline, the chunking strategies, groundedness scoring, agentic RAG, the worldview and stance machinery, and these production lessons. The repo also holds working examples, such as the API spec and the systemd unit templates. github.com/sloptycoon/revive →