feat: fix verb storage pipeline and implement relationship-aware neural clustering

- Add atomic verb saving with rollback on metadata creation failures
- Implement missing getVerbsForNoun() method required by neural APIs
- Fix broken FileSystemStorage filter methods that returned empty arrays
- Add scalable clustersWithRelationships() method with batching for millions of nodes
- Enhance error handling and logging throughout verb storage pipeline
- Add comprehensive relationship analysis with intra/inter-cluster edges
- Improve API consistency between noun and verb methods

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-09-02 16:37:40 -07:00
parent 455689f8fd
commit 3f0c587cb1
5 changed files with 339 additions and 17 deletions

View file

@ -26,6 +26,8 @@ import {
TemporalCluster,
ExplainableCluster,
ConfidentCluster,
EnhancedSemanticCluster,
ClusterEdge,
SimilarityOptions,
SimilarityResult,
NeighborOptions,
@ -484,6 +486,190 @@ export class ImprovedNeuralAPI {
}
}
/**
* Enhanced clustering with relationship analysis using verbs
* Returns clusters with intra-cluster and inter-cluster relationship information
*
* Scalable for millions of nodes - uses efficient batching and filtering
*/
async clustersWithRelationships(
input?: string | string[] | ClusteringOptions,
options?: { batchSize?: number; maxRelationships?: number }
): Promise<EnhancedSemanticCluster[]> {
const startTime = performance.now()
const batchSize = options?.batchSize || 1000
const maxRelationships = options?.maxRelationships || 10000
let processedCount = 0
try {
// Get basic clusters first
const basicClusters = await this.clusters(input)
if (basicClusters.length === 0) {
return []
}
// Build member lookup for O(1) cluster membership checking
const memberToClusterMap = new Map<string, string>()
const clusterMap = new Map<string, SemanticCluster>()
for (const cluster of basicClusters) {
clusterMap.set(cluster.id, cluster)
for (const memberId of cluster.members) {
memberToClusterMap.set(memberId, cluster.id)
}
}
// Initialize cluster edge collections
const clusterEdges = new Map<string, {
intra: ClusterEdge[]
inter: ClusterEdge[]
edgeTypes: Record<string, number>
}>()
for (const cluster of basicClusters) {
clusterEdges.set(cluster.id, {
intra: [],
inter: [],
edgeTypes: {}
})
}
// Process verbs in batches to handle millions of relationships efficiently
let hasMoreVerbs = true
let offset = 0
while (hasMoreVerbs && processedCount < maxRelationships) {
// Get batch of verbs using proper pagination API
const verbResult = await this.brain.getVerbs({
pagination: {
offset: offset,
limit: batchSize
}
})
const verbBatch = verbResult.data
if (verbBatch.length === 0) {
hasMoreVerbs = false
break
}
// Process this batch
for (const verb of verbBatch) {
if (processedCount >= maxRelationships) break
const sourceClusterId = memberToClusterMap.get(verb.sourceId)
const targetClusterId = memberToClusterMap.get(verb.targetId)
// Skip verbs that don't involve any clustered nodes
if (!sourceClusterId && !targetClusterId) continue
const edgeWeight = this._calculateEdgeWeight(verb)
const edgeType = verb.verb || verb.type || 'relationship'
if (sourceClusterId && targetClusterId) {
if (sourceClusterId === targetClusterId) {
// Intra-cluster relationship
const edges = clusterEdges.get(sourceClusterId)!
edges.intra.push({
id: verb.id,
source: verb.sourceId,
target: verb.targetId,
type: edgeType,
weight: edgeWeight,
isInterCluster: false,
sourceCluster: sourceClusterId,
targetCluster: sourceClusterId
})
edges.edgeTypes[edgeType] = (edges.edgeTypes[edgeType] || 0) + 1
} else {
// Inter-cluster relationship
const sourceEdges = clusterEdges.get(sourceClusterId)!
const targetEdges = clusterEdges.get(targetClusterId)!
const edge: ClusterEdge = {
id: verb.id,
source: verb.sourceId,
target: verb.targetId,
type: edgeType,
weight: edgeWeight,
isInterCluster: true,
sourceCluster: sourceClusterId,
targetCluster: targetClusterId
}
sourceEdges.inter.push(edge)
// Don't duplicate - target cluster will see this as incoming
sourceEdges.edgeTypes[edgeType] = (sourceEdges.edgeTypes[edgeType] || 0) + 1
}
} else {
// One-way relationship to/from cluster
const clusterId = sourceClusterId || targetClusterId!
const edges = clusterEdges.get(clusterId)!
edges.inter.push({
id: verb.id,
source: verb.sourceId,
target: verb.targetId,
type: edgeType,
weight: edgeWeight,
isInterCluster: true,
sourceCluster: sourceClusterId || 'external',
targetCluster: targetClusterId || 'external'
})
edges.edgeTypes[edgeType] = (edges.edgeTypes[edgeType] || 0) + 1
}
processedCount++
}
offset += batchSize
// Memory management: if we have too many edges, break early
const totalEdges = Array.from(clusterEdges.values())
.reduce((sum, edges) => sum + edges.intra.length + edges.inter.length, 0)
if (totalEdges >= maxRelationships) {
console.warn(`Relationship analysis stopped at ${totalEdges} edges to maintain performance`)
break
}
// Check if we got fewer verbs than batch size (end of data)
if (verbBatch.length < batchSize) {
hasMoreVerbs = false
}
}
// Build enhanced clusters
const enhancedClusters: EnhancedSemanticCluster[] = []
for (const cluster of basicClusters) {
const edges = clusterEdges.get(cluster.id)!
enhancedClusters.push({
...cluster,
intraClusterEdges: edges.intra,
interClusterEdges: edges.inter,
relationshipSummary: {
totalEdges: edges.intra.length + edges.inter.length,
intraClusterEdges: edges.intra.length,
interClusterEdges: edges.inter.length,
edgeTypes: edges.edgeTypes
}
})
}
this._trackPerformance('clustersWithRelationships', startTime, processedCount, 'enhanced-scalable')
return enhancedClusters
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new ClusteringError(`Failed to perform relationship-aware clustering: ${errorMessage}`, {
input: typeof input === 'object' ? JSON.stringify(input) : input,
processedCount: processedCount || 0
})
}
}
// ===== PUBLIC API: NEIGHBORS & HIERARCHY =====
/**
@ -2718,6 +2904,34 @@ The items were grouped using ${algorithm} clustering. What is the most appropria
return await this.similar(id1, id2) as number
}
private _calculateEdgeWeight(verb: any): number {
// Calculate edge weight based on verb properties
let weight = 1.0
// Factor in connection strength if available
if (verb.connections && verb.connections instanceof Map) {
const connectionCount = verb.connections.size
weight += Math.log(connectionCount + 1) * 0.1
}
// Factor in verb type significance
const significantVerbs = ['caused', 'created', 'contains', 'implements', 'extends']
if (verb.verb && significantVerbs.includes(verb.verb.toLowerCase())) {
weight += 0.3
}
// Factor in recency if available
if (verb.metadata?.createdAt) {
const now = Date.now()
const created = new Date(verb.metadata.createdAt).getTime()
const daysSinceCreated = (now - created) / (1000 * 60 * 60 * 24)
// Newer relationships get slight boost
weight += Math.max(0, (30 - daysSinceCreated) / 100)
}
return Math.min(weight, 3.0) // Cap at 3.0
}
private _sortNeighbors(neighbors: Neighbor[], sortBy: 'similarity' | 'importance' | 'recency'): void {
switch (sortBy) {
case 'similarity':