Graphiti Temporal Memory
Bi-temporal knowledge graph for AI agent memory with Neo4j integration
Graphiti Temporal Memory
Status: Production | Version: Neo4j 5.15+ | Integration: Letta AI Agents
Graphiti provides temporal knowledge graph capabilities for Fixie AI agents, enabling sophisticated memory management with bi-temporal tracking, entity deduplication, and semantic search.
Overview
What is Graphiti?
Graphiti is a temporal knowledge graph system that stores and retrieves facts with full time awareness:
| Feature | Description |
|---|---|
| Bi-Temporal Tracking | When facts became true (event time) vs when they were recorded (transaction time) |
| Entity Deduplication | Intelligent merging of entities across episodes |
| Community Detection | Automatic clustering of related entities |
| Vector Embeddings | Semantic search across the knowledge graph |
| Group Isolation | Multi-tenant memory separation by agent/member/project |
Why Graphiti for Fixies?
Traditional LLM memory is either:
- Short-term (context window): Limited to current conversation
- Long-term (vector store): No relationship awareness
Graphiti combines both:
- Entities extracted from conversations
- Relationships between entities
- Temporal validity tracking
- Semantic search for recall
Architecture
Schema Overview
Graphiti Memory Architecture:
┌─────────────────────────────────────────────────────────────┐
│ Graphiti Subgraph │
│ │
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ GraphitiEpisode │ │ GraphitiEntity │ │
│ │ │ │ │ │
│ │ - Raw messages │───▶│ - Extracted entities │ │
│ │ - Source text │ │ - Deduplicated │ │
│ │ - Timestamps │ │ - Vector embeddings │ │
│ └─────────────────┘ └──────────┬──────────────────┘ │
│ │ │
│ GRAPHITI_RELATES_TO │
│ (bi-temporal edges) │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ GraphitiCommunity │ │
│ │ (entity clusters) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
GRAPHITI_LINKS_TO
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Domain Subgraph │
│ (Member, Circle, Role, Transaction, etc.) │
└─────────────────────────────────────────────────────────────┘Node Types
GraphitiEpisode
Raw input storage - messages, conversations, documents:
// Episode structure
(:GraphitiEpisode {
id: "ep-uuid",
group_id: "fixie:member:123",
source: "conversation",
content: "User mentioned they work at TechCorp...",
reference_time: datetime("2026-01-15T10:30:00Z"),
created_at: datetime()
})GraphitiEntity
Extracted, deduplicated entities:
// Entity structure
(:GraphitiEntity {
id: "ent-uuid",
group_id: "fixie:member:123",
name: "TechCorp",
entity_type: "Organization",
summary: "Technology company where user is employed",
created_at: datetime(),
embedding: [0.123, 0.456, ...] // 1536-dim vector
})GraphitiCommunity
Entity clusters for domain understanding:
// Community structure
(:GraphitiCommunity {
id: "comm-uuid",
group_id: "fixie:member:123",
name: "User's Work Context",
summary: "Entities related to user's employment"
})Bi-Temporal Model
Time Dimensions
Graphiti tracks two independent timelines:
| Dimension | Description | Properties |
|---|---|---|
| Event Time | When the fact became true in reality | t_valid, t_invalid |
| Transaction Time | When the fact was recorded in the system | t_created, t_expired |
Relationship Properties
// Bi-temporal relationship
(a:GraphitiEntity)-[r:GRAPHITI_RELATES_TO {
relationship_type: "WORKS_AT",
// Event timeline (real world)
t_valid: datetime("2024-06-01"), // When user started at TechCorp
t_invalid: null, // Still true (null = current)
// Transaction timeline (system)
t_created: datetime("2026-01-15"), // When we learned this
t_expired: null, // Not superseded
group_id: "fixie:member:123"
}]->(b:GraphitiEntity)Temporal Queries
// Find facts valid at a specific point in time
MATCH (a:GraphitiEntity)-[r:GRAPHITI_RELATES_TO]->(b:GraphitiEntity)
WHERE r.group_id = $groupId
AND r.t_valid <= datetime($queryTime)
AND (r.t_invalid IS NULL OR r.t_invalid > datetime($queryTime))
RETURN a, r, bGroup Isolation
Multi-Tenant Memory
Graphiti uses group_id for memory isolation:
| Pattern | Scope | Example |
|---|---|---|
fixie:member:{id} | Personal agent memory | User's individual context |
fixie:persona:{id} | Persona-specific memory | Professional vs personal |
fixie:project:{id} | Project-scoped memory | Team shared context |
fixie:circle:{id} | Circle agent memory | Department knowledge |
shared:circle:{id} | Circle-wide facts | Shared by all circle members |
shared:org | Organization-wide | Company-level knowledge |
Query Pattern
// Get entities for a specific agent's memory
MATCH (e:GraphitiEntity)
WHERE e.group_id = $agentGroupId
RETURN e.name, e.entity_type, e.summary
ORDER BY e.created_at DESC
LIMIT 50Cypher Examples
Entity Management
Create Entity from Episode
// Extract entity from conversation
MATCH (ep:GraphitiEpisode {id: $episodeId})
CREATE (e:GraphitiEntity {
id: randomUUID(),
group_id: ep.group_id,
name: $entityName,
entity_type: $entityType,
summary: $summary,
created_at: datetime()
})
CREATE (e)-[:GRAPHITI_MENTIONED_IN {
episode_id: ep.id
}]->(ep)
RETURN eDeduplicate Entities
// Merge similar entities
MATCH (e1:GraphitiEntity), (e2:GraphitiEntity)
WHERE e1.group_id = e2.group_id
AND e1.name = e2.name
AND e1.entity_type = e2.entity_type
AND id(e1) < id(e2)
// Merge relationships to e1
MATCH (e2)-[r:GRAPHITI_RELATES_TO]-(other)
CREATE (e1)-[r2:GRAPHITI_RELATES_TO]->(other)
SET r2 = properties(r)
// Delete duplicate
DELETE r, e2
RETURN count(*) as mergedRelationship Queries
Find Related Entities
// Get entity neighborhood
MATCH (e:GraphitiEntity {id: $entityId})
OPTIONAL MATCH (e)-[r:GRAPHITI_RELATES_TO]-(related:GraphitiEntity)
WHERE r.t_invalid IS NULL // Only current facts
RETURN e,
collect(DISTINCT {
entity: related,
relationship: r.relationship_type,
since: r.t_valid
}) as relationshipsTemporal Relationship History
// Track how a relationship changed over time
MATCH (a:GraphitiEntity {name: $entityA})-[r:GRAPHITI_RELATES_TO]->(b:GraphitiEntity {name: $entityB})
WHERE a.group_id = $groupId
RETURN r.relationship_type,
r.t_valid as valid_from,
r.t_invalid as valid_until,
r.t_created as recorded_at
ORDER BY r.t_createdMemory Retrieval
Semantic Search
// Find entities similar to query (requires vector index)
CALL db.index.vector.queryNodes(
'graphiti_entity_embedding',
5, // k nearest neighbors
$queryVector
)
YIELD node, score
WHERE node.group_id = $groupId
RETURN node.name, node.summary, scoreContext Assembly for Prompt
// Get relevant context for an agent response
MATCH (e:GraphitiEntity)
WHERE e.group_id = $groupId
WITH e
ORDER BY e.created_at DESC
LIMIT 20
OPTIONAL MATCH (e)-[r:GRAPHITI_RELATES_TO]-(related:GraphitiEntity)
WHERE r.t_invalid IS NULL
RETURN collect(DISTINCT e.summary) as entitySummaries,
collect(DISTINCT {
from: e.name,
relation: r.relationship_type,
to: related.name
}) as relationshipsCommunity Analysis
Find Entity Communities
// Get community members
MATCH (c:GraphitiCommunity {id: $communityId})
MATCH (e:GraphitiEntity)-[m:GRAPHITI_MEMBER_OF]->(c)
RETURN c.name as community,
c.summary as summary,
collect({
entity: e.name,
type: e.entity_type,
weight: m.weight
}) as members
ORDER BY m.weight DESCIntegration with Fixies
Letta Memory Architecture
Graphiti integrates with Letta's memory system:
Letta Memory Layers:
┌─────────────────────────────────────────────┐
│ Core Memory │
│ (Agent persona, user info, in-context) │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────▼──────────────────────┐
│ Archival Memory │
│ (Searchable vector store, unlimited) │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────▼──────────────────────┐
│ Graphiti External Memory (NEW) │
│ (Temporal knowledge graph, relationships) │
└─────────────────────────────────────────────┘Fixie Tool Integration
// Fixie tool for Graphiti search
const graphitiSearchTool = {
name: "search_memory_graph",
description: "Search temporal knowledge graph for facts and relationships",
parameters: {
query: "string - natural language query",
time_context: "string - 'current' | 'historical' | ISO timestamp"
},
handler: async ({ query, time_context }) => {
// Convert to vector
const embedding = await embedQuery(query);
// Search Graphiti
const results = await neo4j.run(`
CALL db.index.vector.queryNodes('graphiti_entity_embedding', 5, $embedding)
YIELD node, score
WHERE node.group_id = $groupId
RETURN node.name, node.summary, score
`, { embedding, groupId: agent.groupId });
return results;
}
};Tier Availability
| Tier | Graphiti Access | Features |
|---|---|---|
| Free | No | Basic conversation only |
| Standard | No | Core + archival memory |
| Premium | Yes | Full temporal graph + communities |
Indexes
Required Indexes
// Episode indexes
CREATE INDEX index_graphiti_episode_group IF NOT EXISTS
FOR (e:GraphitiEpisode) ON (e.group_id);
CREATE INDEX index_graphiti_episode_source IF NOT EXISTS
FOR (e:GraphitiEpisode) ON (e.source);
CREATE INDEX index_graphiti_episode_created IF NOT EXISTS
FOR (e:GraphitiEpisode) ON (e.created_at);
// Entity indexes
CREATE INDEX index_graphiti_entity_group IF NOT EXISTS
FOR (n:GraphitiEntity) ON (n.group_id);
CREATE INDEX index_graphiti_entity_name IF NOT EXISTS
FOR (n:GraphitiEntity) ON (n.name);
CREATE INDEX index_graphiti_entity_type IF NOT EXISTS
FOR (n:GraphitiEntity) ON (n.entity_type);
// Relationship indexes
CREATE INDEX index_graphiti_relates_to_valid IF NOT EXISTS
FOR ()-[r:GRAPHITI_RELATES_TO]-() ON (r.t_valid);
CREATE INDEX index_graphiti_relates_to_group IF NOT EXISTS
FOR ()-[r:GRAPHITI_RELATES_TO]-() ON (r.group_id);Vector Index (Neo4j 5.15+)
// Create vector index for semantic search
CREATE VECTOR INDEX graphiti_entity_embedding IF NOT EXISTS
FOR (n:GraphitiEntity) ON n.embedding
OPTIONS {
indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}
}Best Practices
Entity Extraction
- Be Specific: Extract concrete entities, not abstract concepts
- Include Context: Summary should explain relationship to user
- Deduplicate Early: Check for existing entities before creating
Temporal Management
- Invalidate, Don't Delete: Set
t_invalidinstead of removing edges - Record Source: Always link to originating episode
- Group Consistently: Use standard group_id patterns
Query Performance
- Filter by Group First: Always include
group_idin WHERE - Limit Results: Use LIMIT for exploration queries
- Use Indexes: Ensure queries hit indexed properties