Files

293 lines
13 KiB
TypeScript
Raw Permalink Normal View History

'use client'
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import { motion } from 'framer-motion'
const skillCategories = [
{
title: 'Core Runtime',
skills: ['Rust', 'actix-web', 'tokio', 'serde'],
},
{
title: 'Storage & Search',
skills: ['PostgreSQL (CloudNativePG)', 'pgvector (HNSW)', 'Cosine Similarity', 'BFS Graph Traversal'],
},
{
title: 'LLM & Embeddings',
skills: ['Ollama (ornith:35b)', 'TEI (nomic-embed)', '768-dim Embeddings'],
},
{
title: 'Identity & Security',
skills: ['Authentik OIDC', 'JWT Verification', 'SOPS Encrypted Secrets', 'RBAC'],
},
{
title: 'Data Pipeline',
skills: ['Fixed-Window Chunking', 'Entity Extraction', 'Fact Extraction', 'Reflection Filtering'],
},
{
title: 'Infrastructure',
skills: ['Kubernetes', 'ArgoCD', 'Kustomize', 'cert-manager'],
},
]
const sectionVariants = {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } },
}
export default function PoimenMemoryPage() {
return (
<main className="min-h-screen bg-white dark:bg-gray-950">
<div className="max-w-6xl mx-auto px-6 py-20">
<Link
href="/#project-poimen-memory"
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-8"
>
<ArrowLeft size={16} />
Back to Projects
</Link>
{/* Hero */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="mb-16"
>
<div className="text-sm font-semibold text-purple-600 dark:text-purple-400 uppercase tracking-wide mb-2">
Poimen (Ποιμήν) · Memory System
</div>
<h1 className="text-5xl font-bold text-gray-900 dark:text-white mb-4">
Teaching the Shepherd to Remember
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 mb-6">
A Graph-RAG memory system that gives AI agents long-term recall, semantic search, and self-compacting knowledge.
</p>
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
Agents forget. Every conversation starts from zero unless you build memory into the system.
Poimen Memory is a <strong>Rust-based Graph-RAG service</strong> that ingests conversations,
extracts entities and relationships via LLM, stores them in a <strong>temporal knowledge graph</strong> backed
by <strong>pgvector</strong>, and serves them back through <strong>three-tier retrieval</strong> — signature
match at 50ms, graph-boosted hybrid search, and Obsidian fallback. The cache self-compacts:
stale embeddings are evicted, frequently-accessed chunks are promoted, and the graph auto-reconciles
on every write.
</p>
</motion.div>
{/* Section 1: Architecture Overview */}
<motion.section
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="mb-16 border-l-4 border-purple-500 pl-8"
>
<div className="mb-6">
<div className="text-sm font-semibold text-purple-600 dark:text-purple-400 uppercase tracking-wide mb-2">
System Design
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Architecture Overview
</h2>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none mb-6">
<p className="text-gray-700 dark:text-gray-300">
The memory system runs as a <strong>Rust microservice</strong> inside the Kubernetes <code>poimen</code> namespace.
An <strong>actix-web</strong> API server handles HTTP requests, authenticates via <strong>Authentik OIDC JWT</strong>,
and routes to either the <strong>search path</strong> (direct pgvector query) or the <strong>ingest path</strong> (async
pipeline through an internal queue → LLM extraction → graph persistence). All data lives in a
<strong> CloudNativePG</strong> cluster with <strong>HNSW indexes</strong> for vector similarity.
</p>
</div>
{/* Architecture Diagram */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
🏗️ System Architecture
</h3>
<div className="rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700 shadow-lg">
<iframe
src="/poimen-memory/architecture.html"
className="w-full bg-white dark:bg-gray-900"
style={{ height: '700px', border: 'none' }}
title="Poimen Memory Architecture"
/>
</div>
</div>
</motion.section>
{/* Section 2: Ingest Pipeline */}
<motion.section
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="mb-16 border-l-4 border-emerald-500 pl-8"
>
<div className="mb-6">
<div className="text-sm font-semibold text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-2">
Data Flow
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Ingest Pipeline
</h2>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none mb-6">
<p className="text-gray-700 dark:text-gray-300">
Conversations arrive as <strong>episodes</strong> — raw message sequences. The pipeline splits into two
parallel LLM extraction paths: <strong>entity extraction</strong> (people, tools, concepts with type + summary)
and <strong>fact extraction</strong> (relationships between entity pairs as directed edges). A <strong>reflection
pass</strong> filters hallucinated entities before persistence. The result is a <strong>temporal knowledge
graph</strong> where every edge carries <code>t_valid</code> and <code>t_invalid</code> timestamps — knowledge
that knows when it was true.
</p>
</div>
{/* Dataflow Diagram */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
🔄 Ingest Data Flow
</h3>
<div className="rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700 shadow-lg">
<iframe
src="/poimen-memory/dataflow.html"
className="w-full bg-white dark:bg-gray-900"
style={{ height: '700px', border: 'none' }}
title="Poimen Ingest Pipeline"
/>
</div>
</div>
</motion.section>
{/* Section 3: Request Lifecycle */}
<motion.section
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="mb-16 border-l-4 border-cyan-500 pl-8"
>
<div className="mb-6">
<div className="text-sm font-semibold text-cyan-600 dark:text-cyan-400 uppercase tracking-wide mb-2">
Sequence
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Ingest Request Lifecycle
</h2>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none mb-6">
<p className="text-gray-700 dark:text-gray-300">
The ingest path is <strong>fully async</strong>. The agent sends a <code>POST /memory/ingest</code> and
gets a <strong>202 Accepted</strong> immediately — no blocking on LLM latency. A background worker
polls jobs from the queue, runs two sequential LLM calls (entity extraction → fact extraction),
cleans JSON responses of thinking tags and fences, then persists entities, edges, and embeddings
to pgvector. The agent can query the graph within seconds of ingestion completing.
</p>
</div>
{/* Sequence Diagram */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
⚡ Request Sequence
</h3>
<div className="rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700 shadow-lg">
<iframe
src="/poimen-memory/sequence.html"
className="w-full bg-white dark:bg-gray-900"
style={{ height: '700px', border: 'none' }}
title="Poimen Ingest Request Lifecycle"
/>
</div>
</div>
</motion.section>
{/* Section 4: Retrieval Strategy */}
<motion.section
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="mb-16 border-l-4 border-orange-500 pl-8"
>
<div className="mb-6">
<div className="text-sm font-semibold text-orange-600 dark:text-orange-400 uppercase tracking-wide mb-2">
Search
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Three-Tier Retrieval
</h2>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 not-prose">
<div className="bg-emerald-50 dark:bg-emerald-950/30 rounded-xl p-6 border border-emerald-200 dark:border-emerald-800">
<div className="text-2xl mb-2">⚡</div>
<h3 className="font-bold text-emerald-700 dark:text-emerald-400 mb-2">Tier 1: Signature Match</h3>
<p className="text-sm text-gray-700 dark:text-gray-300">
<strong>50ms</strong> exact entity lookup by name hash. Instant recall for known entities —
no embedding computation needed.
</p>
</div>
<div className="bg-cyan-50 dark:bg-cyan-950/30 rounded-xl p-6 border border-cyan-200 dark:border-cyan-800">
<div className="text-2xl mb-2">🔍</div>
<h3 className="font-bold text-cyan-700 dark:text-cyan-400 mb-2">Tier 2: Graph-Boosted Hybrid</h3>
<p className="text-sm text-gray-700 dark:text-gray-300">
<strong>HNSW cosine similarity</strong> + <strong>BFS graph traversal</strong>. RRF fusion ranks
results across vector matches and graph neighbors. Context-aware retrieval that follows relationships.
</p>
</div>
<div className="bg-orange-50 dark:bg-orange-950/30 rounded-xl p-6 border border-orange-200 dark:border-orange-800">
<div className="text-2xl mb-2">📚</div>
<h3 className="font-bold text-orange-700 dark:text-orange-400 mb-2">Tier 3: Obsidian Fallback</h3>
<p className="text-sm text-gray-700 dark:text-gray-300">
When graph search yields low confidence, falls back to <strong>wiki-link indexed</strong> Obsidian
vault. Bidirectional link traversal surfaces related notes the graph hasn&apos;t captured yet.
</p>
</div>
</div>
</div>
</motion.section>
{/* Skills Grid */}
<motion.section
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="mb-16"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-8">
Technology Stack
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{skillCategories.map((category) => (
<div
key={category.title}
className="bg-gray-50 dark:bg-gray-900 rounded-xl p-6 border border-gray-200 dark:border-gray-800"
>
<h3 className="text-base font-bold text-gray-900 dark:text-white mb-3">
{category.title}
</h3>
<div className="flex flex-wrap gap-2">
{category.skills.map((skill) => (
<span
key={skill}
className="text-sm px-3 py-1.5 rounded-full bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700"
>
{skill}
</span>
))}
</div>
</div>
))}
</div>
</motion.section>
</div>
</main>
)
}