UNPKG

graphzep

Version:

GraphZep: A temporal knowledge graph memory system for AI agents based on the Zep paper

591 lines (494 loc) 17.5 kB
/** * Ontology Manager for GraphZep RDF support * Handles loading, validation, and management of domain ontologies */ import fs from 'fs/promises'; import path from 'path'; import { NamespaceManager } from './namespaces.js'; import { RDFTriple } from '../drivers/rdf-driver.js'; import { LLMClient } from '../types/index.js'; export interface OntologyClass { uri: string; label?: string; comment?: string; superClasses: string[]; subClasses: string[]; properties: string[]; restrictions: OntologyRestriction[]; } export interface OntologyProperty { uri: string; label?: string; comment?: string; domain: string[]; range: string[]; subProperties: string[]; superProperties: string[]; functional?: boolean; inverseFunctional?: boolean; transitive?: boolean; symmetric?: boolean; } export interface OntologyRestriction { type: 'cardinality' | 'minCardinality' | 'maxCardinality' | 'allValuesFrom' | 'someValuesFrom' | 'hasValue'; property: string; value?: string | number; classRestriction?: string; } export interface ParsedOntology { uri: string; version?: string; classes: Map<string, OntologyClass>; properties: Map<string, OntologyProperty>; individuals: Map<string, any>; rules: OntologyRule[]; imports: string[]; namespaces: Map<string, string>; } export interface OntologyRule { id: string; description: string; type: 'inference' | 'validation' | 'constraint'; sparql?: string; condition?: string; action?: string; } export interface ValidationResult { valid: boolean; errors: ValidationError[]; warnings: ValidationWarning[]; } export interface ValidationError { type: string; message: string; subject?: string; predicate?: string; object?: string; severity: 'error' | 'warning' | 'info'; } export interface ValidationWarning extends ValidationError { severity: 'warning'; } export interface OntologyStats { totalClasses: number; totalProperties: number; totalIndividuals: number; totalRules: number; depth: number; complexity: number; } export interface ExtractionGuidance { entityTypes: string[]; relationTypes: string[]; constraints: string[]; examples: string[]; prompt: string; } export class OntologyManager { private ontologies: Map<string, ParsedOntology> = new Map(); private activeOntology: string | null = null; private nsManager: NamespaceManager; private validationCache: Map<string, ValidationResult> = new Map(); constructor(nsManager?: NamespaceManager) { this.nsManager = nsManager || new NamespaceManager(); } /** * Load ontology from file with format detection */ async loadOntology(filePath: string, ontologyId?: string): Promise<string> { try { const content = await fs.readFile(filePath, 'utf-8'); const format = this.detectFormat(filePath, content); const id = ontologyId || path.basename(filePath, path.extname(filePath)); const ontology = await this.parseOntology(content, format, id); this.ontologies.set(id, ontology); // Update namespace manager with ontology namespaces for (const [prefix, uri] of ontology.namespaces.entries()) { this.nsManager.addNamespace(prefix, uri); } // Set as active if it's the first one if (!this.activeOntology) { this.activeOntology = id; } return id; } catch (error) { throw new Error(`Failed to load ontology from ${filePath}: ${error}`); } } /** * Load ontology from string content */ async loadOntologyFromString(content: string, format: string, ontologyId: string): Promise<string> { try { const ontology = await this.parseOntology(content, format, ontologyId); this.ontologies.set(ontologyId, ontology); // Update namespace manager for (const [prefix, uri] of ontology.namespaces.entries()) { this.nsManager.addNamespace(prefix, uri); } if (!this.activeOntology) { this.activeOntology = ontologyId; } return ontologyId; } catch (error) { throw new Error(`Failed to parse ontology: ${error}`); } } /** * Set active ontology for validation and extraction */ setActiveOntology(ontologyId: string): void { if (!this.ontologies.has(ontologyId)) { throw new Error(`Ontology not found: ${ontologyId}`); } this.activeOntology = ontologyId; } /** * Get active ontology */ getActiveOntology(): ParsedOntology | null { return this.activeOntology ? this.ontologies.get(this.activeOntology) || null : null; } /** * Validate RDF triple against active ontology */ validateTriple(triple: RDFTriple): ValidationResult { const cacheKey = `${triple.subject}:${triple.predicate}:${JSON.stringify(triple.object)}`; // Check cache first if (this.validationCache.has(cacheKey)) { return this.validationCache.get(cacheKey)!; } const result = this.performValidation(triple); // Cache result this.validationCache.set(cacheKey, result); return result; } /** * Validate multiple triples */ validateTriples(triples: RDFTriple[]): ValidationResult { const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; for (const triple of triples) { const result = this.validateTriple(triple); errors.push(...result.errors); warnings.push(...result.warnings); } return { valid: errors.length === 0, errors, warnings }; } /** * Generate extraction guidance for LLM */ generateExtractionGuidance(content: string, ontologyId?: string): ExtractionGuidance { const ontology = ontologyId ? this.ontologies.get(ontologyId) : this.getActiveOntology(); if (!ontology) { throw new Error('No ontology available for guidance'); } const entityTypes = Array.from(ontology.classes.values()) .filter(cls => !cls.uri.startsWith('http://www.w3.org/')) .map(cls => cls.label || cls.uri.split('#').pop() || cls.uri.split('/').pop() || cls.uri) .slice(0, 20); // Limit to avoid token overflow const relationTypes = Array.from(ontology.properties.values()) .filter(prop => prop.domain.length > 0 && prop.range.length > 0) .map(prop => prop.label || prop.uri.split('#').pop() || prop.uri.split('/').pop() || prop.uri) .slice(0, 15); const constraints = this.generateConstraintDescriptions(ontology); const examples = this.generateExamples(ontology); const prompt = this.buildExtractionPrompt(content, entityTypes, relationTypes, constraints, examples); return { entityTypes, relationTypes, constraints, examples, prompt }; } /** * Get ontology statistics */ getOntologyStats(ontologyId?: string): OntologyStats { const ontology = ontologyId ? this.ontologies.get(ontologyId) : this.getActiveOntology(); if (!ontology) { throw new Error('No ontology available'); } const depth = this.calculateOntologyDepth(ontology); const complexity = this.calculateComplexity(ontology); return { totalClasses: ontology.classes.size, totalProperties: ontology.properties.size, totalIndividuals: ontology.individuals.size, totalRules: ontology.rules.length, depth, complexity }; } /** * Get class hierarchy */ getClassHierarchy(rootClass?: string, ontologyId?: string): any { const ontology = ontologyId ? this.ontologies.get(ontologyId) : this.getActiveOntology(); if (!ontology) { throw new Error('No ontology available'); } const root = rootClass || 'http://www.w3.org/2002/07/owl#Thing'; return this.buildClassHierarchy(ontology, root); } /** * Search classes and properties */ search(query: string, type: 'class' | 'property' | 'both' = 'both'): any[] { const results: any[] = []; const queryLower = query.toLowerCase(); for (const ontology of this.ontologies.values()) { if (type === 'class' || type === 'both') { for (const cls of ontology.classes.values()) { if (this.matchesQuery(cls, queryLower)) { results.push({ type: 'class', ontology: ontology.uri, ...cls }); } } } if (type === 'property' || type === 'both') { for (const prop of ontology.properties.values()) { if (this.matchesQuery(prop, queryLower)) { results.push({ type: 'property', ontology: ontology.uri, ...prop }); } } } } return results; } /** * Export ontology to different formats */ async exportOntology(ontologyId: string, format: 'rdf-xml' | 'turtle' | 'json-ld'): Promise<string> { const ontology = this.ontologies.get(ontologyId); if (!ontology) { throw new Error(`Ontology not found: ${ontologyId}`); } // Convert back to RDF format // This is a simplified implementation - real-world would use proper RDF libraries const triples = this.ontologyToTriples(ontology); switch (format) { case 'turtle': return this.triplesToTurtle(triples); case 'rdf-xml': return this.triplesToRDFXML(triples); case 'json-ld': return this.triplesToJSONLD(triples); default: throw new Error(`Unsupported format: ${format}`); } } private async parseOntology(content: string, format: string, id: string): Promise<ParsedOntology> { // Simplified ontology parsing for demo console.log(`Parsing ${format} ontology: ${id}`); return { uri: `http://example.com/ontologies/${id}`, classes: new Map([ ['http://graphzep.ai/ontology#Person', { uri: 'http://graphzep.ai/ontology#Person', label: 'Person', comment: 'A human being', superClasses: [], subClasses: [], properties: [], restrictions: [] }] ]), properties: new Map([ ['http://graphzep.ai/ontology#knows', { uri: 'http://graphzep.ai/ontology#knows', label: 'knows', comment: 'Person knows another person', domain: ['http://graphzep.ai/ontology#Person'], range: ['http://graphzep.ai/ontology#Person'], subProperties: [], superProperties: [] }] ]), individuals: new Map(), rules: [], imports: [], namespaces: new Map([ ['zep', 'http://graphzep.ai/ontology#'], ['rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'] ]) }; } // Simplified methods - these would be implemented with proper RDF parsing in production private detectFormat(filePath: string, content: string): string { const extension = path.extname(filePath).toLowerCase(); // Check by file extension first switch (extension) { case '.rdf': case '.owl': return 'application/rdf+xml'; case '.ttl': return 'text/turtle'; case '.n3': return 'text/n3'; case '.nt': return 'application/n-triples'; case '.jsonld': return 'application/ld+json'; } // Check by content if (content.includes('<?xml') && content.includes('rdf:RDF')) { return 'application/rdf+xml'; } else if (content.includes('@prefix') || content.includes('PREFIX')) { return 'text/turtle'; } else if (content.includes('{') && content.includes('@context')) { return 'application/ld+json'; } // Default to RDF/XML return 'application/rdf+xml'; } private performValidation(triple: RDFTriple): ValidationResult { const ontology = this.getActiveOntology(); if (!ontology) { return { valid: true, errors: [], warnings: [] }; } const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; // Validate property exists const property = ontology.properties.get(triple.predicate); if (!property && !this.isBuiltinProperty(triple.predicate)) { warnings.push({ type: 'unknown_property', message: `Unknown property: ${triple.predicate}`, predicate: triple.predicate, severity: 'warning' }); } // Validate domain and range constraints if (property) { // Domain validation would require knowing the type of the subject // Range validation would require knowing the type of the object // This is simplified for the example } return { valid: errors.length === 0, errors, warnings }; } private isBuiltinProperty(predicate: string): boolean { return predicate.startsWith('http://www.w3.org/1999/02/22-rdf-syntax-ns#') || predicate.startsWith('http://www.w3.org/2000/01/rdf-schema#') || predicate.startsWith('http://www.w3.org/2002/07/owl#'); } private generateConstraintDescriptions(ontology: ParsedOntology): string[] { const constraints: string[] = []; for (const cls of ontology.classes.values()) { if (cls.restrictions.length > 0) { for (const restriction of cls.restrictions) { constraints.push(`${cls.label || cls.uri} must have ${restriction.type} ${restriction.value} for property ${restriction.property}`); } } } return constraints.slice(0, 10); // Limit for prompt size } private generateExamples(ontology: ParsedOntology): string[] { // Generate simple examples based on ontology structure const examples: string[] = []; for (const prop of ontology.properties.values()) { if (prop.domain.length > 0 && prop.range.length > 0) { const domainLabel = this.getClassLabel(ontology, prop.domain[0]); const rangeLabel = this.getClassLabel(ontology, prop.range[0]); const propLabel = prop.label || 'related to'; examples.push(`${domainLabel} ${propLabel} ${rangeLabel}`); } } return examples.slice(0, 5); } private getClassLabel(ontology: ParsedOntology, classUri: string): string { const cls = ontology.classes.get(classUri); return cls?.label || classUri.split('#').pop() || classUri.split('/').pop() || classUri; } private buildExtractionPrompt(content: string, entityTypes: string[], relationTypes: string[], constraints: string[], examples: string[]): string { return ` Extract entities and relationships from the following text using the provided ontology. Available entity types: ${entityTypes.join(', ')} Available relationship types: ${relationTypes.join(', ')} Constraints: ${constraints.map(c => `- ${c}`).join('\n')} Examples: ${examples.map(e => `- ${e}`).join('\n')} Text to analyze: ${content} Please extract entities and their relationships in the format: - Entity: [name] (type: [entity_type]) - Relationship: [entity1] [relationship_type] [entity2] `.trim(); } private calculateOntologyDepth(ontology: ParsedOntology): number { let maxDepth = 0; for (const cls of ontology.classes.values()) { const depth = this.calculateClassDepth(ontology, cls.uri, new Set()); maxDepth = Math.max(maxDepth, depth); } return maxDepth; } private calculateClassDepth(ontology: ParsedOntology, classUri: string, visited: Set<string>): number { if (visited.has(classUri)) return 0; visited.add(classUri); const cls = ontology.classes.get(classUri); if (!cls || cls.superClasses.length === 0) return 1; let maxParentDepth = 0; for (const parentUri of cls.superClasses) { const parentDepth = this.calculateClassDepth(ontology, parentUri, visited); maxParentDepth = Math.max(maxParentDepth, parentDepth); } return maxParentDepth + 1; } private calculateComplexity(ontology: ParsedOntology): number { // Simple complexity metric based on structure return ontology.classes.size + ontology.properties.size * 2 + ontology.rules.length * 3; } private buildClassHierarchy(ontology: ParsedOntology, rootUri: string): any { const cls = ontology.classes.get(rootUri); if (!cls) return null; const children: any[] = []; for (const [uri, otherClass] of ontology.classes.entries()) { if (otherClass.superClasses.includes(rootUri)) { children.push(this.buildClassHierarchy(ontology, uri)); } } return { uri: cls.uri, label: cls.label, comment: cls.comment, children }; } private matchesQuery(item: OntologyClass | OntologyProperty, query: string): boolean { return (item.label?.toLowerCase().includes(query)) || (item.comment?.toLowerCase().includes(query)) || (item.uri.toLowerCase().includes(query)); } private ontologyToTriples(ontology: ParsedOntology): RDFTriple[] { // Simplified conversion - would need proper implementation return []; } private triplesToTurtle(triples: RDFTriple[]): string { // Simplified conversion return ''; } private triplesToRDFXML(triples: RDFTriple[]): string { // Simplified conversion return ''; } private triplesToJSONLD(triples: RDFTriple[]): string { // Simplified conversion return ''; } }