graphzep
Version:
GraphZep: A temporal knowledge graph memory system for AI agents based on the Zep paper
521 lines (440 loc) • 17 kB
text/typescript
/**
* SPARQL Interface for GraphZep with full SPARQL 1.1 support
* Provides high-level query interface with Zep-specific extensions
*/
import { OptimizedRDFDriver, RDFTriple } from '../drivers/rdf-driver.js';
import { NamespaceManager } from './namespaces.js';
import { ZepMemory, ZepFact, ZepSearchParams, ZepSearchResult, MemoryType } from '../zep/types.js';
export interface SPARQLQueryOptions {
timeout?: number;
limit?: number;
offset?: number;
orderBy?: string;
descending?: boolean;
temporalFilter?: TemporalFilter;
confidenceThreshold?: number;
includeInferred?: boolean;
}
export interface TemporalFilter {
type: 'at' | 'between' | 'before' | 'after' | 'during';
timestamp?: Date;
startTime?: Date;
endTime?: Date;
validityCheck?: boolean; // Check if facts are valid at the given time
}
export interface SPARQLQueryResult {
bindings: Record<string, any>[];
count: number;
executionTime: number;
cached: boolean;
}
export class ZepSPARQLInterface {
private driver: OptimizedRDFDriver;
private nsManager: NamespaceManager;
constructor(driver: OptimizedRDFDriver, nsManager?: NamespaceManager) {
this.driver = driver;
this.nsManager = nsManager || new NamespaceManager();
}
/**
* Execute raw SPARQL query with automatic prefix addition
*/
async query(sparqlQuery: string, options: SPARQLQueryOptions = {}): Promise<SPARQLQueryResult> {
const startTime = Date.now();
// Add prefixes if not already present
const enhancedQuery = this.addZepPrefixes(sparqlQuery);
// Apply options to query
const finalQuery = this.applyQueryOptions(enhancedQuery, options);
try {
const results = await this.driver.executeQuery(finalQuery);
return {
bindings: results,
count: results.length,
executionTime: Date.now() - startTime,
cached: false // TODO: implement cache detection
};
} catch (error) {
throw new Error(`SPARQL query execution failed: ${error}`);
}
}
/**
* Search memories using SPARQL with Zep-specific optimizations
*/
async searchMemories(params: ZepSearchParams): Promise<ZepSearchResult[]> {
const query = this.buildMemorySearchQuery(params);
const result = await this.query(query, {
limit: params.limit,
temporalFilter: params.timeRange ? {
type: 'between',
startTime: params.timeRange.start,
endTime: params.timeRange.end
} : undefined
});
return this.formatSearchResults(result.bindings);
}
/**
* Get memories at a specific point in time
*/
async getMemoriesAtTime(timestamp: Date, memoryTypes?: MemoryType[]): Promise<ZepMemory[]> {
const typeFilter = memoryTypes ?
`FILTER(?type IN (${memoryTypes.map(t => `zep:${t.charAt(0).toUpperCase() + t.slice(1)}Memory`).join(', ')}))` :
'FILTER(?type IN (zep:EpisodicMemory, zep:SemanticMemory, zep:ProceduralMemory))';
const query = `
SELECT ?memory ?type ?uuid ?content ?confidence ?sessionId ?createdAt ?validFrom ?validUntil
WHERE {
?memory a ?type ;
zep:uuid ?uuid ;
zep:content ?content ;
zep:confidence ?confidence ;
zep:sessionId ?sessionId ;
zep:createdAt ?createdAt ;
zep:validFrom ?validFrom .
${typeFilter}
FILTER(?validFrom <= "${timestamp.toISOString()}"^^xsd:dateTime)
OPTIONAL {
?memory zep:validUntil ?validUntil .
FILTER(?validUntil > "${timestamp.toISOString()}"^^xsd:dateTime)
}
}
ORDER BY DESC(?confidence) DESC(?createdAt)
`;
const result = await this.query(query);
return this.formatMemoryResults(result.bindings);
}
/**
* Get facts about entities with temporal constraints
*/
async getFactsAboutEntity(entityName: string, validAt?: Date): Promise<ZepFact[]> {
const timeConstraint = validAt ? `
FILTER(?validFrom <= "${validAt.toISOString()}"^^xsd:dateTime)
OPTIONAL {
?fact zep:validUntil ?validUntil .
FILTER(?validUntil > "${validAt.toISOString()}"^^xsd:dateTime)
}
` : '';
const query = `
SELECT ?fact ?subject ?predicate ?object ?confidence ?validFrom ?validUntil ?sourceMemory
WHERE {
?fact a zep:SemanticMemory ;
zep:hasStatement ?statement ;
zep:confidence ?confidence ;
zep:validFrom ?validFrom .
?statement rdf:subject ?subject ;
rdf:predicate ?predicate ;
rdf:object ?object .
OPTIONAL { ?fact zep:validUntil ?validUntil }
OPTIONAL { ?fact zep:derivedFrom ?sourceMemory }
FILTER(CONTAINS(LCASE(STR(?subject)), LCASE("${entityName}")) ||
CONTAINS(LCASE(STR(?object)), LCASE("${entityName}")))
${timeConstraint}
}
ORDER BY DESC(?confidence) DESC(?validFrom)
`;
const result = await this.query(query);
return this.formatFactResults(result.bindings);
}
/**
* Find related entities using graph traversal
*/
async findRelatedEntities(entityName: string, maxHops = 2, minConfidence = 0.5): Promise<any[]> {
const query = `
SELECT DISTINCT ?relatedEntity ?relationPath ?totalConfidence
WHERE {
{
# Direct relations (1 hop)
?fact1 a zep:SemanticMemory ;
zep:hasStatement ?stmt1 ;
zep:confidence ?conf1 .
?stmt1 rdf:subject ?entity1 ;
rdf:predicate ?pred1 ;
rdf:object ?relatedEntity .
FILTER(CONTAINS(LCASE(STR(?entity1)), LCASE("${entityName}")))
FILTER(?conf1 >= ${minConfidence})
BIND(?pred1 AS ?relationPath)
BIND(?conf1 AS ?totalConfidence)
}
${maxHops >= 2 ? `
UNION {
# Indirect relations (2 hops)
?fact1 a zep:SemanticMemory ;
zep:hasStatement ?stmt1 ;
zep:confidence ?conf1 .
?fact2 a zep:SemanticMemory ;
zep:hasStatement ?stmt2 ;
zep:confidence ?conf2 .
?stmt1 rdf:subject ?entity1 ;
rdf:predicate ?pred1 ;
rdf:object ?intermediate .
?stmt2 rdf:subject ?intermediate ;
rdf:predicate ?pred2 ;
rdf:object ?relatedEntity .
FILTER(CONTAINS(LCASE(STR(?entity1)), LCASE("${entityName}")))
FILTER(?conf1 >= ${minConfidence} && ?conf2 >= ${minConfidence})
FILTER(?entity1 != ?relatedEntity)
BIND(CONCAT(STR(?pred1), " -> ", STR(?pred2)) AS ?relationPath)
BIND((?conf1 * ?conf2) AS ?totalConfidence)
}
` : ''}
}
ORDER BY DESC(?totalConfidence)
`;
const result = await this.query(query);
return result.bindings;
}
/**
* Aggregate memories by session with statistics
*/
async getSessionSummary(sessionId: string): Promise<any> {
const query = `
SELECT ?sessionId
(COUNT(?memory) AS ?totalMemories)
(COUNT(?episodic) AS ?episodicCount)
(COUNT(?semantic) AS ?semanticCount)
(COUNT(?procedural) AS ?proceduralCount)
(AVG(?confidence) AS ?avgConfidence)
(MIN(?createdAt) AS ?sessionStart)
(MAX(?createdAt) AS ?sessionEnd)
WHERE {
?memory zep:sessionId "${sessionId}" ;
zep:confidence ?confidence ;
zep:createdAt ?createdAt .
OPTIONAL {
?memory a zep:EpisodicMemory .
BIND(?memory AS ?episodic)
}
OPTIONAL {
?memory a zep:SemanticMemory .
BIND(?memory AS ?semantic)
}
OPTIONAL {
?memory a zep:ProceduralMemory .
BIND(?memory AS ?procedural)
}
}
GROUP BY ?sessionId
`;
const result = await this.query(query);
return result.bindings[0] || null;
}
/**
* Complex analytical query: Memory evolution over time
*/
async getMemoryEvolution(timeWindow: 'day' | 'week' | 'month' = 'day', limit = 30): Promise<any[]> {
const timeFormat = {
day: '%Y-%m-%d',
week: '%Y-W%U',
month: '%Y-%m'
};
const query = `
SELECT ?timePeriod
(COUNT(?memory) AS ?memoryCount)
(COUNT(?episodic) AS ?episodicCount)
(COUNT(?semantic) AS ?semanticCount)
(AVG(?confidence) AS ?avgConfidence)
WHERE {
?memory a ?type ;
zep:confidence ?confidence ;
zep:createdAt ?createdAt .
FILTER(?type IN (zep:EpisodicMemory, zep:SemanticMemory, zep:ProceduralMemory))
# Group by time period
BIND(STRFTIME("${timeFormat[timeWindow]}", ?createdAt) AS ?timePeriod)
OPTIONAL {
?memory a zep:EpisodicMemory .
BIND(?memory AS ?episodic)
}
OPTIONAL {
?memory a zep:SemanticMemory .
BIND(?memory AS ?semantic)
}
}
GROUP BY ?timePeriod
ORDER BY DESC(?timePeriod)
LIMIT ${limit}
`;
const result = await this.query(query);
return result.bindings;
}
/**
* Advanced reasoning query: Infer contradictions
*/
async findContradictions(confidenceThreshold = 0.7): Promise<any[]> {
const query = `
SELECT ?entity ?predicate ?value1 ?value2 ?confidence1 ?confidence2 ?time1 ?time2
WHERE {
?fact1 a zep:SemanticMemory ;
zep:hasStatement ?stmt1 ;
zep:confidence ?confidence1 ;
zep:validFrom ?time1 .
?fact2 a zep:SemanticMemory ;
zep:hasStatement ?stmt2 ;
zep:confidence ?confidence2 ;
zep:validFrom ?time2 .
?stmt1 rdf:subject ?entity ;
rdf:predicate ?predicate ;
rdf:object ?value1 .
?stmt2 rdf:subject ?entity ;
rdf:predicate ?predicate ;
rdf:object ?value2 .
FILTER(?fact1 != ?fact2)
FILTER(?value1 != ?value2)
FILTER(?confidence1 >= ${confidenceThreshold})
FILTER(?confidence2 >= ${confidenceThreshold})
# Only consider overlapping time periods
FILTER(?time1 <= ?time2)
OPTIONAL {
?fact1 zep:validUntil ?until1 .
FILTER(?until1 > ?time2)
}
}
ORDER BY ?entity ?predicate
`;
const result = await this.query(query);
return result.bindings;
}
private addZepPrefixes(query: string): string {
if (query.includes('PREFIX')) {
return query;
}
const prefixes = this.nsManager.getSparqlPrefixes([
'zep', 'zepmem', 'zeptime', 'zepent',
'rdf', 'rdfs', 'owl', 'xsd', 'prov'
]);
return `${prefixes}\n\n${query}`;
}
private applyQueryOptions(query: string, options: SPARQLQueryOptions): string {
let enhancedQuery = query;
// Add temporal filters
if (options.temporalFilter) {
enhancedQuery = this.addTemporalFilter(enhancedQuery, options.temporalFilter);
}
// Add confidence threshold
if (options.confidenceThreshold) {
const confidenceFilter = `FILTER(?confidence >= ${options.confidenceThreshold})`;
enhancedQuery = enhancedQuery.replace(/WHERE\s*\{/, `WHERE {\n ${confidenceFilter}`);
}
// Add ordering
if (options.orderBy && !enhancedQuery.includes('ORDER BY')) {
const direction = options.descending ? 'DESC' : 'ASC';
enhancedQuery += `\nORDER BY ${direction}(${options.orderBy})`;
}
// Add limit and offset
if (options.limit && !enhancedQuery.includes('LIMIT')) {
enhancedQuery += `\nLIMIT ${options.limit}`;
}
if (options.offset && !enhancedQuery.includes('OFFSET')) {
enhancedQuery += `\nOFFSET ${options.offset}`;
}
return enhancedQuery;
}
private addTemporalFilter(query: string, filter: TemporalFilter): string {
let temporalClause = '';
switch (filter.type) {
case 'at':
if (filter.timestamp) {
temporalClause = `
FILTER(?validFrom <= "${filter.timestamp.toISOString()}"^^xsd:dateTime)
OPTIONAL {
?memory zep:validUntil ?validUntil .
FILTER(?validUntil > "${filter.timestamp.toISOString()}"^^xsd:dateTime)
}`;
}
break;
case 'between':
if (filter.startTime && filter.endTime) {
temporalClause = `
FILTER(?validFrom >= "${filter.startTime.toISOString()}"^^xsd:dateTime)
FILTER(?validFrom <= "${filter.endTime.toISOString()}"^^xsd:dateTime)`;
}
break;
case 'before':
if (filter.timestamp) {
temporalClause = `FILTER(?validFrom < "${filter.timestamp.toISOString()}"^^xsd:dateTime)`;
}
break;
case 'after':
if (filter.timestamp) {
temporalClause = `FILTER(?validFrom > "${filter.timestamp.toISOString()}"^^xsd:dateTime)`;
}
break;
}
return query.replace(/WHERE\s*\{/, `WHERE {\n ${temporalClause}`);
}
private buildMemorySearchQuery(params: ZepSearchParams): string {
const typeFilter = params.memoryTypes?.length ?
`FILTER(?type IN (${params.memoryTypes.map(t => `zep:${t.charAt(0).toUpperCase() + t.slice(1)}Memory`).join(', ')}))` :
'';
const sessionFilter = params.sessionId ? `FILTER(?sessionId = "${params.sessionId}")` : '';
const userFilter = params.userId ? `FILTER(?userId = "${params.userId}")` : '';
const relevanceFilter = params.minRelevance ? `FILTER(?confidence >= ${params.minRelevance})` : '';
// Text search using SPARQL's text functions
const textFilter = `FILTER(CONTAINS(LCASE(?content), LCASE("${params.query}")))`;
return `
SELECT ?memory ?type ?uuid ?content ?confidence ?sessionId ?userId ?createdAt
WHERE {
?memory a ?type ;
zep:uuid ?uuid ;
zep:content ?content ;
zep:confidence ?confidence ;
zep:sessionId ?sessionId ;
zep:createdAt ?createdAt .
OPTIONAL { ?memory zep:userId ?userId }
${typeFilter}
${sessionFilter}
${userFilter}
${relevanceFilter}
${textFilter}
}
ORDER BY DESC(?confidence) DESC(?createdAt)
`;
}
private formatSearchResults(bindings: any[]): ZepSearchResult[] {
return bindings.map(binding => ({
memory: this.bindingToMemory(binding),
score: binding.confidence || 0,
distance: 1 - (binding.confidence || 0), // Convert confidence to distance
highlights: [binding.content], // Simple highlighting
context: [] // TODO: implement context retrieval
}));
}
private formatMemoryResults(bindings: any[]): ZepMemory[] {
return bindings.map(binding => this.bindingToMemory(binding));
}
private formatFactResults(bindings: any[]): ZepFact[] {
return bindings.map(binding => ({
uuid: binding.fact?.replace(/.*\//, '') || '',
subject: binding.subject || '',
predicate: binding.predicate || '',
object: binding.object || '',
confidence: binding.confidence || 0,
sourceMemoryIds: binding.sourceMemory ? [binding.sourceMemory.replace(/.*\//, '')] : [],
validFrom: new Date(binding.validFrom || Date.now()),
validUntil: binding.validUntil ? new Date(binding.validUntil) : undefined,
metadata: {}
}));
}
private bindingToMemory(binding: any): ZepMemory {
return {
uuid: binding.uuid || '',
sessionId: binding.sessionId || '',
userId: binding.userId,
content: binding.content || '',
memoryType: this.parseMemoryType(binding.type),
embedding: undefined, // TODO: retrieve embeddings
metadata: {},
createdAt: new Date(binding.createdAt || Date.now()),
lastAccessedAt: undefined,
accessCount: 0,
relevanceScore: binding.confidence,
summary: binding.summary,
validFrom: new Date(binding.validFrom || binding.createdAt || Date.now()),
validUntil: binding.validUntil ? new Date(binding.validUntil) : undefined,
facts: []
};
}
private parseMemoryType(typeUri?: string): MemoryType {
if (!typeUri) return MemoryType.EPISODIC; // default
if (typeUri.includes('EpisodicMemory')) return MemoryType.EPISODIC;
if (typeUri.includes('SemanticMemory')) return MemoryType.SEMANTIC;
if (typeUri.includes('ProceduralMemory')) return MemoryType.PROCEDURAL;
return MemoryType.EPISODIC; // default
}
}