Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down Expand Up @@ -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!"
@echo "✅ Full check complete - ready to commit!"
47 changes: 45 additions & 2 deletions backend/src/hsg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,50 @@ export async function pruneWeakWaypoints(): Promise<number> {
}
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<HSGQueryResult[]> {
const queryClassification = classifyContent(queryText)
const candidateSectors = [queryClassification.primary, ...queryClassification.additional]
Expand Down Expand Up @@ -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)) {
Expand Down
9 changes: 5 additions & 4 deletions backend/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type add_req = { content: string, tags?: string[], metadata?: Record<string, unknown>, 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<string, any> } }
export type MemoryFilters = NonNullable<q_req['filters']>
export type SectorType = 'episodic' | 'semantic' | 'procedural' | 'emotional' | 'reflective'

export type ingest_req = {
Expand Down
128 changes: 128 additions & 0 deletions tests/backend/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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...');

Expand Down Expand Up @@ -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);
Expand Down