brainy/tests/unit/neural/NaturalLanguageProcessor.test.ts

379 lines
13 KiB
TypeScript
Raw Permalink Normal View History

/**
* Natural Language Processor Tests
* Tests NLP features including query parsing, entity extraction, and sentiment analysis
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy'
import { NaturalLanguageProcessor } from '../../../src/neural/naturalLanguageProcessor'
import { NounType } from '../../../src/types/graphTypes'
describe('NaturalLanguageProcessor', () => {
let brain: Brainy<any>
let nlp: NaturalLanguageProcessor
beforeEach(async () => {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1) Brainy 8.0 makes subtype required by default on every public write path (`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`, import). Per the locked C-1 contract, every entity and relation gets a non-empty subtype string by the time the storage layer sees it. OPT-OUT REMAINS FULLY SUPPORTED The runtime flag is still consumer-controlled. Three opt-out paths cover migration / legacy fixtures / typed escape: - `new Brainy({ requireSubtype: false })` — last-resort: turn off the contract entirely. Recommended only for migration windows or test fixtures that legitimately can't supply a subtype. - `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` — per-type allowlist: strict everywhere except the listed types. - `brain.requireSubtype(type, options)` — per-type registration with optional vocabulary. Composes with the brain-wide flag. Default is now `true`. Opt-out is explicit and documented; nothing silently degrades. TEST SWEEP Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call site across 120 test files. Three sed patterns covered the shapes: - `new Brainy({` → `new Brainy({ requireSubtype: false,` - `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,` - `new Brainy()` → `new Brainy({ requireSubtype: false })` tests/helpers/test-factory.ts → createTestConfig() defaults `requireSubtype: false` so test files using the helper inherit the opt-out without per-site edits. The test sites that DO exercise subtype semantics (the subtype-and-facets suite, the strict-mode-self-test suite, the verb- subtype-and-enforcement suite, etc.) already pass real subtypes — they were the 7.30.x acceptance tests for this contract. Those tests continue to pass unchanged. CHANGES src/brainy.ts - normalizeConfig() — `requireSubtype` default `false` → `true`. Comment refreshed to document the three opt-out paths. tests/* (120 files) - Bulk-edited brain construction sites. No functional test changes; the opt-out preserves the test author's original intent. tests/helpers/test-factory.ts - createTestConfig() base config gains `requireSubtype: false`. NO-OP for consumers who were already passing subtype on every write. For consumers who weren't, the upgrade path is one of the three opt-out forms above. Migration recipe documented in 8.0 release notes (next commit). VERIFICATION - npx tsc --noEmit: clean - npm test: 1408 / 1409 (same pre-existing race-condition outstanding; no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
await brain.init()
nlp = new NaturalLanguageProcessor(brain)
// Create a rich test dataset for NLP to work with
await brain.add({
data: 'John Smith is a senior software engineer at Google working on machine learning',
type: NounType.Person,
metadata: {
name: 'John Smith',
role: 'engineer',
level: 'senior',
company: 'Google',
skills: ['JavaScript', 'Python', 'Machine Learning']
}
})
await brain.add({
data: 'Machine learning research paper on neural networks published in 2024',
type: NounType.Document,
metadata: {
title: 'Advances in Neural Networks',
category: 'research',
year: 2024,
topics: ['AI', 'neural networks', 'deep learning']
}
})
await brain.add({
data: 'TechCorp headquarters located in San Francisco California',
type: NounType.Location,
metadata: {
company: 'TechCorp',
city: 'San Francisco',
state: 'CA',
type: 'headquarters'
}
})
await brain.add({
data: 'Product launch event scheduled for December 2024',
type: NounType.Event,
metadata: {
eventType: 'launch',
date: '2024-12-01',
status: 'scheduled'
}
})
await brain.add({
data: 'Python programming language used for data science and machine learning',
type: NounType.Concept,
metadata: {
category: 'programming',
uses: ['data science', 'machine learning', 'web development']
}
})
})
afterEach(async () => {
await brain.close()
})
describe('processNaturalQuery - Core Functionality', () => {
it('should handle questions about entities', async () => {
const query = 'What is John Smith working on?'
const result = await nlp.processNaturalQuery(query)
expect(result).toBeDefined()
// Should search for John Smith
const hasSearch = result.similar || result.like || result.where
expect(hasSearch).toBeDefined()
})
it('should extract location-based queries', async () => {
const query = 'Find companies in San Francisco'
const result = await nlp.processNaturalQuery(query)
expect(result).toBeDefined()
// Should have search criteria (location might be in where clause)
expect(result.like || result.where).toBeDefined()
})
it('should extract limit from queries', async () => {
const query = 'Show me the top 5 machine learning papers'
const result = await nlp.processNaturalQuery(query)
expect(result).toBeDefined()
if (result.limit) {
const limit = typeof result.limit === 'string' ? parseInt(result.limit) : result.limit
expect(limit).toBeGreaterThan(0)
expect(limit).toBeLessThanOrEqual(10) // Should extract a reasonable limit
}
// Limit extraction is optional feature
})
})
describe('extract - Entity and Information Extraction', () => {
it('should extract entities from text', async () => {
const text = 'John Smith works at Google on machine learning projects'
const extraction = await nlp.extract(text)
expect(extraction).toBeDefined()
expect(Array.isArray(extraction)).toBe(true)
// This unit suite runs with mock (zero-vector) embeddings, so the neural embedding
// signal can't fire here — only the array shape is asserted. Real, non-empty extraction
// is verified against actual embeddings in
// tests/integration/advanced-apis-regression.test.ts (BR-ADV-FEATURES-BUN). An empty
// result here reflects the mock embeddings, not expected production behaviour.
})
it('should extract topics and concepts', async () => {
const text = 'This paper discusses neural networks, deep learning, and artificial intelligence'
const extraction = await nlp.extract(text, { types: ['concept', 'topic'] })
expect(extraction).toBeDefined()
expect(Array.isArray(extraction)).toBe(true)
// May or may not find specific concepts depending on neural matcher
// Just verify extraction works
})
it('should extract dates and times', async () => {
const text = 'The meeting is scheduled for December 15, 2024 at 3:00 PM'
const extraction = await nlp.extract(text, { types: ['date', 'time', 'event'] })
expect(extraction).toBeDefined()
expect(Array.isArray(extraction)).toBe(true)
// Neural extraction may or may not find specific dates
})
it('should extract locations', async () => {
const text = 'Our offices are in San Francisco, New York, and London'
const extraction = await nlp.extract(text, { types: ['location', 'place'] })
expect(extraction).toBeDefined()
expect(Array.isArray(extraction)).toBe(true)
// Neural extraction may or may not find specific locations
})
})
describe('sentiment - Sentiment Analysis', () => {
it('should analyze positive sentiment', async () => {
const text = 'This is an excellent machine learning framework! Really impressive results.'
const sentiment = await nlp.sentiment(text)
expect(sentiment).toBeDefined()
expect(sentiment.overall.score).toBeGreaterThan(0) // Positive score
expect(sentiment.overall.label).toBe('positive')
})
it('should analyze negative sentiment', async () => {
const text = 'This approach is terrible and the results are disappointing.'
const sentiment = await nlp.sentiment(text)
expect(sentiment).toBeDefined()
expect(sentiment.overall.score).toBeLessThan(0) // Negative score
expect(sentiment.overall.label).toBe('negative')
})
it('should analyze neutral sentiment', async () => {
const text = 'The document contains information about machine learning.'
const sentiment = await nlp.sentiment(text)
expect(sentiment).toBeDefined()
expect(Math.abs(sentiment.overall.score)).toBeLessThan(0.3) // Close to neutral
expect(sentiment.overall.label).toBe('neutral')
})
it('should provide magnitude scores', async () => {
const text = 'Machine learning is transforming technology'
const sentiment = await nlp.sentiment(text)
expect(sentiment).toBeDefined()
expect(sentiment.overall.magnitude).toBeDefined()
// Magnitude can be 0 for neutral text
expect(sentiment.overall.magnitude).toBeGreaterThanOrEqual(0)
expect(sentiment.overall.magnitude).toBeLessThanOrEqual(10)
})
})
describe('Query Pattern Recognition', () => {
it('should recognize question patterns', async () => {
const questions = [
'What is machine learning?',
'Who is John Smith?',
'Where is Google located?',
'When is the product launch?',
'How does Python work?'
]
for (const q of questions) {
const result = await nlp.processNaturalQuery(q)
expect(result).toBeDefined()
// Should produce a search query
expect(result.similar || result.like || result.where).toBeDefined()
}
})
it('should recognize command patterns', async () => {
const commands = [
'Find all engineers',
'Show me recent papers',
'List upcoming events',
'Get information about Google',
'Search for machine learning'
]
for (const cmd of commands) {
const result = await nlp.processNaturalQuery(cmd)
expect(result).toBeDefined()
// Should have search criteria
expect(result.similar || result.like || result.where).toBeDefined()
}
})
})
describe('Advanced Features', () => {
it('should handle ambiguous queries gracefully', async () => {
const query = 'stuff about that thing'
const result = await nlp.processNaturalQuery(query)
expect(result).toBeDefined()
// Should still attempt to create a query
expect(result).not.toBeNull()
})
it('should handle very long queries', async () => {
const longQuery = 'Find ' + 'machine learning '.repeat(50) + 'papers'
const result = await nlp.processNaturalQuery(longQuery)
expect(result).toBeDefined()
// Should still produce a valid query
expect(result.like || result.where).toBeDefined()
})
it('should handle empty queries', async () => {
const result = await nlp.processNaturalQuery('')
expect(result).toBeDefined()
// Empty query returns minimal query structure
expect(result).toHaveProperty('like')
})
it('should extract modifiers and preferences', async () => {
const queries = [
'Find the most recent papers',
'Show the best engineers',
'Get the latest news',
'Find popular frameworks'
]
for (const q of queries) {
const result = await nlp.processNaturalQuery(q)
expect(result).toBeDefined()
// Might have boost or ordering
if (result.boost) {
expect(['recent', 'popular', 'verified']).toContain(result.boost)
}
}
})
})
describe('Integration with Brainy', () => {
it('should produce queries that work with brain.find()', async () => {
const naturalQueries = [
'Find machine learning papers',
'Search for John Smith',
'Get information about Python'
]
for (const nq of naturalQueries) {
const tripleQuery = await nlp.processNaturalQuery(nq)
// Should be able to use with brain.find()
const results = await brain.find(tripleQuery as any)
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
}
})
it('should work with complex real-world queries', async () => {
const complexQuery = 'Find senior engineers at tech companies working on AI'
const tripleQuery = await nlp.processNaturalQuery(complexQuery)
expect(tripleQuery).toBeDefined()
// Use with brain
const results = await brain.find(tripleQuery as any)
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
})
it('should handle entity extraction for brain operations', async () => {
const text = 'John Smith is a talented engineer at Google'
const extraction = await nlp.extract(text)
// Could use extracted entities to create new entries
if (extraction && extraction.length > 0) {
for (const entity of extraction) {
if (entity.type === 'person' && entity.text) {
// Could add to brain
const id = await brain.add({
data: entity.text,
type: NounType.Person,
metadata: { extracted: true }
})
expect(id).toBeDefined()
// Verify it was added
const retrieved = await brain.get(id)
expect(retrieved).toBeDefined()
}
}
}
})
})
describe('Performance', () => {
it('should process queries quickly', async () => {
const query = 'Find machine learning papers from 2024'
const startTime = Date.now()
const result = await nlp.processNaturalQuery(query)
const duration = Date.now() - startTime
expect(result).toBeDefined()
expect(duration).toBeLessThan(200) // Should be fast
})
it('should handle multiple queries efficiently', async () => {
const queries = Array(10).fill('Find AI research')
const startTime = Date.now()
const results = await Promise.all(
queries.map(q => nlp.processNaturalQuery(q))
)
const duration = Date.now() - startTime
expect(results).toHaveLength(10)
expect(duration).toBeLessThan(2000) // Should handle batch in reasonable time
})
it('should cache pattern matching for performance', async () => {
const query = 'Find machine learning papers'
// First call - might be slower
const start1 = Date.now()
await nlp.processNaturalQuery(query)
const time1 = Date.now() - start1
// Second call - should be faster due to caching
const start2 = Date.now()
await nlp.processNaturalQuery(query)
const time2 = Date.now() - start2
// Second should be similar or faster
expect(time2).toBeLessThanOrEqual(time1 + 10)
})
})
})