rust-kgdb 0.6.40 → 0.6.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +32 -14
- package/examples/quadstore-capabilities-demo.js +407 -0
- package/hypermind-agent.js +432 -48
- package/index.d.ts +28 -0
- package/index.js +6 -0
- package/package.json +2 -2
- package/rust-kgdb-napi.darwin-x64.node +0 -0
- package/vanilla-vs-hypermind-benchmark.js +164 -12
|
@@ -24,10 +24,11 @@ const HARD_TEST_SUITE = [
|
|
|
24
24
|
{
|
|
25
25
|
id: 'A1',
|
|
26
26
|
category: 'ambiguous',
|
|
27
|
-
question: 'Find all teachers', // LUBM uses "teacherOf"
|
|
28
|
-
trap: 'Vanilla might use ub:teacher (wrong) instead of ub:teacherOf',
|
|
27
|
+
question: 'Find all teachers', // LUBM uses "teacherOf" or "Professor" class
|
|
28
|
+
trap: 'Vanilla might use ub:teacher (wrong) instead of ub:teacherOf or ub:Professor',
|
|
29
29
|
correctPattern: 'teacherOf',
|
|
30
|
-
|
|
30
|
+
alternateCorrect: 'Professor', // Professor class is also valid for "teachers"
|
|
31
|
+
wrongPatterns: ['teaches', 'instructor'] // removed 'teacher' - variable names OK
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
id: 'A2',
|
|
@@ -188,6 +189,9 @@ async function callVanillaLLM(model, question) {
|
|
|
188
189
|
})
|
|
189
190
|
})
|
|
190
191
|
const data = JSON.parse(response.data)
|
|
192
|
+
if (data.error) {
|
|
193
|
+
throw new Error(`OpenAI: ${data.error.message}`)
|
|
194
|
+
}
|
|
191
195
|
return data.choices[0].message.content.trim()
|
|
192
196
|
}
|
|
193
197
|
}
|
|
@@ -248,10 +252,36 @@ OUTPUT FORMAT:
|
|
|
248
252
|
})
|
|
249
253
|
})
|
|
250
254
|
const data = JSON.parse(response.data)
|
|
255
|
+
if (data.error) {
|
|
256
|
+
throw new Error(`OpenAI: ${data.error.message}`)
|
|
257
|
+
}
|
|
251
258
|
return data.choices[0].message.content.trim()
|
|
252
259
|
}
|
|
253
260
|
}
|
|
254
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Extract predicates from SPARQL query (not variables)
|
|
264
|
+
* Returns array of predicate local names from ub: prefix and full URIs
|
|
265
|
+
*/
|
|
266
|
+
function extractPredicates(query) {
|
|
267
|
+
const predicates = []
|
|
268
|
+
|
|
269
|
+
// Match ub:predicate patterns (not after ?)
|
|
270
|
+
const ubPattern = /(?<!\?)\bub:([a-zA-Z]+)/g
|
|
271
|
+
let match
|
|
272
|
+
while ((match = ubPattern.exec(query)) !== null) {
|
|
273
|
+
predicates.push(match[1])
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Match full URI predicates in angle brackets
|
|
277
|
+
const uriPattern = /<http:\/\/[^>]*#([a-zA-Z]+)>/g
|
|
278
|
+
while ((match = uriPattern.exec(query)) !== null) {
|
|
279
|
+
predicates.push(match[1])
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return predicates
|
|
283
|
+
}
|
|
284
|
+
|
|
255
285
|
/**
|
|
256
286
|
* Analyze query for issues
|
|
257
287
|
*/
|
|
@@ -269,10 +299,17 @@ function analyzeQuery(query, test) {
|
|
|
269
299
|
issues.push('Contains explanation text')
|
|
270
300
|
}
|
|
271
301
|
|
|
272
|
-
// Check for wrong patterns (
|
|
302
|
+
// Check for wrong predicates in actual triple patterns (not variables)
|
|
273
303
|
if (test.wrongPatterns) {
|
|
304
|
+
const predicates = extractPredicates(query)
|
|
274
305
|
for (const wrong of test.wrongPatterns) {
|
|
275
|
-
if
|
|
306
|
+
// Check if wrong predicate is used AND neither correct nor alternate is present
|
|
307
|
+
const usesWrong = predicates.some(p => p.toLowerCase() === wrong.toLowerCase())
|
|
308
|
+
const usesCorrect = predicates.some(p => p.toLowerCase() === test.correctPattern.toLowerCase())
|
|
309
|
+
const usesAlternate = test.alternateCorrect
|
|
310
|
+
? predicates.some(p => p.toLowerCase() === test.alternateCorrect.toLowerCase())
|
|
311
|
+
: false
|
|
312
|
+
if (usesWrong && !usesCorrect && !usesAlternate) {
|
|
276
313
|
issues.push(`Used wrong predicate: ${wrong} instead of ${test.correctPattern}`)
|
|
277
314
|
}
|
|
278
315
|
}
|
|
@@ -280,8 +317,10 @@ function analyzeQuery(query, test) {
|
|
|
280
317
|
|
|
281
318
|
// Check for required predicates (multi-hop tests)
|
|
282
319
|
if (test.requiredPredicates) {
|
|
320
|
+
const predicates = extractPredicates(query)
|
|
283
321
|
for (const pred of test.requiredPredicates) {
|
|
284
|
-
|
|
322
|
+
const hasIt = predicates.some(p => p.toLowerCase() === pred.toLowerCase())
|
|
323
|
+
if (!hasIt && !queryLower.includes(pred.toLowerCase())) {
|
|
285
324
|
issues.push(`Missing required predicate: ${pred}`)
|
|
286
325
|
}
|
|
287
326
|
}
|
|
@@ -307,10 +346,12 @@ function analyzeQuery(query, test) {
|
|
|
307
346
|
}
|
|
308
347
|
}
|
|
309
348
|
|
|
310
|
-
// Check mustNotContain
|
|
349
|
+
// Check mustNotContain (use word boundary to avoid false positives like WHERE matching Here)
|
|
311
350
|
if (test.mustNotContain) {
|
|
312
351
|
for (const mustNot of test.mustNotContain) {
|
|
313
|
-
|
|
352
|
+
// Use word boundary regex - match whole word only
|
|
353
|
+
const regex = new RegExp(`\\b${mustNot}\\b`, 'i')
|
|
354
|
+
if (regex.test(query)) {
|
|
314
355
|
issues.push(`Contains forbidden: ${mustNot}`)
|
|
315
356
|
}
|
|
316
357
|
}
|
|
@@ -319,6 +360,86 @@ function analyzeQuery(query, test) {
|
|
|
319
360
|
return issues
|
|
320
361
|
}
|
|
321
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Load native Rust functions for predicate correction
|
|
365
|
+
* Use direct native require to avoid circular dependency with index.js
|
|
366
|
+
*/
|
|
367
|
+
let computeSimilarity, tokenizeIdentifier, stemWord
|
|
368
|
+
try {
|
|
369
|
+
const os = require('os')
|
|
370
|
+
const platform = os.platform()
|
|
371
|
+
const arch = os.arch()
|
|
372
|
+
const nativePath = platform === 'darwin' && arch === 'arm64'
|
|
373
|
+
? './rust-kgdb-napi.darwin-arm64.node'
|
|
374
|
+
: platform === 'darwin'
|
|
375
|
+
? './rust-kgdb-napi.darwin-x64.node'
|
|
376
|
+
: './rust-kgdb-napi.linux-x64-gnu.node'
|
|
377
|
+
const native = require(nativePath)
|
|
378
|
+
computeSimilarity = native.computeSimilarity
|
|
379
|
+
tokenizeIdentifier = native.tokenizeIdentifier
|
|
380
|
+
stemWord = native.stemWord
|
|
381
|
+
} catch (e) {
|
|
382
|
+
// Test-only fallback - simple string matching
|
|
383
|
+
computeSimilarity = (a, b) => a === b ? 1.0 : 0.0
|
|
384
|
+
tokenizeIdentifier = (s) => [s]
|
|
385
|
+
stemWord = (s) => s
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// LUBM schema predicates (from schema context)
|
|
389
|
+
const LUBM_PREDICATES = [
|
|
390
|
+
'worksFor', 'memberOf', 'advisor', 'takesCourse', 'teacherOf',
|
|
391
|
+
'publicationAuthor', 'subOrganizationOf', 'researchInterest', 'name',
|
|
392
|
+
'emailAddress', 'telephone', 'degreeFrom', 'headOf'
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Correct predicates using native Rust similarity (simple, no bloat)
|
|
397
|
+
*/
|
|
398
|
+
function correctPredicates(query) {
|
|
399
|
+
let corrected = query
|
|
400
|
+
|
|
401
|
+
// Find ub: prefixed predicates
|
|
402
|
+
const predPattern = /ub:([a-zA-Z]+)/g
|
|
403
|
+
let match
|
|
404
|
+
while ((match = predPattern.exec(query)) !== null) {
|
|
405
|
+
const usedPred = match[1]
|
|
406
|
+
|
|
407
|
+
// Check if it's already a valid predicate
|
|
408
|
+
if (LUBM_PREDICATES.includes(usedPred)) continue
|
|
409
|
+
|
|
410
|
+
// Find best match using native Rust similarity
|
|
411
|
+
let bestMatch = null
|
|
412
|
+
let bestScore = 0.6 // minimum threshold
|
|
413
|
+
|
|
414
|
+
for (const schemaPred of LUBM_PREDICATES) {
|
|
415
|
+
// Direct similarity
|
|
416
|
+
const directScore = computeSimilarity(usedPred.toLowerCase(), schemaPred.toLowerCase())
|
|
417
|
+
|
|
418
|
+
// Token-based matching (e.g., "teacher" matches "teacherOf" via token)
|
|
419
|
+
const tokens = tokenizeIdentifier(schemaPred)
|
|
420
|
+
let tokenScore = 0
|
|
421
|
+
for (const token of tokens) {
|
|
422
|
+
const score = computeSimilarity(usedPred.toLowerCase(), token.toLowerCase())
|
|
423
|
+
tokenScore = Math.max(tokenScore, score)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const score = Math.max(directScore, tokenScore)
|
|
427
|
+
if (score > bestScore) {
|
|
428
|
+
bestScore = score
|
|
429
|
+
bestMatch = schemaPred
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Replace with best match if found
|
|
434
|
+
if (bestMatch && bestMatch !== usedPred) {
|
|
435
|
+
if (process.env.DEBUG) console.log(` [DEBUG] Correcting ${usedPred} -> ${bestMatch}`)
|
|
436
|
+
corrected = corrected.replace(new RegExp(`ub:${usedPred}\\b`, 'g'), `ub:${bestMatch}`)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return corrected
|
|
441
|
+
}
|
|
442
|
+
|
|
322
443
|
/**
|
|
323
444
|
* Clean SPARQL (HyperMind's cleaning)
|
|
324
445
|
*/
|
|
@@ -327,17 +448,31 @@ function cleanSparql(raw) {
|
|
|
327
448
|
.replace(/```sparql\n?/gi, '')
|
|
328
449
|
.replace(/```sql\n?/gi, '')
|
|
329
450
|
.replace(/```\n?/g, '')
|
|
330
|
-
.replace(/^Here.*?:\s*/i, '')
|
|
331
|
-
.replace(/^This query.*?:\s*/i, '')
|
|
332
451
|
.trim()
|
|
333
452
|
|
|
334
|
-
//
|
|
453
|
+
// Remove common LLM explanation patterns before extracting SPARQL
|
|
454
|
+
// These patterns appear BEFORE the query
|
|
455
|
+
clean = clean.replace(/^Here\s+(is|are)\s+[^:\n]*:?\s*/gi, '')
|
|
456
|
+
clean = clean.replace(/^This\s+query\s+[^:\n]*:?\s*/gi, '')
|
|
457
|
+
clean = clean.replace(/^The\s+following\s+[^:\n]*:?\s*/gi, '')
|
|
458
|
+
clean = clean.replace(/^Sure[^:\n]*:?\s*/gi, '')
|
|
459
|
+
clean = clean.trim()
|
|
460
|
+
|
|
461
|
+
// Extract just the SPARQL part - find PREFIX or SELECT start
|
|
335
462
|
const prefixMatch = clean.match(/PREFIX[\s\S]*/i)
|
|
336
463
|
if (prefixMatch) clean = prefixMatch[0]
|
|
337
464
|
|
|
338
465
|
const selectMatch = clean.match(/SELECT[\s\S]*/i)
|
|
339
466
|
if (!clean.includes('PREFIX') && selectMatch) clean = selectMatch[0]
|
|
340
467
|
|
|
468
|
+
// Remove trailing explanation after query
|
|
469
|
+
clean = clean.replace(/\n\nThis\s+(query|will|returns)[\s\S]*/i, '')
|
|
470
|
+
clean = clean.replace(/\n\nNote:[\s\S]*/i, '')
|
|
471
|
+
clean = clean.trim()
|
|
472
|
+
|
|
473
|
+
// Correct predicates using native Rust similarity
|
|
474
|
+
clean = correctPredicates(clean)
|
|
475
|
+
|
|
341
476
|
return clean
|
|
342
477
|
}
|
|
343
478
|
|
|
@@ -362,7 +497,24 @@ async function runBenchmark() {
|
|
|
362
497
|
hypermind: { claude: { pass: 0, fail: 0 }, gpt4o: { pass: 0, fail: 0 } }
|
|
363
498
|
}
|
|
364
499
|
|
|
365
|
-
const
|
|
500
|
+
const allModels = ['claude-sonnet-4', 'gpt-4o']
|
|
501
|
+
// Filter models based on available API keys
|
|
502
|
+
const models = allModels.filter(m => {
|
|
503
|
+
if (m.includes('claude') && !process.env.ANTHROPIC_API_KEY) {
|
|
504
|
+
console.log(`\n ⚠️ Skipping ${m} (ANTHROPIC_API_KEY not set)`)
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
if (m.includes('gpt') && !process.env.OPENAI_API_KEY) {
|
|
508
|
+
console.log(`\n ⚠️ Skipping ${m} (OPENAI_API_KEY not set)`)
|
|
509
|
+
return false
|
|
510
|
+
}
|
|
511
|
+
return true
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
if (models.length === 0) {
|
|
515
|
+
console.log('\n ❌ No API keys configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY')
|
|
516
|
+
return results
|
|
517
|
+
}
|
|
366
518
|
|
|
367
519
|
for (const model of models) {
|
|
368
520
|
const modelKey = model.includes('claude') ? 'claude' : 'gpt4o'
|