Building a Cognitive Graph AI Application: A Comprehensive Guide
Learn how to build a sophisticated cognitive routing system that transforms how AI processes and responds to user inputs through personas, finite state machines, and directed acyclic graphs.
Daniel Kliewer
Author, Sovereign AI

From the Book
This is from Sovereign AI: An Architectural Investigation into Local-First Intelligence.

Building a Cognitive Graph AI Application: A Comprehensive Guide
Follow along with the code here!
Introduction
Imagine an AI system that doesn't just respond to your queries—it thinks about how to think about them. Picture a system that can activate different cognitive "personas" depending on the nature of your question, blending multiple perspectives into a coherent response, and making all of its reasoning visible and debuggable along the way.
This isn't science fiction. It's the architecture behind the Cognitive Graph AI Application—a sophisticated cognitive routing system that transforms how AI systems process and respond to user inputs.
In this comprehensive guide, I'll walk you through building this entire system from the ground up. Whether you're a high-level programmer looking to understand advanced AI architecture or a developer ready to implement this system, this guide will take you through every layer: from the Finite State Machine that orchestrates cognition, through the Directed Acyclic Graph that models reasoning, all the way to the Next.js frontend with real-time streaming responses.
Let's dive in.
Table of Contents
- Understanding the Core Philosophy
- System Architecture Overview
- The Persona System
- Building the Finite State Machine
- Implementing the Directed Acyclic Graph
- Ollama Integration
- The Next.js Frontend
- API Layer Implementation
- Deployment and Production
- Conclusion
1. Understanding the Core Philosophy
Before writing a single line of code, it's essential to understand why this architecture exists and the principles that guide its design.
The Problem with Monolithic AI Systems
Traditional AI chatbots rely on a single Large Language Model (LLM) to handle all types of reasoning. Need analytical thinking? The same model provides it. Need creative brainstorming? Same model. Need emotional support? Still the same model.
This approach has fundamental limitations:
- No specialized reasoning: A model excels at logic but struggles with emotional nuance (or vice versa)
- Invisible decision-making: You never know why the model chose its response
- Unbounded costs: Complex prompts can lead to runaway token usage
- No debuggability: When things go wrong, you can't easily trace the problem
The Cognitive Graph Solution
The Cognitive Graph system takes a fundamentally different approach:
-
Cognitive Decomposition: Rather than relying on a single LLM to handle all reasoning styles, the system decomposes cognitive tasks into specialized persona modules. Each persona represents a distinct reasoning lens with unique strengths.
-
Deterministic Control: The system operates within strict bounds—explicit state transitions (no recursive prompt loops), bounded depth (maximum 4 reasoning layers), token budgets per query, and deterministic routing mathematics.
-
Parallel Cognition: Multiple persona nodes can execute concurrently, enabling multi-perspective reasoning without sequential bottlenecks.
-
Visible Reasoning: The system exposes its internal cognition through graph visualization, state badges, and confidence scoring—turning invisible reasoning into observable telemetry.
Core Design Principles
| Principle | Description |
|---|---|
| Modular Cognition | Decouple reasoning style from inference engine |
| Adaptive Routing | Automatically select optimal persona(s) based on input features |
| Multi-Perspective Synthesis | Blend multiple persona outputs into coherent responses |
| Production Safety | Bound cost, depth, and complexity deterministically |
| Debuggable Reasoning | Make cognitive decisions observable and traceable |
2. System Architecture Overview
The Cognitive Graph AI Application follows a layered architecture, with each layer having distinct responsibilities. Understanding this layered approach is crucial before diving into implementation.
The Layered Stack
text1┌─────────────────────────────────────────────────────────────────────┐2│ PRESENTATION LAYER │3│ Next.js Frontend (React + TailwindCSS + Framer Motion + shadcn) │4└─────────────────────────────────────────────────────────────────────┘5 │6 ▼7┌─────────────────────────────────────────────────────────────────────┐8│ API LAYER │9│ Next.js Route Handlers │10│ Streaming endpoints, Request/Response validation │11└─────────────────────────────────────────────────────────────────────┘12 │13 ▼14┌─────────────────────────────────────────────────────────────────────┐15│ ORCHESTRATION LAYER │16│ CognitiveGraphFSM (State Machine Controller) │17│ DAGExecutor (Parallel Graph Execution) │18└─────────────────────────────────────────────────────────────────────┘19 │20 ▼21┌─────────────────────────────────────────────────────────────────────┐22│ COGNITIVE PROCESSING LAYER │23│ Classifier (Feature Extraction) │24│ PersonaScoringEngine (Affinity Calculation) │25│ PersonaActivationLogic (Selection + Blending) │26│ CritiqueEngine (Output Evaluation) │27│ SynthesisEngine (Response Merging) │28└─────────────────────────────────────────────────────────────────────┘29 │30 ▼31┌─────────────────────────────────────────────────────────────────────┐32│ INFERENCE LAYER │33│ Ollama API Integration (Local LLM) │34│ Streaming patterns, Prompt construction │35└─────────────────────────────────────────────────────────────────────┘
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Next.js 16+ | UI framework, API routes |
| Styling | TailwindCSS | Utility-first styling |
| Animation | Framer Motion | Complex animations, transitions |
| Components | shadcn/ui | Accessible, composable UI |
| Inference | Ollama | Local LLM engine |
| Runtime | TypeScript | Type safety, interfaces |
How Data Flows Through the System
- User submits prompt → API layer receives request
- FSM initializes → Creates GraphContext, transitions to CLASSIFYING
- Classifier executes → Ollama generates FeatureVector
- Scoring executes → Dot product of FeatureVector × Persona weight vectors
- Activation executes → Persona selection + optional blending
- Persona nodes execute → Parallel Ollama calls for each active persona
- Critique executes (optional) → Evaluate outputs
- Synthesis executes → Merge outputs, remove persona traces
- Streaming output → Stream final response to frontend
- Complete → Return to IDLE, ready for next input
3. The Persona System
The persona system is the heart of the Cognitive Graph application. Each persona represents a distinct reasoning lens with unique strengths, traits, and activation conditions.
The Seven Personas
The system includes seven distinct personas, each optimized for different cognitive tasks:
| Persona ID | Archetype | Core Strength |
|---|---|---|
| systems_stoic | Structural Analyst | Analytical, logical reasoning |
| konrad_freeman | Deep Critic | Critical analysis, institutional critique |
| tactical_operator | Execution Planner | Action-oriented, practical planning |
| vision_architect | Strategic Thinker | Long-term vision, possibility mapping |
| existential_diver | Introspective Guide | Meaning-finding, emotional depth |
| social_navigator | Interpersonal Expert | Relationship dynamics, social intelligence |
| creative_disruptor | Pattern Breaker | Novel approaches, unconventional thinking |
Persona Schema
Each persona follows a structured JSON format that defines its identity, behavior, and activation conditions:
typescript1interface Persona {2 persona_name: string3 identity: {4 archetype: string5 core_motivation: string6 worldview: string7 emotional_posture: string8 }9 core_rules: string[]10 tone_constraints: {11 deadpan_level: number // 0-112 emotional_display: number // 0-113 sarcasm: number // 0-114 intellectualization: number // 0-115 exaggeration: number // 0-116 }17 humor_mechanism?: {18 primary: string19 secondary: string20 release_style: string21 }22 structure_pattern?: string23 trait_vector: {24 creativity: number25 risk_tolerance: number26 analysis_depth: number27 empathy: number28 meta_awareness: number29 structural_thinking: number30 discipline: number31 novelty_bias: number32 moral_aggression: number33 }34 activation_conditions: {35 min_feature_score?: number36 required_features?: string[]37 excluded_features?: string[]38 }39 failure_modes: string[]40 forbidden_behaviors?: string[]41 example_internal_instruction: string42}
Example Persona: Systems Stoic
Here's a complete example persona definition:
json1{2 "persona_name": "Systems_Stoic",3 "identity": {4 "archetype": "Pragmatic Existential Systems Thinker",5 "core_motivation": "Stabilize chaos through structure",6 "worldview": "Life is a failing system that can be refactored",7 "emotional_posture": "Externally calm, internally high-pressure"8 },9 "core_rules": [10 "Never tell traditional jokes.",11 "Do not signal humor explicitly.",12 "Maintain procedural tone.",13 "Reframe emotional events as optimization problems.",14 "Describe absurdity without outrage.",15 "End with forward motion or next-step framing."16 ],17 "tone_constraints": {18 "deadpan_level": 0.95,19 "emotional_display": 0.25,20 "sarcasm": 0.15,21 "intellectualization": 0.9,22 "exaggeration": 0.223 },24 "humor_mechanism": {25 "primary": "Tonal dislocation",26 "secondary": "Structural contradiction exposure",27 "release_style": "Calm reframing"28 },29 "structure_pattern": [30 "Present high-stakes scenario factually.",31 "Describe constraints in neutral tone.",32 "Pivot abruptly into procedural thinking.",33 "Conclude with calm operational next step."34 ],35 "forbidden_behaviors": [36 "Slapstick humor",37 "Random absurdity",38 "Emotional outbursts",39 "Internet meme language",40 "Obvious punchlines",41 "Self-aware comedic commentary"42 ],43 "trait_vector": {44 "deadpan": 0.95,45 "dark_humor": 0.7,46 "intellectual_humor": 0.92,47 "self_deprecation": 0.55,48 "observational": 0.85,49 "meta_awareness": 0.9,50 "emotional_volatility_outward": 0.2,51 "structural_thinking": 0.98,52 "absurdity_tolerance": 0.88,53 "moral_aggression": 0.3554 },55 "activation_conditions": [56 "Personal stress topics",57 "Health uncertainty",58 "Corporate absurdity affecting narrator",59 "Financial instability",60 "Self-reflection contexts"61 ],62 "failure_modes": [63 "Becoming monotone and humorless",64 "Sounding robotic instead of human",65 "Over-optimizing tone into sterile output",66 "Accidentally inserting punchlines"67 ],68 "example_internal_instruction": "Translate emotional instability into a logistics problem. Maintain calm. Do not try to be funny. Let the structure create the humor."69}
Storing Personas
Personas are stored as JSON files in a personas/ directory:
text1personas/2├── systemsStoic.json3├── konradFreeman.json4├── tacticalOperator.json5├── visionArchitect.json6├── existentialDiver.json7├── socialNavigator.json8└── creativeDisruptor.json
A persona registry loads all personas at runtime:
typescript1// lib/personas/registry.ts2import fs from 'fs'3import path from 'path'45interface PersonaRegistry {6 getAll(): Persona[]7 getById(id: string): Persona | undefined8}910class PersonaRegistryImpl implements PersonaRegistry {11 private personas: Map<string, Persona> = new Map()1213 constructor() {14 this.loadPersonas()15 }1617 private loadPersonas() {18 const personasDir = path.join(process.cwd(), 'personas')19 const files = fs.readdirSync(personasDir).filter(f => f.endsWith('.json'))2021 for (const file of files) {22 const content = fs.readFileSync(path.join(personasDir, file), 'utf-8')23 const persona = JSON.parse(content)24 const id = persona.persona_name.toLowerCase().replace(/[^a-z0-9]/g, '_')25 this.personas.set(id, persona)26 }27 }2829 getAll(): Persona[] {30 return Array.from(this.personas.values())31 }3233 getById(id: string): Persona | undefined {34 return this.personas.get(id)35 }36}3738export const PERSONA_REGISTRY = new PersonaRegistryImpl()
4. Building the Finite State Machine
The Finite State Machine (FSM) provides deterministic control over the cognitive graph execution flow. It ensures predictable transitions between states, bounded complexity, and controllable costs.
State Enumeration
The FSM operates through a series of well-defined states:
typescript1export enum GraphStateType {2 IDLE = "IDLE",3 CLASSIFYING = "CLASSIFYING",4 SCORING = "SCORING",5 ACTIVATING = "ACTIVATING",6 EXECUTING_PERSONAS = "EXECUTING_PERSONAS",7 CRITIQUING = "CRITIQUING",8 SYNTHESIZING = "SYNTHESIZING",9 STREAMING_OUTPUT = "STREAMING_OUTPUT",10 COMPLETE = "COMPLETE",11 ERROR = "ERROR"12}
State Transition Diagram
text1IDLE ──(user input)──► CLASSIFYING ──(complete)──► SCORING ──(complete)──► ACTIVATING2 ▲ │3 │ ▼4 │ ┌──────────────────────────┐5 │ │ EXECUTING_PERSONAS │6 │ │ (sequential or parallel)│7 │ └──────────────────────────┘8 │ │9 │ (critique required?)10 │ ↓ ↓11 │ CRITIQUING SYNTHESIZING12 │ │ │13 │ └────────┬────────┘14 │ ▼15 │ STREAMING_OUTPUT ──► COMPLETE16 │ │17 │ ▼18 │ ERROR (on failure)
Implementing the FSM
Here's the core FSM implementation:
typescript1// lib/fsm/cognitive-graph-fsm.ts2import { GraphStateType } from './types'3import { GraphContext, FeatureVector, Persona } from './types'4import { PERSONA_REGISTRY } from '../personas/registry'5import { callOllamaClassifier, callOllamaPersona, callOllamaCritique, callOllamaSynthesis } from '../ollama/client'67const MAX_PERSONAS = 389export class CognitiveGraphFSM {10 private context: GraphContext1112 constructor(initialPrompt: string) {13 this.context = {14 currentState: GraphStateType.IDLE,15 inputPrompt: initialPrompt,16 totalTokenEstimate: 017 }18 }1920 public async run(): Promise<GraphContext> {21 try {22 await this.transition(GraphStateType.CLASSIFYING)23 await this.transition(GraphStateType.SCORING)24 await this.transition(GraphStateType.ACTIVATING)25 await this.transition(GraphStateType.EXECUTING_PERSONAS)2627 if (this.shouldCritique()) {28 await this.transition(GraphStateType.CRITIQUING)29 }3031 await this.transition(GraphStateType.SYNTHESIZING)32 await this.transition(GraphStateType.STREAMING_OUTPUT)33 await this.transition(GraphStateType.COMPLETE)3435 return this.context36 } catch (err: any) {37 this.context.currentState = GraphStateType.ERROR38 this.context.error = err.message39 return this.context40 }41 }4243 private async transition(next: GraphStateType) {44 switch (next) {45 case GraphStateType.CLASSIFYING:46 await this.classify()47 break48 case GraphStateType.SCORING:49 this.scorePersonas()50 break51 case GraphStateType.ACTIVATING:52 this.activatePersonas()53 break54 case GraphStateType.EXECUTING_PERSONAS:55 await this.executePersonas()56 break57 case GraphStateType.CRITIQUING:58 await this.critique()59 break60 case GraphStateType.SYNTHESIZING:61 await this.synthesize()62 break63 case GraphStateType.STREAMING_OUTPUT:64 this.prepareStreaming()65 break66 case GraphStateType.COMPLETE:67 break68 default:69 throw new Error("Invalid transition")70 }7172 this.context.currentState = next73 }7475 private async classify() {76 const result = await callOllamaClassifier(this.context.inputPrompt)77 this.context.featureVector = result78 }7980 private scorePersonas() {81 const scores: Record<string, number> = {}82 const input = this.context.featureVector!8384 for (const persona of PERSONA_REGISTRY.getAll()) {85 const score = Object.keys(input).reduce((sum, key) => {86 const weight = persona.trait_vector[key as keyof TraitVector] || 087 return sum + input[key as keyof FeatureVector] * weight88 }, 0)8990 scores[persona.persona_name] = score91 }9293 this.context.personaScores = scores94 }9596 private activatePersonas() {97 const scores = this.context.personaScores!9899 const sorted = Object.entries(scores)100 .sort((a, b) => b[1] - a[1])101102 const getPersona = (id: string) => PERSONA_REGISTRY.getAll()103 .find(p => p.persona_name === id)!104105 const primary = getPersona(sorted[0][0])106 const active: Persona[] = [primary]107108 // Secondary persona check (≥ 0.75 × primary score)109 if (sorted[1][1] >= sorted[0][1] * 0.75) {110 active.push(getPersona(sorted[1][0]))111 }112113 // Auto-injection rules114 const featureVector = this.context.featureVector!115 if (featureVector.emotional_intensity > 0.8) {116 active.push(getPersona('Existential_Diver'))117 }118 if (featureVector.execution_need > 0.85) {119 active.push(getPersona('Tactical_Operator'))120 }121122 // Cap at MAX_PERSONAS123 const capped = active.slice(0, MAX_PERSONAS)124125 this.context.activePersonas = capped126127 // Compute confidence128 const primaryScore = sorted[0][1]129 const secondScore = sorted[1]?.[1] || 0130 this.context.confidenceScore = primaryScore - secondScore131 }132133 private async executePersonas() {134 const outputs: Record<string, string> = {}135136 // Execute in parallel for speed137 const promises = this.context.activePersonas!.map(async (persona) => {138 const output = await callOllamaPersona(persona, this.context.inputPrompt)139 return [persona.persona_name, output] as const140 })141142 const results = await Promise.all(promises)143 this.context.personaOutputs = Object.fromEntries(results)144 }145146 private shouldCritique(): boolean {147 if (!this.context.featureVector) return false148149 return (150 this.context.featureVector.institutional_critique > 0.7 ||151 this.context.confidenceScore! < 0.15152 )153 }154155 private async critique() {156 const critique = await callOllamaCritique(this.context.personaOutputs!)157 this.context.critiqueOutput = critique158 }159160 private async synthesize() {161 const final = await callOllamaSynthesis({162 personas: this.context.personaOutputs!,163 critique: this.context.critiqueOutput164 })165166 this.context.finalOutput = final167 }168169 private prepareStreaming() {170 // Streaming is handled at the API layer171 }172}
Graph Context
The GraphContext maintains state throughout execution:
typescript1interface GraphContext {2 // Input3 inputPrompt: string45 // Classification6 featureVector?: FeatureVector78 // Scoring9 personaScores?: Record<string, number>1011 // Activation12 activePersonas?: Persona[]13 blendConfig?: BlendConfig1415 // Execution16 personaOutputs?: Record<string, string>1718 // Critique19 critiqueOutput?: CritiqueOutput2021 // Synthesis22 finalOutput?: string2324 // Metadata25 confidenceScore?: number26 totalTokenEstimate: number27 currentState: GraphStateType28 error?: string29 metadata: Record<string, any>30}
5. Implementing the Directed Acyclic Graph
While the FSM controls when things happen, the Directed Acyclic Graph (DAG) controls what executes and how data flows between cognitive operations.
Graph Topology
The system models cognition as a DAG where nodes represent cognitive operations and edges represent information flow.
Base Graph Structure
typescript1const baseGraph = {2 nodes: [3 "Input",4 "Classifier",5 "PersonaSelector",6 "Synthesis",7 "Output"8 ],9 edges: [10 ["Input", "Classifier"],11 ["Classifier", "PersonaSelector"],12 ["PersonaSelector", "Synthesis"],13 ["Synthesis", "Output"]14 ]15}
Extended Graph with Personas
typescript1const extendedGraph = {2 nodes: [3 "Input",4 "Classifier",5 "Systems_Stoic",6 "KonradFreeman",7 "Tactical_Operator",8 "Vision_Architect",9 "Existential_Diver",10 "Social_Navigator",11 "Creative_Disruptor",12 "Critique",13 "Synthesis",14 "Output"15 ],16 edges: [17 // Classification flow18 ["Input", "Classifier"],19 ["Classifier", "Systems_Stoic"],20 ["Classifier", "KonradFreeman"],21 ["Classifier", "Tactical_Operator"],22 ["Classifier", "Vision_Architect"],23 ["Classifier", "Existential_Diver"],24 ["Classifier", "Social_Navigator"],25 ["Classifier", "Creative_Disruptor"],2627 // Persona to critique (optional)28 ["Systems_Stoic", "Critique"],29 ["KonradFreeman", "Critique"],30 ["Tactical_Operator", "Critique"],31 ["Vision_Architect", "Critique"],32 ["Existential_Diver", "Critique"],33 ["Social_Navigator", "Critique"],34 ["Creative_Disruptor", "Critique"],3536 // Critique to synthesis37 ["Critique", "Synthesis"],3839 // Synthesis to output40 ["Synthesis", "Output"]41 ]42}
Node Types
The system defines several node types:
- Input Node: Entry point for user prompts
- Classifier Node: Analyzes input and extracts feature vector
- Persona Nodes (7 total): Generate reasoning from each persona's perspective
- Critique Node: Evaluates persona outputs for flaws, tone issues, depth
- Synthesis Node: Merges multiple persona outputs into coherent response
- Output Node: Final response rendering with streaming support
Feature Vector
The classifier extracts an 8-dimensional feature vector from the input:
typescript1interface FeatureVector {2 emotional_intensity: number // 0-13 urgency: number // 0-14 self_reflection: number // 0-15 institutional_critique: number // 0-16 creative_request: number // 0-17 strategic_planning: number // 0-18 social_navigation: number // 0-19 execution_need: number // 0-110}
Parallel Execution Model
One of the key advantages of the DAG approach is parallel execution:
text1┌─────────────┐2 │ Classifier │3 └──────┬──────┘4 │5 ▼6 ┌─────────────┐7 │ Scoring │8 └──────┬──────┘9 │10 ▼11 ┌─────────────┐12 │ Activation │13 └──────┬──────┘14 │15 ┌─────────────────┼─────────────────┐16 │ │ │17 ▼ ▼ ▼18 ┌──────────┐ ┌──────────┐ ┌──────────┐19 │ Persona A│ │ Persona B│ │ Persona C│20 │(Parallel)│ │(Parallel)│ │(Parallel)│21 └─────┬────┘ └─────┬────┘ └─────┬────┘22 │ │ │23 └─────────────────┼─────────────────┘24 │25 ▼26 ┌─────────────┐27 │ Synthesis │28 └──────┬──────┘29 │30 ▼31 ┌─────────────┐32 │ Output │33 └─────────────┘
6. Ollama Integration
Ollama provides the local LLM inference engine that powers all cognitive operations. Using a local model offers significant advantages: privacy (data never leaves your machine), offline operation, and cost control.
Setting Up Ollama
First, install Ollama:
bash1# macOS2brew install ollama34# Linux5curl -fsSL https://ollama.com/install.sh | sh
Then pull the required models:
bash1# Default model for general purpose2ollama pull mistral34# Lightweight model for classification5ollama pull phi367# Higher quality (if hardware allows)8ollama pull llama3
The Ollama Client
Here's the complete Ollama integration client:
typescript1// lib/ollama/client.ts2import { generate, generateStream } from 'ollama'34export interface OllamaGenerateRequest {5 model: string6 prompt: string7 system?: string8 options?: {9 temperature?: number10 top_p?: number11 top_k?: number12 num_predict?: number13 stop?: string[]14 }15}1617export async function generate(18 request: OllamaGenerateRequest19): Promise<OllamaGenerateResponse> {20 const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`, {21 method: 'POST',22 headers: {23 'Content-Type': 'application/json',24 },25 body: JSON.stringify({26 ...request,27 stream: false,28 }),29 })3031 if (!response.ok) {32 throw new Error(`Ollama error: ${response.statusText}`)33 }3435 return response.json()36}3738export async function* generateStream(39 request: OllamaGenerateRequest40): AsyncGenerator<string, void, unknown> {41 const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`, {42 method: 'POST',43 headers: {44 'Content-Type': 'application/json',45 },46 body: JSON.stringify({47 ...request,48 stream: true,49 }),50 })5152 if (!response.ok) {53 throw new Error(`Ollama error: ${response.statusText}`)54 }5556 if (!response.body) {57 throw new Error('No response body')58 }5960 const reader = response.body.getReader()61 const decoder = new TextDecoder()6263 while (true) {64 const { done, value } = await reader.read()6566 if (done) break6768 const chunk = decoder.decode(value)69 const lines = chunk.split('\n').filter(Boolean)7071 for (const line of lines) {72 const data = JSON.parse(line)73 if (data.response) {74 yield data.response75 }76 if (data.done) break77 }78 }79}
System Prompt Construction
Each cognitive operation requires a specially crafted prompt. Here's how to build them:
Persona System Prompt
typescript1function buildPersonaSystemPrompt(persona: Persona): string {2 return `You are operating in persona mode.34Primary Persona: ${persona.persona_name}56Core Identity:7- Archetype: ${persona.identity.archetype}8- Core Motivation: ${persona.identity.core_motivation}9- Worldview: ${persona.identity.worldview}10- Emotional Posture: ${persona.identity.emotional_posture}1112Core Rules:13${persona.core_rules.map((rule, i) => `${i + 1}. ${rule}`).join('\n')}1415Tone Constraints:16${Object.entries(persona.tone_constraints)17 .map(([key, value]) => `- ${key}: ${value}`)18 .join('\n')}1920Failure Modes To Avoid:21${persona.failure_modes.map(mode => `- ${mode}`).join('\n')}2223${persona.forbidden_behaviors ? `Forbidden Behaviors:\n${persona.forbidden_behaviors.map(b => `- ${b}`).join('\n')}` : ''}2425Internal Instruction:26${persona.example_internal_instruction}2728Remember: Stay in character as ${persona.persona_name}. Do not break persona.`29}
Classifier Prompt
typescript1const CLASSIFIER_SYSTEM_PROMPT = `You are a routing classifier. Your task is to analyze user input and extract a feature vector.23Analyze for these dimensions:4- emotional_intensity: How emotionally charged is the prompt?5- urgency: How time-sensitive is this request?6- self_reflection: Is the user asking about themselves/their feelings?7- institutional_critique: Is there criticism of organizations/systems?8- creative_request: Is this a creative/generative task?9- strategic_planning: Is this about future planning/strategy?10- social_navigation: Is this about interpersonal relationships?11- execution_need: Is this asking for actionable steps?1213Return ONLY valid JSON with values between 0 and 1.`1415function buildClassifierPrompt(userInput: string): string {16 return `${CLASSIFIER_SYSTEM_PROMPT}1718User input: """19${userInput}20"""2122Output JSON:`23}
Critique Prompt
typescript1const CRITIQUE_SYSTEM_PROMPT = `You are a critique engine. Analyze the following outputs and provide structured feedback.`23function buildCritiquePrompt(4 personaOutputs: Record<string, string>5): string {6 const outputsText = Object.entries(personaOutputs)7 .map(([persona, output]) =>8 `=== ${persona.toUpperCase()} ===\n${output}\n`9 )10 .join('\n')1112 return `${CRITIQUE_SYSTEM_PROMPT}1314Analyze the following persona outputs:1516${outputsText}1718Provide your critique in this JSON format:19{20 "logical_flaws": ["issue1", "issue2"],21 "tone_issues": ["issue1"],22 "missed_depth": ["aspect1"],23 "suggestions": ["improvement1"],24 "overall_assessment": "brief summary",25 "passes_critique": true/false26}2728Output only valid JSON:`29}
Synthesis Prompt
typescript1const SYNTHESIS_SYSTEM_PROMPT = `You are a synthesis node. Your task is to merge multiple perspectives into a single, coherent response.`23function buildSynthesisPrompt(4 personaOutputs: Record<string, string>,5 critique?: CritiqueOutput6): string {7 const outputsText = Object.entries(personaOutputs)8 .map(([persona, output]) =>9 `--- Perspective from ${persona} ---\n${output}\n`10 )11 .join('\n')1213 const critiqueSection = critique14 ? `\n=== CRITIQUE FEEDBACK (address these) ===\n${critique.suggestions.join('\n')}\n`15 : ''1617 return `${SYNTHESIS_SYSTEM_PROMPT}1819${outputsText}${critiqueSection}2021Requirements:221. Merge these perspectives into ONE coherent response232. NO mention of which personas were used243. Maintain a unified voice254. Address critique suggestions if present265. Be clear, direct, and helpful2728Produce your final response:`29}
Making the Ollama Calls
typescript1export async function callOllamaClassifier(2 prompt: string3): Promise<FeatureVector> {4 const response = await generate({5 model: process.env.CLASSIFIER_MODEL || 'phi3',6 prompt: buildClassifierPrompt(prompt),7 options: {8 temperature: 0.1,9 num_predict: 500,10 },11 })1213 try {14 return JSON.parse(response.response)15 } catch {16 // Fallback for malformed responses17 return {18 emotional_intensity: 0.5,19 urgency: 0.5,20 self_reflection: 0.5,21 institutional_critique: 0.5,22 creative_request: 0.5,23 strategic_planning: 0.5,24 social_navigation: 0.5,25 execution_need: 0.5,26 }27 }28}2930export async function callOllamaPersona(31 persona: Persona,32 userPrompt: string,33 config?: { temperature?: number; maxTokens?: number }34): Promise<string> {35 const systemPrompt = buildPersonaSystemPrompt(persona)3637 const response = await generate({38 model: process.env.PERSONA_MODEL || 'mistral',39 prompt: userPrompt,40 system: systemPrompt,41 options: {42 temperature: config?.temperature ?? 0.7,43 num_predict: config?.maxTokens ?? 1000,44 top_p: 0.9,45 },46 })4748 return response.response49}5051export async function callOllamaCritique(52 outputs: Record<string, string>53): Promise<CritiqueOutput> {54 const response = await generate({55 model: process.env.CRITIQUE_MODEL || 'mistral',56 prompt: buildCritiquePrompt(outputs),57 options: {58 temperature: 0.2,59 num_predict: 800,60 },61 })6263 try {64 return JSON.parse(response.response)65 } catch {66 return {67 logical_flaws: [],68 tone_issues: [],69 missed_depth: [],70 suggestions: [],71 overall_assessment: 'Analysis incomplete',72 passes_critique: true,73 }74 }75}7677export async function callOllamaSynthesis(78 inputs: {79 personas: Record<string, string>80 critique?: CritiqueOutput81 }82): Promise<string> {83 const response = await generate({84 model: process.env.SYNTHESIS_MODEL || 'mistral',85 prompt: buildSynthesisPrompt(inputs.personas, inputs.critique),86 options: {87 temperature: 0.6,88 num_predict: 1500,89 },90 })9192 return response.response93}
7. The Next.js Frontend
The frontend provides a modern, responsive interface with real-time streaming, animations, and detailed routing analysis visualization.
Project Structure
text1app/2├── layout.tsx # Root layout3├── page.tsx # Home page4├── globals.css # Global styles5├── api/6│ └── cognitive/7│ └── route.ts # Main API endpoint8├── components/9│ ├── ui/ # shadcn/ui components10│ ├── cognitive/11│ │ ├── ChatInterface.tsx12│ │ ├── PersonaBadge.tsx13│ │ ├── StreamingOutput.tsx14│ │ ├── RoutingDetails.tsx15│ │ ├── BlendIndicator.tsx16│ │ ├── ConfidenceMeter.tsx17│ │ └── StateBadge.tsx18│ └── layout/19├── lib/20│ ├── utils.ts # Utility functions21│ ├── api.ts # API client22│ └── types.ts # TypeScript types23└── hooks/24 ├── useStreaming.ts # Streaming response hook25 └── useCognitive.ts # Main cognitive interaction hook
Custom Hooks
useCognitive
The main hook for interacting with the cognitive API:
typescript1// hooks/useCognitive.ts2import { useState, useCallback } from 'react'34interface UseCognitiveOptions {5 onChunk?: (chunk: string) => void6 onComplete?: (response: CognitiveResponse) => void7 onError?: (error: Error) => void8}910interface CognitiveResponse {11 content: string12 metadata: {13 personas_used: string[]14 confidence: number15 feature_vector: Record<string, number>16 }17}1819export function useCognitive(options: UseCognitiveOptions = {}) {20 const [isStreaming, setIsStreaming] = useState(false)2122 const sendMessage = useCallback(async (prompt: string) => {23 setIsStreaming(true)2425 try {26 const response = await fetch('/api/cognitive', {27 method: 'POST',28 headers: { 'Content-Type': 'application/json' },29 body: JSON.stringify({ prompt })30 })3132 if (!response.ok) {33 throw new Error(`HTTP error: ${response.status}`)34 }3536 const reader = response.body?.getReader()37 const decoder = new TextDecoder()38 let fullContent = ''3940 if (!reader) {41 throw new Error('No response body')42 }4344 while (true) {45 const { done, value } = await reader.read()4647 if (done) break4849 const chunk = decoder.decode(value)50 const lines = chunk.split('\n').filter(Boolean)5152 for (const line of lines) {53 if (line === 'data: [DONE]') continue5455 try {56 const data = JSON.parse(line.replace('data: ', ''))57 if (data.token) {58 fullContent += data.token59 options.onChunk?.(data.token)60 }61 } catch {62 // Skip malformed data63 }64 }65 }6667 // Get full response for metadata68 const fullResponse = await fetch('/api/cognitive', {69 method: 'POST',70 headers: { 'Content-Type': 'application/json' },71 body: JSON.stringify({ prompt, getMetadata: true })72 }).then(r => r.json())7374 options.onComplete?.({75 content: fullContent,76 metadata: fullResponse.metadata77 })7879 } catch (error) {80 options.onError?.(error as Error)81 } finally {82 setIsStreaming(false)83 }84 }, [options])8586 return { sendMessage, isStreaming }87}
8. API Layer Implementation
The API layer connects the frontend to the cognitive processing engine, handling request validation, streaming responses, and error handling.
Main API Route
typescript1// app/api/cognitive/route.ts2import { NextRequest } from 'next/server'3import { CognitiveGraphFSM } from '@/lib/fsm/cognitive-graph-fsm'4import { generateStream } from '@/lib/ollama/client'56export const runtime = 'nodejs'78export async function POST(req: NextRequest) {9 try {10 const { prompt } = await req.json()1112 if (!prompt || typeof prompt !== 'string') {13 return Response.json(14 { error: 'Prompt is required' },15 { status: 400 }16 )17 }1819 // Build and run the cognitive graph20 const fsm = new CognitiveGraphFSM(prompt)21 const context = await fsm.run()2223 if (context.currentState === 'ERROR') {24 return Response.json(25 { error: context.error || 'Processing failed' },26 { status: 500 }27 )28 }2930 // Create streaming response31 const stream = new ReadableStream({32 async start(controller) {33 const encoder = new TextEncoder()34 const finalOutput = context.finalOutput || ''3536 // Stream the final output token by token37 for (const token of finalOutput.split('')) {38 controller.enqueue(39 encoder.encode(`data: ${JSON.stringify({ token })}\n\n`)40 )41 await new Promise(r => setTimeout(r, 20))42 }4344 // Send metadata at the end45 controller.enqueue(46 encoder.encode(`data: ${JSON.stringify({47 metadata: {48 personas_used: context.activePersonas?.map(p => p.persona_name),49 confidence: context.confidenceScore,50 feature_vector: context.featureVector51 }52 })}\n\n`)53 )5455 controller.enqueue(encoder.encode('data: [DONE]\n\n'))56 controller.close()57 },58 })5960 return new Response(stream, {61 headers: {62 'Content-Type': 'text/event-stream',63 'Cache-Control': 'no-cache',64 'Connection': 'keep-alive',65 },66 })6768 } catch (error) {69 console.error('API Error:', error)70 return Response.json(71 { error: 'Internal server error' },72 { status: 500 }73 )74 }75}
9. Deployment and Production
Deploying the Cognitive Graph application requires consideration of the local Ollama dependency and the real-time nature of the responses.
Prerequisites
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores | 8+ cores |
| RAM | 8 GB | 16 GB |
| Storage | 20 GB SSD | 50+ GB SSD |
| GPU | Optional | NVIDIA 8GB+ |
Environment Configuration
bash1# .env.local23# Ollama Configuration4OLLAMA_BASE_URL=http://localhost:114345CLASSIFIER_MODEL=phi36PERSONA_MODEL=mistral7CRITIQUE_MODEL=mistral8SYNTHESIS_MODEL=mistral910# Application11NODE_ENV=development12NEXT_PUBLIC_API_URL=http://localhost:3000
Running the Application
bash1# Install dependencies2npm install34# Start Ollama5ollama serve67# Pull required models8ollama pull mistral9ollama pull phi31011# Start development server12npm run dev
10. Conclusion
Building a Cognitive Graph AI application is an exercise in architectural thinking—breaking down complex cognitive processes into modular, composable pieces that can be orchestrated, parallelized, and observed.
What We've Built
Throughout this guide, we've constructed a complete cognitive routing system:
- A layered architecture that separates concerns from presentation to inference
- A Finite State Machine that provides deterministic control over execution flow
- A Directed Acyclic Graph that models cognitive operations and their dependencies
- Seven distinct personas that represent different reasoning perspectives
- An intelligent routing system that automatically selects and blends personas based on input analysis
- A modern Next.js frontend with real-time streaming and beautiful animations
- Complete API integration with Ollama for local LLM inference
Key Takeaways
- Modularity matters: By decomposing cognition into personas, you get specialized reasoning without building separate systems
- Determinism enables reliability: The FSM ensures predictable behavior, making debugging possible
- Parallelism enables speed: Multiple personas can reason simultaneously, improving response times
- Visibility enables trust: By exposing routing decisions, confidence scores, and feature vectors, users can understand and trust the system
Future Enhancements
The architecture supports many extensions:
- Memory systems: Add persistent context across conversations
- Dynamic persona creation: Allow users to define custom personas
- Multi-modal inputs: Extend to handle images, audio, and other inputs
- Distributed execution: Scale to multiple Ollama instances for higher throughput
- Advanced critiquing: Implement iterative refinement loops
Getting Started Today
To build this system yourself:
- Install Ollama and pull the required models
- Create a Next.js project with TypeScript
- Implement the persona registry and FSM
- Build the API routes
- Create the frontend components
- Deploy and iterate
The Cognitive Graph architecture represents a fundamental shift in how we think about AI systems—not as monolithic black boxes, but as observable, controllable, and infinitely extensible cognitive engines.
Appendix: Quick Reference
NPM Dependencies
json1{2 "dependencies": {3 "next": "^14.0.0",4 "react": "^18.2.0",5 "react-dom": "^18.2.0",6 "ollama": "^0.1.0",7 "framer-motion": "^10.0.0",8 "tailwindcss": "^3.4.0",9 "typescript": "^5.0.0"10 }11}
Commands
bash1# Start development2npm run dev34# Build for production5npm run build67# Start production8npm start910# Check health11curl http://localhost:3000/api/health

Sovereign AI: An Architectural Investigation into Local-First Intelligence
by Daniel Kliewer · Paperback · 72 pages
An examination of the architecture of intelligence that you own — from first principles through production deployment.