feat: add heartbeat monitor and vector memory search (Tier 2)

Heartbeat:
- HeartbeatMonitor with 5 checks: gateway, model, channels, memory, disk
- Configurable interval, failure threshold, notification channel
- Recovery notifications when health restores
- 25 new tests

Vector Memory Search:
- EmbeddingProvider interface with OpenAI, Gemini, Ollama, LlamaCpp backends
- SQLite-backed VectorStore with cosine similarity search
- Text chunker with paragraph-aware splitting and overlap
- HybridSearch merging keyword + vector results with configurable weight
- Background indexer with dirty-namespace tracking
- Graceful fallback to keyword search when embeddings unavailable
- 51 new tests

Config: automation.heartbeat + memory.embedding schema sections
Total: 950 tests passing, all types clean
This commit is contained in:
William Valentin
2026-02-07 14:45:11 -08:00
parent b50c140d25
commit 88731a50e3
17 changed files with 2354 additions and 7 deletions
+159
View File
@@ -0,0 +1,159 @@
import { describe, it, expect } from 'vitest';
import {
createEmbeddingProvider,
OpenAIEmbeddingProvider,
GeminiEmbeddingProvider,
OllamaEmbeddingProvider,
LlamaCppEmbeddingProvider,
} from './embeddings.js';
import type { EmbeddingConfig } from '../config/schema.js';
describe('createEmbeddingProvider', () => {
const baseConfig: EmbeddingConfig = {
enabled: true,
provider: 'openai',
model: 'text-embedding-3-small',
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
it('creates OpenAI provider', () => {
const provider = createEmbeddingProvider({ ...baseConfig, provider: 'openai' });
expect(provider).toBeInstanceOf(OpenAIEmbeddingProvider);
});
it('creates Gemini provider', () => {
const provider = createEmbeddingProvider({ ...baseConfig, provider: 'gemini' });
expect(provider).toBeInstanceOf(GeminiEmbeddingProvider);
});
it('creates Ollama provider', () => {
const provider = createEmbeddingProvider({ ...baseConfig, provider: 'ollama' });
expect(provider).toBeInstanceOf(OllamaEmbeddingProvider);
});
it('creates LlamaCpp provider', () => {
const provider = createEmbeddingProvider({ ...baseConfig, provider: 'llamacpp' });
expect(provider).toBeInstanceOf(LlamaCppEmbeddingProvider);
});
it('throws on unknown provider', () => {
expect(() => createEmbeddingProvider({ ...baseConfig, provider: 'unknown' as never })).toThrow('Unknown embedding provider');
});
});
describe('OpenAIEmbeddingProvider', () => {
it('reports configured dimensions', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'openai',
model: 'text-embedding-3-small',
dimensions: 512,
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
const provider = new OpenAIEmbeddingProvider(config);
expect(provider.dimensions).toBe(512);
});
it('defaults to 1536 dimensions', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'openai',
model: 'text-embedding-3-small',
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
const provider = new OpenAIEmbeddingProvider(config);
expect(provider.dimensions).toBe(1536);
});
});
describe('GeminiEmbeddingProvider', () => {
it('reports configured dimensions', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'gemini',
model: 'text-embedding-004',
dimensions: 256,
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
const provider = new GeminiEmbeddingProvider(config);
expect(provider.dimensions).toBe(256);
});
it('defaults to 768 dimensions', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'gemini',
model: 'text-embedding-004',
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
const provider = new GeminiEmbeddingProvider(config);
expect(provider.dimensions).toBe(768);
});
});
describe('OllamaEmbeddingProvider', () => {
it('reports configured dimensions', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'ollama',
model: 'nomic-embed-text',
dimensions: 384,
endpoint: 'http://localhost:11434',
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
const provider = new OllamaEmbeddingProvider(config);
expect(provider.dimensions).toBe(384);
});
});
describe('LlamaCppEmbeddingProvider', () => {
it('reports configured dimensions', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'llamacpp',
model: 'unused',
dimensions: 768,
endpoint: 'http://localhost:8080',
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
const provider = new LlamaCppEmbeddingProvider(config);
expect(provider.dimensions).toBe(768);
});
it('defaults endpoint to localhost:8080', () => {
const config: EmbeddingConfig = {
enabled: true,
provider: 'llamacpp',
model: 'unused',
dimensions: 768,
chunk_size: 512,
chunk_overlap: 50,
top_k: 5,
hybrid_weight: 0.7,
};
// Provider should be constructable without endpoint
const provider = new LlamaCppEmbeddingProvider(config);
expect(provider.dimensions).toBe(768);
});
});