diff --git a/Makefile b/Makefile index 1336e9e..65ab820 100644 --- a/Makefile +++ b/Makefile @@ -59,26 +59,19 @@ stop: ## Stop server (if running as daemon) @pkill -f "node.*openmemory" || echo "No server process found" # Testing -test: ## Run all tests - @echo "๐Ÿงช Running all tests..." - @echo "Testing backend API..." - node tests/backend/api-simple.test.js - @echo "Testing JavaScript SDK..." - node tests/js-sdk/sdk-simple.test.js - @echo "Testing Python SDK..." - cd tests/py-sdk && python test-simple.py +test: test-backend test-js-sdk test-py-sdk test-backend: ## Run backend tests only @echo "๐Ÿงช Testing backend API..." - node tests/backend/api-simple.test.js + node tests/backend/*.test.js test-js-sdk: ## Run JavaScript SDK tests only @echo "๐Ÿงช Testing JavaScript SDK..." - node tests/js-sdk/sdk-simple.test.js + node tests/js-sdk/js-sdk.test.js test-py-sdk: ## Run Python SDK tests only @echo "๐Ÿงช Testing Python SDK..." - cd tests/py-sdk && python test-simple.py + cd tests/py-sdk && python test-sdk.py test-integration: ## Run integration tests @echo "๐Ÿ”— Running integration tests..." @@ -173,4 +166,4 @@ quick-test: build test-backend ## Quick test after build @echo "โšก Quick test complete!" full-check: clean install build lint test ## Full check before commit - @echo "โœ… Full check complete - ready to commit!" \ No newline at end of file + @echo "โœ… Full check complete - ready to commit!" diff --git a/backend/src/hsg/index.ts b/backend/src/hsg/index.ts index f3b89a3..0c6d1d8 100644 --- a/backend/src/hsg/index.ts +++ b/backend/src/hsg/index.ts @@ -327,10 +327,50 @@ export async function pruneWeakWaypoints(): Promise { } import { embedForSector, embedMultiSector, cosineSimilarity, bufferToVector, vectorToBuffer, EmbeddingResult } from '../embedding' import { chunkText } from '../utils/chunking' +import type { MemoryFilters } from '../types' + +/** + * Helper function to check if a memory passes all filters + */ +function passesFilters(memory: HSGMemory, filters?: MemoryFilters): boolean { + // Salience filtering + if (filters?.min_score !== undefined && memory.salience < filters.min_score) { + return false + } + + // Tag filtering + if (filters?.tags && filters.tags.length > 0) { + try { + const memoryTags = memory.tags ? JSON.parse(memory.tags) : [] + const hasMatchingTag = filters.tags.some(tag => memoryTags.includes(tag)) + if (!hasMatchingTag) return false + } catch { + // If JSON parsing fails, memory doesn't pass filter + return false + } + } + + // Metadata filtering + if (filters?.metadata && Object.keys(filters.metadata).length > 0) { + try { + const memoryMeta = memory.meta ? JSON.parse(memory.meta) : {} + const allMatch = Object.entries(filters.metadata).every( + ([key, value]) => memoryMeta[key] === value + ) + if (!allMatch) return false + } catch { + // If JSON parsing fails, memory doesn't pass filter + return false + } + } + + return true +} + export async function hsgQuery( queryText: string, k: number = 10, - filters?: { sectors?: string[], minSalience?: number } + filters?: MemoryFilters ): Promise { const queryClassification = classifyContent(queryText) const candidateSectors = [queryClassification.primary, ...queryClassification.additional] @@ -371,7 +411,10 @@ export async function hsgQuery( for (const memoryId of Array.from(allMemoryIds)) { const memory = await q.get_mem.get(memoryId) if (!memory) continue - if (filters?.minSalience && memory.salience < filters.minSalience) continue + + // Apply all filters (salience, tags, metadata) + if (!passesFilters(memory, filters)) continue + let bestSimilarity = 0 let bestSector = memory.primary_sector for (const [sector, results] of Object.entries(sectorResults)) { diff --git a/backend/src/server/index.ts b/backend/src/server/index.ts index b998b55..3b5e23d 100644 --- a/backend/src/server/index.ts +++ b/backend/src/server/index.ts @@ -119,10 +119,11 @@ app.post('/memory/query', async (req: any, res: any) => { const b = req.body as q_req const k = b.k || 8 try { - const filters = { - sectors: b.filters?.sector ? [b.filters.sector] : undefined, - minSalience: b.filters?.min_score - } + // Convert sector string to sectors array for backward compatibility + const filters = b.filters ? { + ...b.filters, + sectors: b.filters.sector ? [b.filters.sector] : undefined + } : undefined const matches = await hsgQuery(b.query, k, filters) res.json({ query: b.query, diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts index ee4a5c4..d04b39d 100644 --- a/backend/src/types/index.ts +++ b/backend/src/types/index.ts @@ -1,5 +1,6 @@ export type add_req = { content: string, tags?: string[], metadata?: Record, salience?: number, decay_lambda?: number } -export type q_req = { query: string, k?: number, filters?: { tags?: string[], min_score?: number, sector?: string } } +export type q_req = { query: string, k?: number, filters?: { tags?: string[], min_score?: number, sector?: string, metadata?: Record } } +export type MemoryFilters = NonNullable export type SectorType = 'episodic' | 'semantic' | 'procedural' | 'emotional' | 'reflective' export type ingest_req = { diff --git a/tests/backend/api.test.js b/tests/backend/api.test.js index 6e62d07..632b3b6 100644 --- a/tests/backend/api.test.js +++ b/tests/backend/api.test.js @@ -166,6 +166,133 @@ async function testSectorOperations() { } } +async function testQueryFilters() { + console.log('\n๐Ÿ” Testing Query Filters...'); + + let testMemoryId1, testMemoryId2; + + try { + const response1 = await makeRequest(`${BASE_URL}/memory/add`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: 'Filter test memory with special tag', + tags: ['test-filter', 'special'], + metadata: { category: 'test' }, + }), + }); + testMemoryId1 = response1.data.id; + assertEqual( + response1.status, + 200, + 'Add first filter test memory should return 200', + ); + } catch (error) { + assert(false, `Add first filter test memory failed: ${error.message}`); + } + + try { + const response2 = await makeRequest(`${BASE_URL}/memory/add`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: 'Another filter test memory', + tags: ['test-filter'], + metadata: { category: 'other' }, + }), + }); + testMemoryId2 = response2.data.id; + assertEqual( + response2.status, + 200, + 'Add second filter test memory should return 200', + ); + } catch (error) { + assert(false, `Add second filter test memory failed: ${error.message}`); + } + + try { + const response = await makeRequest(`${BASE_URL}/memory/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: 'filter test', + k: 10, + filters: { + tags: ['special'], + }, + }), + }); + + console.log(` Debug: Query with tag filter response:`, response.data); + assertEqual( + response.status, + 200, + 'Query with tag filter should return 200', + ); + assertProperty( + response.data, + 'matches', + 'Filtered query should have matches', + ); + assertArray(response.data.matches, 'Filtered matches should be an array'); + } catch (error) { + assert(false, `Query with tag filters failed: ${error.message}`); + } + + try { + const response = await makeRequest(`${BASE_URL}/memory/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: 'filter test', + k: 10, + filters: { + metadata: { category: 'test' }, + }, + }), + }); + + console.log( + ` Debug: Query with metadata filter response:`, + response.data, + ); + assertEqual( + response.status, + 200, + 'Query with metadata filter should return 200', + ); + assertProperty( + response.data, + 'matches', + 'Filtered query should have matches', + ); + assertArray(response.data.matches, 'Filtered matches should be an array'); + } catch (error) { + assert(false, `Query with metadata filters failed: ${error.message}`); + } + + if (testMemoryId1) { + try { + await makeRequest(`${BASE_URL}/memory/${testMemoryId1}`, { + method: 'DELETE', + }); + } catch (error) { + console.log(` Warning: Failed to cleanup test memory ${testMemoryId1}`); + } + } + + if (testMemoryId2) { + try { + await makeRequest(`${BASE_URL}/memory/${testMemoryId2}`, { + method: 'DELETE', + }); + } catch (error) { + console.log(` Warning: Failed to cleanup test memory ${testMemoryId2}`); + } + } +} + async function testErrorHandling() { console.log('\nโš ๏ธ Testing Error Handling...'); @@ -199,6 +326,7 @@ async function runBackendTests() { await testHealthCheck(); await testMemoryOperations(); await testSectorOperations(); + await testQueryFilters(); await testErrorHandling(); } catch (error) { console.error('โŒ Test execution failed:', error.message);