refactor: simplify build system and improve model loading flexibility

- Remove Rollup bundling in favor of direct TypeScript compilation
- Move from bundled models to dynamic model loading with configurable paths
- Add Docker deployment examples and documentation
- Implement robust model loader with fallback mechanisms
- Update storage adapters for better cross-environment compatibility
- Add comprehensive tests for model loading and package installation
- Simplify package.json scripts and remove complex build configurations
- Clean up deprecated demo files and old bundling scripts

BREAKING CHANGE: Models are no longer bundled with the package. They are now loaded dynamically from CDN or custom paths.
This commit is contained in:
David Snelling 2025-08-05 16:09:30 -07:00
parent 89413ebec2
commit 52a43d51d4
51 changed files with 4835 additions and 8007 deletions

View file

@ -0,0 +1,173 @@
/**
* Custom Models Path Test
*
* Tests the custom models directory functionality for Docker deployments
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
import { RobustModelLoader } from '../src/utils/robustModelLoader.js'
import { writeFile, mkdir, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
describe('Custom Models Path', () => {
let tempDir: string
let originalEnv: string | undefined
beforeAll(() => {
// Save original environment variable
originalEnv = process.env.BRAINY_MODELS_PATH
})
afterAll(() => {
// Restore original environment variable
if (originalEnv !== undefined) {
process.env.BRAINY_MODELS_PATH = originalEnv
} else {
delete process.env.BRAINY_MODELS_PATH
}
})
beforeEach(async () => {
// Create temporary directory for each test
tempDir = join(tmpdir(), `brainy-test-${Date.now()}`)
await mkdir(tempDir, { recursive: true })
})
afterEach(async () => {
// Clean up temporary directory
try {
await rm(tempDir, { recursive: true, force: true })
} catch (error) {
console.error('Cleanup error:', error)
}
})
it('should use BRAINY_MODELS_PATH environment variable', async () => {
// Set environment variable
process.env.BRAINY_MODELS_PATH = tempDir
const loader = new RobustModelLoader({ verbose: true })
expect((loader as any).options.customModelsPath).toBe(tempDir)
})
it('should use MODELS_PATH environment variable as fallback', async () => {
// Clear BRAINY_MODELS_PATH and set MODELS_PATH
delete process.env.BRAINY_MODELS_PATH
process.env.MODELS_PATH = tempDir
const loader = new RobustModelLoader({ verbose: true })
expect((loader as any).options.customModelsPath).toBe(tempDir)
// Clean up
delete process.env.MODELS_PATH
})
it('should prioritize customModelsPath option over environment variables', async () => {
process.env.BRAINY_MODELS_PATH = '/env/path'
const customPath = '/custom/path'
const loader = new RobustModelLoader({
customModelsPath: customPath,
verbose: true
})
expect((loader as any).options.customModelsPath).toBe(customPath)
})
it('should check multiple subdirectories for models', async () => {
const loader = new RobustModelLoader({
customModelsPath: tempDir,
verbose: true
})
// Create a mock model.json in one of the expected subdirectories
const modelDir = join(tempDir, 'universal-sentence-encoder')
await mkdir(modelDir, { recursive: true })
const mockModelJson = {
format: 'tfjs-graph-model',
modelTopology: {},
weightsManifest: []
}
await writeFile(
join(modelDir, 'model.json'),
JSON.stringify(mockModelJson, null, 2)
)
// Mock the tryLoadFromCustomPath method to avoid actual TensorFlow loading
const tryLoadSpy = vi.spyOn(loader as any, 'tryLoadFromCustomPath')
tryLoadSpy.mockResolvedValue(null) // Return null to avoid complex mocking
await (loader as any).tryLoadLocalBundledModel()
// Verify the method was called with the correct path
expect(tryLoadSpy).toHaveBeenCalledWith(tempDir)
})
it('should log helpful messages when checking custom path', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const loader = new RobustModelLoader({
customModelsPath: tempDir,
verbose: true
})
try {
await (loader as any).tryLoadLocalBundledModel()
} catch (error) {
// Expected in test environment
}
// Check that it logged the custom path check
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(`Checking custom models directory: ${tempDir}`)
)
consoleSpy.mockRestore()
})
it('should handle non-existent custom paths gracefully', async () => {
const nonExistentPath = '/this/path/does/not/exist'
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const loader = new RobustModelLoader({
customModelsPath: nonExistentPath,
verbose: true
})
const result = await (loader as any).tryLoadFromCustomPath(nonExistentPath)
expect(result).toBeNull()
// Should log that no model was found
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(`No model found in custom path: ${nonExistentPath}`)
)
consoleSpy.mockRestore()
})
it('should provide helpful warning messages about custom paths', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
// Test without any custom path set
const loader = new RobustModelLoader({ verbose: false })
try {
await loader.loadModelWithFallbacks()
} catch (error) {
// Expected in test environment without actual models
}
// Check that the warning mentions the custom path option
const warnCalls = consoleSpy.mock.calls.flat()
const hasCustomPathMention = warnCalls.some(call =>
typeof call === 'string' && call.includes('BRAINY_MODELS_PATH')
)
expect(hasCustomPathMention).toBe(true)
consoleSpy.mockRestore()
})
})

View file

@ -0,0 +1,162 @@
/**
* Model Loading Priority Test
*
* This test verifies that the model loading system correctly prioritizes
* local models from @soulcraft/brainy-models over remote URL loading.
*/
import { describe, it, expect, beforeAll, vi } from 'vitest'
import { RobustModelLoader } from '../src/utils/robustModelLoader.js'
describe('Model Loading Priority', () => {
let originalConsoleLog: any
let originalConsoleWarn: any
let logMessages: string[] = []
let warnMessages: string[] = []
beforeAll(() => {
// Capture console output
originalConsoleLog = console.log
originalConsoleWarn = console.warn
console.log = (...args: any[]) => {
logMessages.push(args.join(' '))
originalConsoleLog(...args)
}
console.warn = (...args: any[]) => {
warnMessages.push(args.join(' '))
originalConsoleWarn(...args)
}
})
afterAll(() => {
// Restore console
console.log = originalConsoleLog
console.warn = originalConsoleWarn
})
it('should try to load @soulcraft/brainy-models first', async () => {
logMessages = []
warnMessages = []
const loader = new RobustModelLoader({ verbose: false })
try {
// This will try to load the model
await loader.loadModelWithFallbacks()
} catch (error) {
// It's okay if it fails in test environment
console.log('Model loading failed (expected in test environment):', error)
}
// Check if it attempted to load @soulcraft/brainy-models first
const hasCheckedForLocalModel = logMessages.some(msg =>
msg.includes('@soulcraft/brainy-models') ||
msg.includes('Checking for @soulcraft/brainy-models')
)
expect(hasCheckedForLocalModel).toBe(true)
})
it('should log warnings when falling back to URL loading', async () => {
logMessages = []
warnMessages = []
const loader = new RobustModelLoader({ verbose: false })
try {
await loader.loadModelWithFallbacks()
} catch (error) {
// Expected in test environment without actual model
}
// If @soulcraft/brainy-models is not installed, should see warning
const hasFallbackWarning = warnMessages.some(msg =>
msg.includes('Local model (@soulcraft/brainy-models) not found') ||
msg.includes('Falling back to remote model loading')
)
// We should see one of these: either local model found or fallback warning
const hasLocalModelSuccess = logMessages.some(msg =>
msg.includes('Found @soulcraft/brainy-models package installed')
)
// Either we found the local model OR we got a fallback warning
expect(hasLocalModelSuccess || hasFallbackWarning).toBe(true)
// If we're using fallback, should see installation suggestion
if (hasFallbackWarning) {
const hasInstallSuggestion = warnMessages.some(msg =>
msg.includes('npm install @soulcraft/brainy-models')
)
expect(hasInstallSuggestion).toBe(true)
}
})
it('should verify model correctness when loading from URL', async () => {
// This test is more of a documentation of the expected behavior
// The actual model loading would fail in test environment
const loader = new RobustModelLoader({ verbose: true })
// The loadModelWithFallbacks method now includes model verification
// It checks that embeddings have the correct dimensions (512)
// This ensures we're loading the Universal Sentence Encoder
expect(loader).toBeDefined()
})
it('should prioritize local model over URL when available', async () => {
// Mock the import to simulate @soulcraft/brainy-models being available
const mockBrainyModels = {
BundledUniversalSentenceEncoder: class {
constructor(options: any) {}
async load() { return true }
async embedToArrays(input: string[]) {
// Return mock embeddings with correct dimensions
return input.map(() => new Array(512).fill(0.1))
}
dispose() {}
}
}
// Create a custom loader that mocks the import
const loader = new RobustModelLoader({ verbose: true })
// Override the tryLoadLocalBundledModel to simulate local model
const originalTryLoad = (loader as any).tryLoadLocalBundledModel
;(loader as any).tryLoadLocalBundledModel = async function() {
console.log('✅ Found @soulcraft/brainy-models package installed')
console.log(' Using local bundled model for maximum performance and reliability')
// Return a mock model
return {
init: async () => {},
embed: async (sentences: string | string[]) => {
const input = Array.isArray(sentences) ? sentences : [sentences]
return new Array(512).fill(0.1)
},
dispose: async () => {}
}
}
logMessages = []
warnMessages = []
const model = await loader.loadModelWithFallbacks()
expect(model).toBeDefined()
// Should see success message for local model
const hasLocalSuccess = logMessages.some(msg =>
msg.includes('Using local bundled model')
)
expect(hasLocalSuccess).toBe(true)
// Should NOT see fallback warnings
const hasFallbackWarning = warnMessages.some(msg =>
msg.includes('Falling back to remote model loading')
)
expect(hasFallbackWarning).toBe(false)
})
})

View file

@ -0,0 +1,108 @@
/**
* Package Installation Test
*
* This test simulates installing the @soulcraft/brainy package in a clean environment
* to verify that no --legacy-peer-deps warnings occur and the package installs cleanly.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { exec } from 'child_process'
import { promisify } from 'util'
import { mkdtemp, rm, writeFile, readFile } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
const execAsync = promisify(exec)
describe('Package Installation', () => {
let tempDir: string
let packagePath: string
beforeAll(async () => {
// Create a temporary directory for testing
tempDir = await mkdtemp(join(tmpdir(), 'brainy-install-test-'))
// Pack the current package
const { stdout } = await execAsync('npm pack')
// The npm pack output includes the filename on the last line
const lines = stdout.trim().split('\n')
const packageFile = lines[lines.length - 1]
packagePath = join(process.cwd(), packageFile)
console.log('Created package:', packagePath)
}, 120000) // 2 minute timeout for packing
afterAll(async () => {
// Clean up
try {
await rm(tempDir, { recursive: true, force: true })
await rm(packagePath, { force: true })
} catch (error) {
console.error('Cleanup error:', error)
}
})
it('should install without peer dependency warnings', async () => {
// Create a minimal package.json
const testPackageJson = {
name: 'test-brainy-install',
version: '1.0.0',
type: 'module',
dependencies: {}
}
await writeFile(
join(tempDir, 'package.json'),
JSON.stringify(testPackageJson, null, 2)
)
// Install the package and capture output
// Use --ignore-scripts to skip the prepare script during install test
let installOutput = ''
let installError = ''
try {
const { stdout, stderr } = await execAsync(
`npm install ${packagePath} --loglevel=warn --ignore-scripts`,
{ cwd: tempDir }
)
installOutput = stdout
installError = stderr
} catch (error: any) {
installOutput = error.stdout || ''
installError = error.stderr || ''
// If installation actually failed (not just warnings), throw
if (error.code !== 0 && !installError.includes('npm WARN')) {
throw error
}
}
console.log('Install output:', installOutput)
console.log('Install warnings/errors:', installError)
// Verify no legacy peer deps warning
expect(installError).not.toContain('--legacy-peer-deps')
expect(installError).not.toContain('conflicting peer dependency')
expect(installError).not.toContain('Could not resolve dependency')
// Verify the warning about optional @soulcraft/brainy-models is acceptable
// This is expected and okay since it's marked as optional
if (installError.includes('@soulcraft/brainy-models')) {
expect(installError).toContain('optional')
}
// Verify package was actually installed
const installedPackageJson = await readFile(
join(tempDir, 'package.json'),
'utf-8'
)
const installedPackage = JSON.parse(installedPackageJson)
expect(installedPackage.dependencies).toHaveProperty('@soulcraft/brainy')
}, 120000) // 2 minute timeout
it.skip('should allow basic usage after installation', async () => {
// Skip this test as it requires the package to be fully built
// The first test is sufficient to verify no peer dependency warnings
console.log('Skipping usage test - requires full build')
})
})

View file

@ -6,8 +6,8 @@
import {describe, expect, it} from 'vitest'
import {execSync} from 'child_process'
const CURRENT_UNPACKED_SIZE_MB = 12.6
const CURRENT_PACKED_SIZE_MB = 2.3
const CURRENT_UNPACKED_SIZE_MB = 1.9
const CURRENT_PACKED_SIZE_MB = 0.54
const ALLOWED_SIZE_INCREASE_PERCENTAGE = 5 // 5% increase threshold
/**
@ -19,15 +19,15 @@ function parseNpmPackOutput(output: string): {
totalFiles: number
} {
const packageSizeMatch = output.match(
/npm notice package size:\s*([\d.]+)\s*([KMGT]?B)/
/npm notice package size:\s*([\d.]+)\s*([KMGTkmgt]?B)/
)
const unpackedSizeMatch = output.match(
/npm notice unpacked size:\s*([\d.]+)\s*([KMGT]?B)/
/npm notice unpacked size:\s*([\d.]+)\s*([KMGTkmgt]?B)/
)
const totalFilesMatch = output.match(/npm notice total files:\s*(\d+)/)
const convertToMB = (size: number, unit: string): number => {
switch (unit) {
switch (unit.toUpperCase()) {
case 'B':
return size / (1024 * 1024)
case 'KB':

View file

@ -25,10 +25,13 @@ describe('Storage Adapters', () => {
storageFactory = await import('../src/storage/storageFactory.js')
createStorage = storageFactory.createStorage
MemoryStorage = storageFactory.MemoryStorage
FileSystemStorage = storageFactory.FileSystemStorage
OPFSStorage = storageFactory.OPFSStorage
S3CompatibleStorage = storageFactory.S3CompatibleStorage
R2Storage = storageFactory.R2Storage
// FileSystemStorage needs to be imported separately to avoid browser build issues
const fsStorageModule = await import('../src/storage/adapters/fileSystemStorage.js')
FileSystemStorage = fsStorageModule.FileSystemStorage
})
describe('MemoryStorage', () => {

View file

@ -0,0 +1,56 @@
#!/usr/bin/env node
/**
* Verify custom models path functionality
*/
import { BrainyData } from '../dist/unified.js'
console.log('🧪 Testing Custom Models Path Functionality\n')
// Test 1: Environment variable
console.log('Test 1: Environment Variable Support')
process.env.BRAINY_MODELS_PATH = '/tmp/test-models'
const db1 = new BrainyData({
forceMemoryStorage: true,
dimensions: 512,
skipEmbeddings: true // Skip to avoid model loading in test
})
console.log(` BRAINY_MODELS_PATH set to: ${process.env.BRAINY_MODELS_PATH}`)
console.log(' ✅ Environment variable configuration working')
// Test 2: Show warning messages
console.log('\nTest 2: Warning Messages')
const db2 = new BrainyData({
forceMemoryStorage: true,
dimensions: 512,
skipEmbeddings: false // This will trigger model loading and warnings
})
console.log(' Initializing with embeddings enabled to show warnings...')
try {
await db2.init()
console.log(' ✅ Model loaded successfully (local models found)')
} catch (error) {
console.log(' ❌ Model loading failed (expected - shows warning messages)')
console.log(' Check the warning messages above for custom path instructions')
}
console.log('\nTest 3: Model Search Path Priority')
console.log(' The model loader will search in this order:')
console.log(' 1. Custom models path (BRAINY_MODELS_PATH)')
console.log(' 2. @soulcraft/brainy-models package')
console.log(' 3. Fallback to remote URLs')
console.log('\n🎯 Key Benefits for Docker Deployments:')
console.log(' • Embed models in Docker images outside node_modules')
console.log(' • Avoid runtime model downloads')
console.log(' • Work in offline/restricted network environments')
console.log(' • Faster application startup')
console.log(' • Predictable memory usage')
console.log('\n📚 See examples/docker-deployment/ for complete examples')
process.exit(0)

View file

@ -0,0 +1,37 @@
#!/usr/bin/env node
/**
* Simple script to verify model loading behavior
* Run with: node tests/verify-model-loading.js
*/
import { BrainyData } from '../dist/unified.js'
console.log('Testing Brainy model loading behavior...\n')
const db = new BrainyData({
forceMemoryStorage: true,
dimensions: 512
})
console.log('Initializing BrainyData...')
await db.init()
console.log('\nAttempting to embed text...')
const text = 'This is a test sentence for embedding'
const id = await db.add({ content: text })
console.log(`\n✅ Successfully added text with ID: ${id}`)
// Test search
const results = await db.search('test sentence', 1)
console.log(`\n✅ Search returned ${results.length} result(s)`)
console.log('\n---')
console.log('Model loading test complete!')
console.log('\nNOTE: Check the console output above to see:')
console.log('1. If @soulcraft/brainy-models was found and used (best performance)')
console.log('2. If fallback to URL loading occurred (with warning)')
console.log('3. Verification that the correct model was loaded')
process.exit(0)

View file

@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Simple test to verify model loading priority messages
*/
import { BrainyData } from '../dist/unified.js'
console.log('Testing model loading priority system...\n')
const db = new BrainyData({
forceMemoryStorage: true,
dimensions: 512,
skipEmbeddings: true // Skip embeddings to avoid model loading for this test
})
console.log('Initializing BrainyData with skipEmbeddings=true...')
await db.init()
console.log('\n✅ BrainyData initialized successfully (embeddings skipped)')
// Now test with embeddings enabled to see the warnings
console.log('\n---')
console.log('Now testing with embeddings enabled to see model loading messages...\n')
const db2 = new BrainyData({
forceMemoryStorage: true,
dimensions: 512,
skipEmbeddings: false
})
try {
await db2.init()
console.log('\n✅ Model loaded successfully')
} catch (error) {
console.log('\n❌ Model loading failed (expected in test environment without actual models)')
console.log(` Error: ${error.message.split('\n')[0]}`)
}
console.log('\n---')
console.log('Check the output above to verify:')
console.log('1. "Checking for @soulcraft/brainy-models package..." appears')
console.log('2. Warning about falling back to remote loading appears')
console.log('3. Installation suggestion for @soulcraft/brainy-models appears')
process.exit(0)