·23 min

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.

DK

Daniel Kliewer

Author, Sovereign AI

aicognitive-architectureollamanextjspersonasllm
Sovereign AI book cover

From the Book

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

Get the Book — $88
Building a Cognitive Graph AI Application: A Comprehensive Guide

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

  1. Understanding the Core Philosophy
  2. System Architecture Overview
  3. The Persona System
  4. Building the Finite State Machine
  5. Implementing the Directed Acyclic Graph
  6. Ollama Integration
  7. The Next.js Frontend
  8. API Layer Implementation
  9. Deployment and Production
  10. 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:

  1. 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.

  2. 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.

  3. Parallel Cognition: Multiple persona nodes can execute concurrently, enabling multi-perspective reasoning without sequential bottlenecks.

  4. 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

PrincipleDescription
Modular CognitionDecouple reasoning style from inference engine
Adaptive RoutingAutomatically select optimal persona(s) based on input features
Multi-Perspective SynthesisBlend multiple persona outputs into coherent responses
Production SafetyBound cost, depth, and complexity deterministically
Debuggable ReasoningMake 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

text
1┌─────────────────────────────────────────────────────────────────────┐
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

LayerTechnologyPurpose
FrontendNext.js 16+UI framework, API routes
StylingTailwindCSSUtility-first styling
AnimationFramer MotionComplex animations, transitions
Componentsshadcn/uiAccessible, composable UI
InferenceOllamaLocal LLM engine
RuntimeTypeScriptType safety, interfaces

How Data Flows Through the System

  1. User submits prompt → API layer receives request
  2. FSM initializes → Creates GraphContext, transitions to CLASSIFYING
  3. Classifier executes → Ollama generates FeatureVector
  4. Scoring executes → Dot product of FeatureVector × Persona weight vectors
  5. Activation executes → Persona selection + optional blending
  6. Persona nodes execute → Parallel Ollama calls for each active persona
  7. Critique executes (optional) → Evaluate outputs
  8. Synthesis executes → Merge outputs, remove persona traces
  9. Streaming output → Stream final response to frontend
  10. 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 IDArchetypeCore Strength
systems_stoicStructural AnalystAnalytical, logical reasoning
konrad_freemanDeep CriticCritical analysis, institutional critique
tactical_operatorExecution PlannerAction-oriented, practical planning
vision_architectStrategic ThinkerLong-term vision, possibility mapping
existential_diverIntrospective GuideMeaning-finding, emotional depth
social_navigatorInterpersonal ExpertRelationship dynamics, social intelligence
creative_disruptorPattern BreakerNovel approaches, unconventional thinking

Persona Schema

Each persona follows a structured JSON format that defines its identity, behavior, and activation conditions:

typescript
1interface Persona {
2 persona_name: string
3 identity: {
4 archetype: string
5 core_motivation: string
6 worldview: string
7 emotional_posture: string
8 }
9 core_rules: string[]
10 tone_constraints: {
11 deadpan_level: number // 0-1
12 emotional_display: number // 0-1
13 sarcasm: number // 0-1
14 intellectualization: number // 0-1
15 exaggeration: number // 0-1
16 }
17 humor_mechanism?: {
18 primary: string
19 secondary: string
20 release_style: string
21 }
22 structure_pattern?: string
23 trait_vector: {
24 creativity: number
25 risk_tolerance: number
26 analysis_depth: number
27 empathy: number
28 meta_awareness: number
29 structural_thinking: number
30 discipline: number
31 novelty_bias: number
32 moral_aggression: number
33 }
34 activation_conditions: {
35 min_feature_score?: number
36 required_features?: string[]
37 excluded_features?: string[]
38 }
39 failure_modes: string[]
40 forbidden_behaviors?: string[]
41 example_internal_instruction: string
42}

Example Persona: Systems Stoic

Here's a complete example persona definition:

json
1{
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.2
23 },
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.35
54 },
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:

text
1personas/
2├── systemsStoic.json
3├── konradFreeman.json
4├── tacticalOperator.json
5├── visionArchitect.json
6├── existentialDiver.json
7├── socialNavigator.json
8└── creativeDisruptor.json

A persona registry loads all personas at runtime:

typescript
1// lib/personas/registry.ts
2import fs from 'fs'
3import path from 'path'
4
5interface PersonaRegistry {
6 getAll(): Persona[]
7 getById(id: string): Persona | undefined
8}
9
10class PersonaRegistryImpl implements PersonaRegistry {
11 private personas: Map<string, Persona> = new Map()
12
13 constructor() {
14 this.loadPersonas()
15 }
16
17 private loadPersonas() {
18 const personasDir = path.join(process.cwd(), 'personas')
19 const files = fs.readdirSync(personasDir).filter(f => f.endsWith('.json'))
20
21 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 }
28
29 getAll(): Persona[] {
30 return Array.from(this.personas.values())
31 }
32
33 getById(id: string): Persona | undefined {
34 return this.personas.get(id)
35 }
36}
37
38export 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:

typescript
1export 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

text
1IDLE ──(user input)──► CLASSIFYING ──(complete)──► SCORING ──(complete)──► ACTIVATING
2 ▲ │
3 │ ▼
4 │ ┌──────────────────────────┐
5 │ │ EXECUTING_PERSONAS │
6 │ │ (sequential or parallel)│
7 │ └──────────────────────────┘
8 │ │
9 │ (critique required?)
10 │ ↓ ↓
11 │ CRITIQUING SYNTHESIZING
12 │ │ │
13 │ └────────┬────────┘
14 │ ▼
15 │ STREAMING_OUTPUT ──► COMPLETE
16 │ │
17 │ ▼
18 │ ERROR (on failure)

Implementing the FSM

Here's the core FSM implementation:

typescript
1// lib/fsm/cognitive-graph-fsm.ts
2import { GraphStateType } from './types'
3import { GraphContext, FeatureVector, Persona } from './types'
4import { PERSONA_REGISTRY } from '../personas/registry'
5import { callOllamaClassifier, callOllamaPersona, callOllamaCritique, callOllamaSynthesis } from '../ollama/client'
6
7const MAX_PERSONAS = 3
8
9export class CognitiveGraphFSM {
10 private context: GraphContext
11
12 constructor(initialPrompt: string) {
13 this.context = {
14 currentState: GraphStateType.IDLE,
15 inputPrompt: initialPrompt,
16 totalTokenEstimate: 0
17 }
18 }
19
20 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)
26
27 if (this.shouldCritique()) {
28 await this.transition(GraphStateType.CRITIQUING)
29 }
30
31 await this.transition(GraphStateType.SYNTHESIZING)
32 await this.transition(GraphStateType.STREAMING_OUTPUT)
33 await this.transition(GraphStateType.COMPLETE)
34
35 return this.context
36 } catch (err: any) {
37 this.context.currentState = GraphStateType.ERROR
38 this.context.error = err.message
39 return this.context
40 }
41 }
42
43 private async transition(next: GraphStateType) {
44 switch (next) {
45 case GraphStateType.CLASSIFYING:
46 await this.classify()
47 break
48 case GraphStateType.SCORING:
49 this.scorePersonas()
50 break
51 case GraphStateType.ACTIVATING:
52 this.activatePersonas()
53 break
54 case GraphStateType.EXECUTING_PERSONAS:
55 await this.executePersonas()
56 break
57 case GraphStateType.CRITIQUING:
58 await this.critique()
59 break
60 case GraphStateType.SYNTHESIZING:
61 await this.synthesize()
62 break
63 case GraphStateType.STREAMING_OUTPUT:
64 this.prepareStreaming()
65 break
66 case GraphStateType.COMPLETE:
67 break
68 default:
69 throw new Error("Invalid transition")
70 }
71
72 this.context.currentState = next
73 }
74
75 private async classify() {
76 const result = await callOllamaClassifier(this.context.inputPrompt)
77 this.context.featureVector = result
78 }
79
80 private scorePersonas() {
81 const scores: Record<string, number> = {}
82 const input = this.context.featureVector!
83
84 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] || 0
87 return sum + input[key as keyof FeatureVector] * weight
88 }, 0)
89
90 scores[persona.persona_name] = score
91 }
92
93 this.context.personaScores = scores
94 }
95
96 private activatePersonas() {
97 const scores = this.context.personaScores!
98
99 const sorted = Object.entries(scores)
100 .sort((a, b) => b[1] - a[1])
101
102 const getPersona = (id: string) => PERSONA_REGISTRY.getAll()
103 .find(p => p.persona_name === id)!
104
105 const primary = getPersona(sorted[0][0])
106 const active: Persona[] = [primary]
107
108 // 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 }
112
113 // Auto-injection rules
114 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 }
121
122 // Cap at MAX_PERSONAS
123 const capped = active.slice(0, MAX_PERSONAS)
124
125 this.context.activePersonas = capped
126
127 // Compute confidence
128 const primaryScore = sorted[0][1]
129 const secondScore = sorted[1]?.[1] || 0
130 this.context.confidenceScore = primaryScore - secondScore
131 }
132
133 private async executePersonas() {
134 const outputs: Record<string, string> = {}
135
136 // Execute in parallel for speed
137 const promises = this.context.activePersonas!.map(async (persona) => {
138 const output = await callOllamaPersona(persona, this.context.inputPrompt)
139 return [persona.persona_name, output] as const
140 })
141
142 const results = await Promise.all(promises)
143 this.context.personaOutputs = Object.fromEntries(results)
144 }
145
146 private shouldCritique(): boolean {
147 if (!this.context.featureVector) return false
148
149 return (
150 this.context.featureVector.institutional_critique > 0.7 ||
151 this.context.confidenceScore! < 0.15
152 )
153 }
154
155 private async critique() {
156 const critique = await callOllamaCritique(this.context.personaOutputs!)
157 this.context.critiqueOutput = critique
158 }
159
160 private async synthesize() {
161 const final = await callOllamaSynthesis({
162 personas: this.context.personaOutputs!,
163 critique: this.context.critiqueOutput
164 })
165
166 this.context.finalOutput = final
167 }
168
169 private prepareStreaming() {
170 // Streaming is handled at the API layer
171 }
172}

Graph Context

The GraphContext maintains state throughout execution:

typescript
1interface GraphContext {
2 // Input
3 inputPrompt: string
4
5 // Classification
6 featureVector?: FeatureVector
7
8 // Scoring
9 personaScores?: Record<string, number>
10
11 // Activation
12 activePersonas?: Persona[]
13 blendConfig?: BlendConfig
14
15 // Execution
16 personaOutputs?: Record<string, string>
17
18 // Critique
19 critiqueOutput?: CritiqueOutput
20
21 // Synthesis
22 finalOutput?: string
23
24 // Metadata
25 confidenceScore?: number
26 totalTokenEstimate: number
27 currentState: GraphStateType
28 error?: string
29 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

typescript
1const 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

typescript
1const 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 flow
18 ["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"],
26
27 // 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"],
35
36 // Critique to synthesis
37 ["Critique", "Synthesis"],
38
39 // Synthesis to output
40 ["Synthesis", "Output"]
41 ]
42}

Node Types

The system defines several node types:

  1. Input Node: Entry point for user prompts
  2. Classifier Node: Analyzes input and extracts feature vector
  3. Persona Nodes (7 total): Generate reasoning from each persona's perspective
  4. Critique Node: Evaluates persona outputs for flaws, tone issues, depth
  5. Synthesis Node: Merges multiple persona outputs into coherent response
  6. Output Node: Final response rendering with streaming support

Feature Vector

The classifier extracts an 8-dimensional feature vector from the input:

typescript
1interface FeatureVector {
2 emotional_intensity: number // 0-1
3 urgency: number // 0-1
4 self_reflection: number // 0-1
5 institutional_critique: number // 0-1
6 creative_request: number // 0-1
7 strategic_planning: number // 0-1
8 social_navigation: number // 0-1
9 execution_need: number // 0-1
10}

Parallel Execution Model

One of the key advantages of the DAG approach is parallel execution:

text
1┌─────────────┐
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:

bash
1# macOS
2brew install ollama
3
4# Linux
5curl -fsSL https://ollama.com/install.sh | sh

Then pull the required models:

bash
1# Default model for general purpose
2ollama pull mistral
3
4# Lightweight model for classification
5ollama pull phi3
6
7# Higher quality (if hardware allows)
8ollama pull llama3

The Ollama Client

Here's the complete Ollama integration client:

typescript
1// lib/ollama/client.ts
2import { generate, generateStream } from 'ollama'
3
4export interface OllamaGenerateRequest {
5 model: string
6 prompt: string
7 system?: string
8 options?: {
9 temperature?: number
10 top_p?: number
11 top_k?: number
12 num_predict?: number
13 stop?: string[]
14 }
15}
16
17export async function generate(
18 request: OllamaGenerateRequest
19): 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 })
30
31 if (!response.ok) {
32 throw new Error(`Ollama error: ${response.statusText}`)
33 }
34
35 return response.json()
36}
37
38export async function* generateStream(
39 request: OllamaGenerateRequest
40): 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 })
51
52 if (!response.ok) {
53 throw new Error(`Ollama error: ${response.statusText}`)
54 }
55
56 if (!response.body) {
57 throw new Error('No response body')
58 }
59
60 const reader = response.body.getReader()
61 const decoder = new TextDecoder()
62
63 while (true) {
64 const { done, value } = await reader.read()
65
66 if (done) break
67
68 const chunk = decoder.decode(value)
69 const lines = chunk.split('\n').filter(Boolean)
70
71 for (const line of lines) {
72 const data = JSON.parse(line)
73 if (data.response) {
74 yield data.response
75 }
76 if (data.done) break
77 }
78 }
79}

System Prompt Construction

Each cognitive operation requires a specially crafted prompt. Here's how to build them:

Persona System Prompt

typescript
1function buildPersonaSystemPrompt(persona: Persona): string {
2 return `You are operating in persona mode.
3
4Primary Persona: ${persona.persona_name}
5
6Core 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}
11
12Core Rules:
13${persona.core_rules.map((rule, i) => `${i + 1}. ${rule}`).join('\n')}
14
15Tone Constraints:
16${Object.entries(persona.tone_constraints)
17 .map(([key, value]) => `- ${key}: ${value}`)
18 .join('\n')}
19
20Failure Modes To Avoid:
21${persona.failure_modes.map(mode => `- ${mode}`).join('\n')}
22
23${persona.forbidden_behaviors ? `Forbidden Behaviors:\n${persona.forbidden_behaviors.map(b => `- ${b}`).join('\n')}` : ''}
24
25Internal Instruction:
26${persona.example_internal_instruction}
27
28Remember: Stay in character as ${persona.persona_name}. Do not break persona.`
29}

Classifier Prompt

typescript
1const CLASSIFIER_SYSTEM_PROMPT = `You are a routing classifier. Your task is to analyze user input and extract a feature vector.
2
3Analyze 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?
12
13Return ONLY valid JSON with values between 0 and 1.`
14
15function buildClassifierPrompt(userInput: string): string {
16 return `${CLASSIFIER_SYSTEM_PROMPT}
17
18User input: """
19${userInput}
20"""
21
22Output JSON:`
23}

Critique Prompt

typescript
1const CRITIQUE_SYSTEM_PROMPT = `You are a critique engine. Analyze the following outputs and provide structured feedback.`
2
3function 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')
11
12 return `${CRITIQUE_SYSTEM_PROMPT}
13
14Analyze the following persona outputs:
15
16${outputsText}
17
18Provide 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/false
26}
27
28Output only valid JSON:`
29}

Synthesis Prompt

typescript
1const SYNTHESIS_SYSTEM_PROMPT = `You are a synthesis node. Your task is to merge multiple perspectives into a single, coherent response.`
2
3function buildSynthesisPrompt(
4 personaOutputs: Record<string, string>,
5 critique?: CritiqueOutput
6): string {
7 const outputsText = Object.entries(personaOutputs)
8 .map(([persona, output]) =>
9 `--- Perspective from ${persona} ---\n${output}\n`
10 )
11 .join('\n')
12
13 const critiqueSection = critique
14 ? `\n=== CRITIQUE FEEDBACK (address these) ===\n${critique.suggestions.join('\n')}\n`
15 : ''
16
17 return `${SYNTHESIS_SYSTEM_PROMPT}
18
19${outputsText}${critiqueSection}
20
21Requirements:
221. Merge these perspectives into ONE coherent response
232. NO mention of which personas were used
243. Maintain a unified voice
254. Address critique suggestions if present
265. Be clear, direct, and helpful
27
28Produce your final response:`
29}

Making the Ollama Calls

typescript
1export async function callOllamaClassifier(
2 prompt: string
3): 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 })
12
13 try {
14 return JSON.parse(response.response)
15 } catch {
16 // Fallback for malformed responses
17 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}
29
30export async function callOllamaPersona(
31 persona: Persona,
32 userPrompt: string,
33 config?: { temperature?: number; maxTokens?: number }
34): Promise<string> {
35 const systemPrompt = buildPersonaSystemPrompt(persona)
36
37 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 })
47
48 return response.response
49}
50
51export 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 })
62
63 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}
76
77export async function callOllamaSynthesis(
78 inputs: {
79 personas: Record<string, string>
80 critique?: CritiqueOutput
81 }
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 })
91
92 return response.response
93}

7. The Next.js Frontend

The frontend provides a modern, responsive interface with real-time streaming, animations, and detailed routing analysis visualization.

Project Structure

text
1app/
2├── layout.tsx # Root layout
3├── page.tsx # Home page
4├── globals.css # Global styles
5├── api/
6│ └── cognitive/
7│ └── route.ts # Main API endpoint
8├── components/
9│ ├── ui/ # shadcn/ui components
10│ ├── cognitive/
11│ │ ├── ChatInterface.tsx
12│ │ ├── PersonaBadge.tsx
13│ │ ├── StreamingOutput.tsx
14│ │ ├── RoutingDetails.tsx
15│ │ ├── BlendIndicator.tsx
16│ │ ├── ConfidenceMeter.tsx
17│ │ └── StateBadge.tsx
18│ └── layout/
19├── lib/
20│ ├── utils.ts # Utility functions
21│ ├── api.ts # API client
22│ └── types.ts # TypeScript types
23└── hooks/
24 ├── useStreaming.ts # Streaming response hook
25 └── useCognitive.ts # Main cognitive interaction hook

Custom Hooks

useCognitive

The main hook for interacting with the cognitive API:

typescript
1// hooks/useCognitive.ts
2import { useState, useCallback } from 'react'
3
4interface UseCognitiveOptions {
5 onChunk?: (chunk: string) => void
6 onComplete?: (response: CognitiveResponse) => void
7 onError?: (error: Error) => void
8}
9
10interface CognitiveResponse {
11 content: string
12 metadata: {
13 personas_used: string[]
14 confidence: number
15 feature_vector: Record<string, number>
16 }
17}
18
19export function useCognitive(options: UseCognitiveOptions = {}) {
20 const [isStreaming, setIsStreaming] = useState(false)
21
22 const sendMessage = useCallback(async (prompt: string) => {
23 setIsStreaming(true)
24
25 try {
26 const response = await fetch('/api/cognitive', {
27 method: 'POST',
28 headers: { 'Content-Type': 'application/json' },
29 body: JSON.stringify({ prompt })
30 })
31
32 if (!response.ok) {
33 throw new Error(`HTTP error: ${response.status}`)
34 }
35
36 const reader = response.body?.getReader()
37 const decoder = new TextDecoder()
38 let fullContent = ''
39
40 if (!reader) {
41 throw new Error('No response body')
42 }
43
44 while (true) {
45 const { done, value } = await reader.read()
46
47 if (done) break
48
49 const chunk = decoder.decode(value)
50 const lines = chunk.split('\n').filter(Boolean)
51
52 for (const line of lines) {
53 if (line === 'data: [DONE]') continue
54
55 try {
56 const data = JSON.parse(line.replace('data: ', ''))
57 if (data.token) {
58 fullContent += data.token
59 options.onChunk?.(data.token)
60 }
61 } catch {
62 // Skip malformed data
63 }
64 }
65 }
66
67 // Get full response for metadata
68 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())
73
74 options.onComplete?.({
75 content: fullContent,
76 metadata: fullResponse.metadata
77 })
78
79 } catch (error) {
80 options.onError?.(error as Error)
81 } finally {
82 setIsStreaming(false)
83 }
84 }, [options])
85
86 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

typescript
1// app/api/cognitive/route.ts
2import { NextRequest } from 'next/server'
3import { CognitiveGraphFSM } from '@/lib/fsm/cognitive-graph-fsm'
4import { generateStream } from '@/lib/ollama/client'
5
6export const runtime = 'nodejs'
7
8export async function POST(req: NextRequest) {
9 try {
10 const { prompt } = await req.json()
11
12 if (!prompt || typeof prompt !== 'string') {
13 return Response.json(
14 { error: 'Prompt is required' },
15 { status: 400 }
16 )
17 }
18
19 // Build and run the cognitive graph
20 const fsm = new CognitiveGraphFSM(prompt)
21 const context = await fsm.run()
22
23 if (context.currentState === 'ERROR') {
24 return Response.json(
25 { error: context.error || 'Processing failed' },
26 { status: 500 }
27 )
28 }
29
30 // Create streaming response
31 const stream = new ReadableStream({
32 async start(controller) {
33 const encoder = new TextEncoder()
34 const finalOutput = context.finalOutput || ''
35
36 // Stream the final output token by token
37 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 }
43
44 // Send metadata at the end
45 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.featureVector
51 }
52 })}\n\n`)
53 )
54
55 controller.enqueue(encoder.encode('data: [DONE]\n\n'))
56 controller.close()
57 },
58 })
59
60 return new Response(stream, {
61 headers: {
62 'Content-Type': 'text/event-stream',
63 'Cache-Control': 'no-cache',
64 'Connection': 'keep-alive',
65 },
66 })
67
68 } 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

ComponentMinimumRecommended
CPU4 cores8+ cores
RAM8 GB16 GB
Storage20 GB SSD50+ GB SSD
GPUOptionalNVIDIA 8GB+

Environment Configuration

bash
1# .env.local
2
3# Ollama Configuration
4OLLAMA_BASE_URL=http://localhost:11434
5CLASSIFIER_MODEL=phi3
6PERSONA_MODEL=mistral
7CRITIQUE_MODEL=mistral
8SYNTHESIS_MODEL=mistral
9
10# Application
11NODE_ENV=development
12NEXT_PUBLIC_API_URL=http://localhost:3000

Running the Application

bash
1# Install dependencies
2npm install
3
4# Start Ollama
5ollama serve
6
7# Pull required models
8ollama pull mistral
9ollama pull phi3
10
11# Start development server
12npm 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:

  1. A layered architecture that separates concerns from presentation to inference
  2. A Finite State Machine that provides deterministic control over execution flow
  3. A Directed Acyclic Graph that models cognitive operations and their dependencies
  4. Seven distinct personas that represent different reasoning perspectives
  5. An intelligent routing system that automatically selects and blends personas based on input analysis
  6. A modern Next.js frontend with real-time streaming and beautiful animations
  7. 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:

  1. Install Ollama and pull the required models
  2. Create a Next.js project with TypeScript
  3. Implement the persona registry and FSM
  4. Build the API routes
  5. Create the frontend components
  6. 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

json
1{
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

bash
1# Start development
2npm run dev
3
4# Build for production
5npm run build
6
7# Start production
8npm start
9
10# Check health
11curl http://localhost:3000/api/health

Sovereign AI: An Architectural Investigation into Local-First Intelligence by Daniel Kliewer

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.