graphzep
Version:
GraphZep: A temporal knowledge graph memory system for AI agents based on the Zep paper
428 lines (423 loc) • 15.9 kB
JavaScript
/**
* 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';
export class OntologyManager {
ontologies = new Map();
activeOntology = null;
nsManager;
validationCache = new Map();
constructor(nsManager) {
this.nsManager = nsManager || new NamespaceManager();
}
/**
* Load ontology from file with format detection
*/
async loadOntology(filePath, ontologyId) {
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, format, ontologyId) {
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) {
if (!this.ontologies.has(ontologyId)) {
throw new Error(`Ontology not found: ${ontologyId}`);
}
this.activeOntology = ontologyId;
}
/**
* Get active ontology
*/
getActiveOntology() {
return this.activeOntology ? this.ontologies.get(this.activeOntology) || null : null;
}
/**
* Validate RDF triple against active ontology
*/
validateTriple(triple) {
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) {
const errors = [];
const warnings = [];
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, ontologyId) {
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) {
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, ontologyId) {
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, type = 'both') {
const results = [];
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, format) {
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}`);
}
}
async parseOntology(content, format, id) {
// 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
detectFormat(filePath, content) {
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';
}
performValidation(triple) {
const ontology = this.getActiveOntology();
if (!ontology) {
return { valid: true, errors: [], warnings: [] };
}
const errors = [];
const warnings = [];
// 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
};
}
isBuiltinProperty(predicate) {
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#');
}
generateConstraintDescriptions(ontology) {
const constraints = [];
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
}
generateExamples(ontology) {
// Generate simple examples based on ontology structure
const examples = [];
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);
}
getClassLabel(ontology, classUri) {
const cls = ontology.classes.get(classUri);
return cls?.label || classUri.split('#').pop() || classUri.split('/').pop() || classUri;
}
buildExtractionPrompt(content, entityTypes, relationTypes, constraints, examples) {
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();
}
calculateOntologyDepth(ontology) {
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;
}
calculateClassDepth(ontology, classUri, visited) {
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;
}
calculateComplexity(ontology) {
// Simple complexity metric based on structure
return ontology.classes.size + ontology.properties.size * 2 + ontology.rules.length * 3;
}
buildClassHierarchy(ontology, rootUri) {
const cls = ontology.classes.get(rootUri);
if (!cls)
return null;
const children = [];
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
};
}
matchesQuery(item, query) {
return (item.label?.toLowerCase().includes(query)) ||
(item.comment?.toLowerCase().includes(query)) ||
(item.uri.toLowerCase().includes(query));
}
ontologyToTriples(ontology) {
// Simplified conversion - would need proper implementation
return [];
}
triplesToTurtle(triples) {
// Simplified conversion
return '';
}
triplesToRDFXML(triples) {
// Simplified conversion
return '';
}
triplesToJSONLD(triples) {
// Simplified conversion
return '';
}
}
//# sourceMappingURL=ontology-manager.js.map