feat: Universal Import with intelligent type matching (v2.1.0)
✨ ONE universal import method for everything - Auto-detects files, URLs, and raw data - Intelligent noun/verb type matching using embeddings - Support for JSON, CSV, YAML, and text formats - Zero configuration required 🧠 Intelligent Type Matching - Uses semantic embeddings to match 31 noun types - Automatically detects 40 verb relationship types - Confidence scores for type predictions - Caching for improved performance 📦 Import Manager - Centralized import logic with lazy loading - Integrates NeuralImportAugmentation for AI processing - Proper CSV parsing with quote handling - Basic YAML support 🎯 Simplified API - brain.import() - ONE method that handles everything - Auto-detection of URLs and file paths - Backwards compatible with existing code - Clean, modern, delightful developer experience 📚 Documentation - Comprehensive import guide in docs/guides/import-anything.md - Examples for every format and use case - Philosophy of simplicity and zero config ✅ Tests - Full unit test coverage for import functionality - Type matching tests for all 31 nouns and 40 verbs - Tests for CSV, YAML, JSON, and text formats BREAKING CHANGES: None - fully backward compatible
This commit is contained in:
parent
492267b509
commit
1ef01f394a
10 changed files with 1833 additions and 102 deletions
160
tests/unit/import.unit.test.ts
Normal file
160
tests/unit/import.unit.test.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Tests for the Universal Import functionality
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { BrainyData } from '../../src/brainyData.js'
|
||||
import * as fs from 'fs/promises'
|
||||
|
||||
describe('Universal Import', () => {
|
||||
let brain: BrainyData
|
||||
|
||||
beforeAll(async () => {
|
||||
// Use mock embedding for speed and in-memory storage
|
||||
brain = new BrainyData({
|
||||
embeddingFunction: async () => new Array(384).fill(0.1),
|
||||
storage: { forceMemoryStorage: true }
|
||||
})
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up any test files
|
||||
await fs.unlink('test-data.csv').catch(() => {})
|
||||
await fs.unlink('test-data.yaml').catch(() => {})
|
||||
})
|
||||
|
||||
describe('Import API', () => {
|
||||
it('should have ONE universal import method', () => {
|
||||
expect(typeof brain.import).toBe('function')
|
||||
})
|
||||
|
||||
it('should NOT have separate importFile method', () => {
|
||||
expect(brain.importFile).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should NOT have separate importUrl method', () => {
|
||||
expect(brain.importUrl).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data Import', () => {
|
||||
it('should import array of objects', async () => {
|
||||
const data = [
|
||||
{ name: 'Alice', type: 'person' },
|
||||
{ name: 'Bob', type: 'person' }
|
||||
]
|
||||
|
||||
const ids = await brain.import(data)
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(Array.isArray(ids)).toBe(true)
|
||||
expect(ids.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should import single object', async () => {
|
||||
const data = { name: 'Test Item', value: 123 }
|
||||
|
||||
const ids = await brain.import(data)
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(Array.isArray(ids)).toBe(true)
|
||||
expect(ids.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should import CSV format', async () => {
|
||||
const csv = `name,age,role
|
||||
John,30,Engineer
|
||||
Jane,25,Designer`
|
||||
|
||||
const ids = await brain.import(csv, { format: 'csv' })
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should import text format', async () => {
|
||||
const text = 'This is a test sentence.'
|
||||
|
||||
const ids = await brain.import(text, { format: 'text' })
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle CSV with quoted values', async () => {
|
||||
const csv = `name,description
|
||||
"Smith, John","A person with a comma in name"
|
||||
"Regular Name","Normal description"`
|
||||
|
||||
const ids = await brain.import(csv, { format: 'csv' })
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should import YAML format', async () => {
|
||||
const yaml = `name: TestProject
|
||||
version: 1.0
|
||||
active: true`
|
||||
|
||||
const ids = await brain.import(yaml, { format: 'yaml' })
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('File Import', () => {
|
||||
it('should import from CSV file', async () => {
|
||||
// Create test file
|
||||
const csvContent = `product,price
|
||||
Widget,19.99
|
||||
Gadget,29.99`
|
||||
|
||||
await fs.writeFile('test-data.csv', csvContent)
|
||||
|
||||
const ids = await brain.import('test-data.csv')
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBe(2)
|
||||
|
||||
// Clean up
|
||||
await fs.unlink('test-data.csv').catch(() => {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Import Options', () => {
|
||||
it('should respect batch size option', async () => {
|
||||
const data = Array(10).fill(null).map((_, i) => ({ id: i, name: `Item ${i}` }))
|
||||
|
||||
const ids = await brain.import(data, { batchSize: 5 })
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBe(10)
|
||||
})
|
||||
|
||||
it('should auto-detect format when not specified', async () => {
|
||||
const jsonData = [{ test: 'data' }]
|
||||
|
||||
const ids = await brain.import(jsonData)
|
||||
|
||||
expect(ids).toBeDefined()
|
||||
expect(ids.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data Retrieval', () => {
|
||||
it('should store imported data with metadata', async () => {
|
||||
const data = { name: 'TestItem', category: 'test' }
|
||||
|
||||
const [id] = await brain.import(data)
|
||||
const noun = await brain.getNoun(id)
|
||||
|
||||
expect(noun).toBeDefined()
|
||||
expect(noun?.metadata).toBeDefined()
|
||||
expect(noun?.metadata?.name).toBe('TestItem')
|
||||
expect(noun?.metadata?.category).toBe('test')
|
||||
})
|
||||
})
|
||||
})
|
||||
212
tests/unit/type-matching.unit.test.ts
Normal file
212
tests/unit/type-matching.unit.test.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* Tests for Intelligent Type Matching with embeddings
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { IntelligentTypeMatcher } from '../../src/augmentations/typeMatching/intelligentTypeMatcher.js'
|
||||
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||||
|
||||
describe('Intelligent Type Matching', () => {
|
||||
let matcher: IntelligentTypeMatcher
|
||||
|
||||
beforeAll(async () => {
|
||||
matcher = new IntelligentTypeMatcher()
|
||||
await matcher.init()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await matcher.dispose()
|
||||
})
|
||||
|
||||
describe('Noun Type Detection', () => {
|
||||
it('should detect Person type from user data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
age: 30
|
||||
})
|
||||
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0)
|
||||
expect(result.alternatives).toBeDefined()
|
||||
expect(result.alternatives.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it('should detect Organization type from company data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
companyName: 'Acme Corp',
|
||||
employees: 500,
|
||||
industry: 'Technology'
|
||||
})
|
||||
|
||||
expect(result.type).toBe(NounType.Organization)
|
||||
expect(result.confidence).toBeGreaterThan(0.3)
|
||||
})
|
||||
|
||||
it('should detect Location type from geographic data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
city: 'San Francisco'
|
||||
})
|
||||
|
||||
expect(result.type).toBe(NounType.Location)
|
||||
expect(result.confidence).toBeGreaterThan(0.3)
|
||||
})
|
||||
|
||||
it('should detect Document type from text content', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
title: 'Research Paper',
|
||||
content: 'Abstract: This paper discusses...',
|
||||
author: 'Dr. Smith',
|
||||
pages: 20
|
||||
})
|
||||
|
||||
// Could be Document or Content
|
||||
expect([NounType.Document, NounType.Content]).toContain(result.type)
|
||||
})
|
||||
|
||||
it('should detect Product type from commercial data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
price: 99.99,
|
||||
sku: 'PROD-123',
|
||||
inventory: 50,
|
||||
productId: 'abc-123'
|
||||
})
|
||||
|
||||
expect(result.type).toBe(NounType.Product)
|
||||
expect(result.confidence).toBeGreaterThan(0.25)
|
||||
})
|
||||
|
||||
it('should detect Event type from temporal data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
startTime: '2024-01-01',
|
||||
endTime: '2024-01-02',
|
||||
attendees: 100,
|
||||
eventType: 'conference'
|
||||
})
|
||||
|
||||
expect(result.type).toBe(NounType.Event)
|
||||
expect(result.confidence).toBeGreaterThan(0.4)
|
||||
})
|
||||
|
||||
it('should handle ambiguous data with alternatives', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
value: 100,
|
||||
type: 'unknown',
|
||||
data: 'mixed'
|
||||
})
|
||||
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.alternatives).toBeDefined()
|
||||
expect(result.alternatives.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Verb Type Detection', () => {
|
||||
it('should detect MemberOf relationship', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'user1', type: 'person' },
|
||||
{ id: 'org1', type: 'organization' },
|
||||
'memberOf'
|
||||
)
|
||||
|
||||
expect(result.type).toBe(VerbType.MemberOf)
|
||||
expect(result.confidence).toBeGreaterThan(0.3)
|
||||
})
|
||||
|
||||
it('should detect CreatedBy relationship', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'doc1', type: 'document' },
|
||||
{ id: 'author1', type: 'person' },
|
||||
'created by'
|
||||
)
|
||||
|
||||
expect(result.type).toBe(VerbType.CreatedBy)
|
||||
expect(result.confidence).toBeGreaterThan(0.5)
|
||||
})
|
||||
|
||||
it('should detect Contains relationship', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'folder1', type: 'collection' },
|
||||
{ id: 'file1', type: 'file' },
|
||||
'contains'
|
||||
)
|
||||
|
||||
expect(result.type).toBe(VerbType.Contains)
|
||||
expect(result.confidence).toBeGreaterThan(0.4)
|
||||
})
|
||||
|
||||
it('should handle unknown relationships with defaults', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'a' },
|
||||
{ id: 'b' },
|
||||
'somehow connected'
|
||||
)
|
||||
|
||||
expect(result.type).toBeDefined()
|
||||
expect(Object.values(VerbType)).toContain(result.type)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type Coverage', () => {
|
||||
it('should have embeddings for all 31 noun types', async () => {
|
||||
const nounTypes = Object.values(NounType)
|
||||
expect(nounTypes.length).toBe(31)
|
||||
|
||||
// Test that each type can be matched
|
||||
for (const nounType of nounTypes) {
|
||||
const result = await matcher.matchNounType({
|
||||
type: nounType,
|
||||
test: 'coverage check'
|
||||
})
|
||||
expect(result.type).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should have embeddings for all 40 verb types', async () => {
|
||||
const verbTypes = Object.values(VerbType)
|
||||
expect(verbTypes.length).toBe(40)
|
||||
|
||||
// Test that each type can be matched
|
||||
for (const verbType of verbTypes) {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'source' },
|
||||
{ id: 'target' },
|
||||
verbType
|
||||
)
|
||||
expect(result.type).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cache Performance', () => {
|
||||
it('should cache repeated type matches', async () => {
|
||||
const testData = { name: 'Cache Test', value: 123 }
|
||||
|
||||
const start1 = Date.now()
|
||||
const result1 = await matcher.matchNounType(testData)
|
||||
const time1 = Date.now() - start1
|
||||
|
||||
const start2 = Date.now()
|
||||
const result2 = await matcher.matchNounType(testData)
|
||||
const time2 = Date.now() - start2
|
||||
|
||||
expect(result1.type).toBe(result2.type)
|
||||
expect(result1.confidence).toBe(result2.confidence)
|
||||
// Second call should be faster due to cache
|
||||
expect(time2).toBeLessThanOrEqual(time1)
|
||||
})
|
||||
|
||||
it('should clear cache when requested', async () => {
|
||||
const testData = { name: 'Clear Cache Test' }
|
||||
|
||||
await matcher.matchNounType(testData) // Populate cache
|
||||
matcher.clearCache()
|
||||
|
||||
// Should still work after cache clear
|
||||
const result = await matcher.matchNounType(testData)
|
||||
expect(result.type).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue