Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a36a5f1687 | |||
| c598d9cf01 | |||
| d9ef748b6f | |||
| 96d7323377 |
@@ -5,10 +5,11 @@ A plugin that integrates Ollama with Obsidian to create a chat interface that ca
|
||||
## Features
|
||||
|
||||
- Chat with Ollama models directly in Obsidian
|
||||
- Vault context search - the assistant can reference your notes
|
||||
- Tool integration - create files based on chat responses
|
||||
- Vault context search — the assistant can reference your notes
|
||||
- Tool integration — create files based on chat responses
|
||||
- Streaming responses
|
||||
- Customizable model and URL settings
|
||||
- Semantic response cache — repeated or similar queries are answered instantly without hitting the model (requires ChromaDB)
|
||||
- Customisable model, URL, and cache settings
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -17,48 +18,87 @@ A plugin that integrates Ollama with Obsidian to create a chat interface that ca
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Install Ollama**: Follow the instructions at [ollama.ai](https://ollama.ai) to install Ollama
|
||||
2. **Start Ollama service**: `ollama serve`
|
||||
3. **Pull a model**: `ollama pull llama3` (or any other model you prefer)
|
||||
### Required
|
||||
|
||||
1. **Install Ollama**: Follow the instructions at [ollama.ai](https://ollama.ai)
|
||||
2. **Start Ollama**: `ollama serve`
|
||||
3. **Pull a chat model**: `ollama pull llama3` (or any other model you prefer)
|
||||
|
||||
### Optional — Semantic Cache
|
||||
|
||||
The semantic cache stores responses in a local [ChromaDB](https://www.trychroma.com) vector database. When you ask a question that is semantically similar to one already cached, the stored answer is returned immediately instead of calling the model.
|
||||
|
||||
1. **Install ChromaDB**:
|
||||
```bash
|
||||
pip install chromadb
|
||||
```
|
||||
2. **Start ChromaDB**:
|
||||
```bash
|
||||
chroma run --host localhost --port 8000
|
||||
```
|
||||
3. **Pull an embedding model** (used to generate vectors for cache lookups):
|
||||
```bash
|
||||
ollama pull nomic-embed-text
|
||||
```
|
||||
4. Enable the cache in the plugin settings and configure the ChromaDB URL.
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Open the plugin settings via Obsidian's settings panel
|
||||
2. Configure the Ollama URL (default: `http://localhost:11434`)
|
||||
3. Configure the model name (default: `llama3`)
|
||||
4. Restart the plugin if needed
|
||||
Open **Settings → Ollama Chat** to configure the plugin.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Ollama URL | `http://localhost:11434` | Base URL of your Ollama instance |
|
||||
| Model | `llama3` | Model used for chat responses |
|
||||
| Enable Semantic Cache | Off | Cache responses for fast repeated queries |
|
||||
| ChromaDB URL | `http://localhost:8000` | URL of your running ChromaDB instance |
|
||||
| Cache Embedding Model | `nomic-embed-text` | Ollama model used to generate cache embeddings |
|
||||
| Cache Similarity Threshold | `0.85` | Minimum cosine similarity (0–1) for a cache hit — higher values require closer matches |
|
||||
| Clear Semantic Cache | — | Button to wipe all cached responses from ChromaDB |
|
||||
|
||||
## Usage
|
||||
|
||||
1. Click the ribbon icon to open the chat view
|
||||
2. Type your message in the input box
|
||||
3. Press Enter or click Send to send your message
|
||||
4. Click the "New Chat" button to start a fresh conversation
|
||||
3. Press **Enter** or click **Send** to send your message
|
||||
4. Press **Shift+Enter** to insert a line break
|
||||
5. Click **New Chat** to start a fresh conversation
|
||||
|
||||
## Semantic Cache Behaviour
|
||||
|
||||
- The cache is **bypassed** when tool calls are involved (e.g. file creation), since those requests have side effects.
|
||||
- Responses are stored against the last user message in the conversation. If a new query is sufficiently similar (above the configured threshold), the cached response is returned.
|
||||
- Re-asking the same question updates the existing cache entry rather than creating a duplicate.
|
||||
- Use the **Clear Semantic Cache** button in settings to remove all stored responses (for example after switching embedding models).
|
||||
|
||||
## Supported Models
|
||||
|
||||
Any model supported by Ollama should work, including:
|
||||
|
||||
- llama3
|
||||
- llama2
|
||||
- mistral
|
||||
- codellama
|
||||
- etc.
|
||||
- `llama3`
|
||||
- `llama2`
|
||||
- `mistral`
|
||||
- `codellama`
|
||||
- and many more — see [ollama.com/library](https://ollama.com/library)
|
||||
|
||||
## Development
|
||||
|
||||
To build from source:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Connection issues**: Ensure Ollama is running and accessible at the configured URL
|
||||
- **Model not found**: Make sure you've pulled the model (`ollama pull <modelname>`)
|
||||
- **Permission issues**: Check that your Obsidian vault has proper write permissions
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| Cannot connect to Ollama | Ollama is not running | Run `ollama serve` |
|
||||
| Model not found | Model not pulled | Run `ollama pull <model>` |
|
||||
| Semantic cache unavailable (notice shown) | ChromaDB is not running, or the ChromaDB URL is wrong | Start ChromaDB (`chroma run`) and verify the URL in settings |
|
||||
| Cache always misses | Similarity threshold is too high, or the embedding model was changed | Lower the threshold or click **Clear Semantic Cache** and let the cache rebuild |
|
||||
| Slow first response after enabling cache | Embedding model not yet pulled | Run `ollama pull nomic-embed-text` (or the model you configured) |
|
||||
| Permission issues | Vault write permissions | Check that your Obsidian vault has proper write permissions |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+23
-11
@@ -72,6 +72,15 @@ export class ChatView extends ItemView {
|
||||
undefined,
|
||||
newSettings.cacheConfig
|
||||
);
|
||||
void this.ollamaClient.initializeCache().catch(() => {
|
||||
new Notice(
|
||||
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async clearCache(): Promise<void> {
|
||||
await this.ollamaClient.clearCache();
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
@@ -83,7 +92,13 @@ export class ChatView extends ItemView {
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
await this.ollamaClient.initializeCache();
|
||||
try {
|
||||
await this.ollamaClient.initializeCache();
|
||||
} catch {
|
||||
new Notice(
|
||||
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
|
||||
);
|
||||
}
|
||||
this.render();
|
||||
this.removeEventListeners(); // Clean up any existing listeners before reattaching
|
||||
this.setupEventListeners();
|
||||
@@ -93,7 +108,7 @@ export class ChatView extends ItemView {
|
||||
this.updateSettings(newSettings);
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
onClose(): Promise<void> {
|
||||
this.ollamaClient.cancelStream();
|
||||
this.removeEventListeners();
|
||||
this.cleanupStreamingResources();
|
||||
@@ -268,7 +283,7 @@ export class ChatView extends ItemView {
|
||||
if (streamingMessage && !this.lastMessageEl) {
|
||||
this.lastMessageEl = (this.chatContainer ?? this.contentEl).createEl('div', {
|
||||
cls: `ollama-message assistant`,
|
||||
}) as HTMLElement;
|
||||
});
|
||||
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
||||
}
|
||||
if (this.lastMessageEl) {
|
||||
@@ -314,14 +329,11 @@ export class ChatView extends ItemView {
|
||||
|
||||
return [
|
||||
systemMessage,
|
||||
...this.messages.map(
|
||||
(m) =>
|
||||
({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls,
|
||||
}) as OllamaMessage
|
||||
),
|
||||
...this.messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls,
|
||||
})),
|
||||
userMessageWithContext,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ export const DEFAULT_SETTINGS = {
|
||||
similarityThreshold: 0.85,
|
||||
collectionName: 'ollama_semantic_cache',
|
||||
embeddingModel: 'nomic-embed-text',
|
||||
chromaURL: 'http://localhost:8000',
|
||||
},
|
||||
};
|
||||
|
||||
+72
-1
@@ -49,7 +49,11 @@ export default class OllamaPlugin extends Plugin {
|
||||
const data = (await this.loadData()) as Partial<PluginSettings> | null;
|
||||
if (data) {
|
||||
Logger.debug('Loading saved settings', 'settings');
|
||||
this.settings = Object.assign({}, this.settings, data);
|
||||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...data,
|
||||
cacheConfig: { ...DEFAULT_SETTINGS.cacheConfig, ...data.cacheConfig },
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
ErrorHandler.handleError(error, 'settings load');
|
||||
@@ -89,6 +93,17 @@ export default class OllamaPlugin extends Plugin {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async clearSemanticCache(): Promise<void> {
|
||||
const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view');
|
||||
for (const leaf of leaves) {
|
||||
const view = leaf.view;
|
||||
if (view instanceof ChatView) {
|
||||
await view.clearCache();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OllamaSettingTab extends PluginSettingTab {
|
||||
@@ -152,6 +167,62 @@ class OllamaSettingTab extends PluginSettingTab {
|
||||
this.plugin.notifyChatViews();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(container)
|
||||
.setName('ChromaDB URL')
|
||||
.setDesc('URL for your ChromaDB instance (default: http://localhost:8000)')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(this.plugin.settings.cacheConfig.chromaURL || 'http://localhost:8000')
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cacheConfig.chromaURL = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(container)
|
||||
.setName('Cache Embedding Model')
|
||||
.setDesc('Ollama model used to generate embeddings for the semantic cache')
|
||||
.addText((text) =>
|
||||
text.setValue(this.plugin.settings.cacheConfig.embeddingModel).onChange(async (value) => {
|
||||
this.plugin.settings.cacheConfig.embeddingModel = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.notifyChatViews();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(container)
|
||||
.setName('Cache Similarity Threshold')
|
||||
.setDesc(
|
||||
'Minimum cosine similarity (0–1) for a cache hit. Higher values require closer matches.'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(String(this.plugin.settings.cacheConfig.similarityThreshold))
|
||||
.onChange(async (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) {
|
||||
this.plugin.settings.cacheConfig.similarityThreshold = parsed;
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
new Notice('Similarity threshold must be a number between 0 and 1.');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(container)
|
||||
.setName('Clear Semantic Cache')
|
||||
.setDesc('Delete all cached responses from ChromaDB')
|
||||
.addButton((button) =>
|
||||
button.setButtonText('Clear Cache').onClick(async () => {
|
||||
try {
|
||||
await this.plugin.clearSemanticCache();
|
||||
new Notice('Semantic cache cleared.');
|
||||
} catch {
|
||||
new Notice('Failed to clear semantic cache. Is ChromaDB running?');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
|
||||
+20
-12
@@ -1,9 +1,9 @@
|
||||
// src/ollama-client.ts
|
||||
|
||||
import type { OllamaMessage, OllamaTool } from './types';
|
||||
import { ApiError } from './types';
|
||||
import { ApiError, CacheConfig } from './types';
|
||||
import { Logger } from './utils';
|
||||
import { SemanticCacheService, CacheConfig } from './semantic-cache';
|
||||
import { SemanticCacheService } from './semantic-cache';
|
||||
|
||||
interface OllamaChatResponse {
|
||||
message?: Partial<OllamaMessage>;
|
||||
@@ -33,6 +33,12 @@ export class OllamaClient {
|
||||
}
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
if (this.cacheService) {
|
||||
await this.cacheService.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
cancelStream(): void {
|
||||
if (this.currentStreamController) {
|
||||
this.currentStreamController.abort();
|
||||
@@ -50,7 +56,7 @@ export class OllamaClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
|
||||
const lastUserMsg = messages.findLast((m) => m.role === 'user');
|
||||
if (lastUserMsg && this.cacheService) {
|
||||
const cached = await this.cacheService.getCache(lastUserMsg.content);
|
||||
if (cached) {
|
||||
@@ -59,16 +65,16 @@ export class OllamaClient {
|
||||
}
|
||||
}
|
||||
|
||||
const chunks: OllamaMessage[] = [];
|
||||
for await (const chunk of this.streamChatWithRetry(messages, tools, 0)) {
|
||||
chunks.push(chunk);
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
// Populate cache in background after successful stream
|
||||
if (this.cacheService && lastUserMsg) {
|
||||
const chunks: OllamaMessage[] = [];
|
||||
for await (const chunk of this.streamChatWithRetry(messages, tools, 0)) {
|
||||
chunks.push(chunk);
|
||||
yield chunk;
|
||||
}
|
||||
const fullContent = chunks.map((c) => c.content).join('');
|
||||
void this.cacheService.setCache(lastUserMsg.content, fullContent);
|
||||
} else {
|
||||
yield* this.streamChatWithRetry(messages, tools, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +95,7 @@ export class OllamaClient {
|
||||
return this.chatWithRetry(messages, tools, 0);
|
||||
}
|
||||
|
||||
const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
|
||||
const lastUserMsg = messages.findLast((m) => m.role === 'user');
|
||||
if (lastUserMsg && this.cacheService) {
|
||||
const cached = await this.cacheService.getCache(lastUserMsg.content);
|
||||
if (cached) {
|
||||
@@ -334,7 +340,9 @@ export class OllamaClient {
|
||||
|
||||
private throwIfOllamaError(parsed: Record<string, unknown>): void {
|
||||
if (parsed.error) {
|
||||
throw new Error(`Ollama error: ${String(parsed.error)}`);
|
||||
const errorMsg =
|
||||
typeof parsed.error === 'string' ? parsed.error : JSON.stringify(parsed.error);
|
||||
throw new Error(`Ollama error: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-3
@@ -6,14 +6,17 @@ import { CacheConfig } from './types';
|
||||
|
||||
export class SemanticCacheService {
|
||||
private client: ChromaClient;
|
||||
private collection: ReturnType<ChromaClient['getOrCreateCollection']> | null = null;
|
||||
private collection: any | null = null;
|
||||
private config: CacheConfig;
|
||||
private ollamaURL: string;
|
||||
private chromaURL: string;
|
||||
|
||||
constructor(ollamaURL: string, config: CacheConfig) {
|
||||
this.ollamaURL = ollamaURL.replace(/\/+$/, '');
|
||||
this.config = config;
|
||||
this.client = new ChromaClient({ path: 'http://localhost:8000' });
|
||||
// Use configurable ChromaDB URL or default to localhost
|
||||
this.chromaURL = config.chromaURL || 'http://localhost:8000';
|
||||
this.client = new ChromaClient({ path: this.chromaURL });
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
@@ -94,8 +97,17 @@ export class SemanticCacheService {
|
||||
const embedding = await this.getEmbedding(prompt);
|
||||
if (!embedding.length) return;
|
||||
|
||||
// Fallback for crypto.randomUUID() if not available
|
||||
let id: string;
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
id = crypto.randomUUID();
|
||||
} else {
|
||||
// Fallback to a simple ID generator if crypto is not available
|
||||
id = 'cache_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
await this.collection.add({
|
||||
ids: [crypto.randomUUID()],
|
||||
ids: [id],
|
||||
embeddings: [embedding],
|
||||
metadatas: [{ fullResponse: response }],
|
||||
});
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface CacheConfig {
|
||||
similarityThreshold: number;
|
||||
collectionName: string;
|
||||
embeddingModel: string;
|
||||
chromaURL?: string;
|
||||
}
|
||||
|
||||
export interface PluginSettings {
|
||||
|
||||
@@ -32,6 +32,13 @@ const mockSettings: PluginSettings = {
|
||||
vaultSearchLimit: 3,
|
||||
maxMessageHistory: 50,
|
||||
lastIndexTime: 0,
|
||||
cacheConfig: {
|
||||
enabled: false,
|
||||
similarityThreshold: 0.9,
|
||||
collectionName: 'test-cache',
|
||||
embeddingModel: 'nomic-embed-text',
|
||||
chromaURL: 'http://localhost:8000',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ChatView', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OllamaMessage, OllamaTool, CacheConfig } from '../src/types';
|
||||
const mockInitialize = jest.fn().mockResolvedValue(undefined);
|
||||
const mockGetCache = jest.fn().mockResolvedValue(null);
|
||||
const mockSetCache = jest.fn().mockResolvedValue(undefined);
|
||||
const mockClearCache = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
// Mock the semantic cache service BEFORE importing OllamaClient
|
||||
jest.mock('../src/semantic-cache', () => ({
|
||||
@@ -13,6 +14,7 @@ jest.mock('../src/semantic-cache', () => ({
|
||||
initialize: mockInitialize,
|
||||
getCache: mockGetCache,
|
||||
setCache: mockSetCache,
|
||||
clearCache: mockClearCache,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -68,6 +70,7 @@ describe('OllamaClient with Semantic Cache', () => {
|
||||
similarityThreshold: 0.85,
|
||||
collectionName: 'test_cache',
|
||||
embeddingModel: 'nomic-embed-text',
|
||||
chromaURL: 'http://localhost:8000',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -101,6 +104,7 @@ describe('OllamaClient with Semantic Cache', () => {
|
||||
});
|
||||
|
||||
it('should not create cache service when no config provided', () => {
|
||||
jest.clearAllMocks(); // Reset the call recorded by beforeEach before checking
|
||||
new OllamaClient('http://localhost:11434', 'llama3', mockFetch);
|
||||
|
||||
expect(SemanticCacheService).not.toHaveBeenCalled();
|
||||
@@ -350,19 +354,8 @@ describe('OllamaClient with Semantic Cache', () => {
|
||||
// Mock cache service to throw an error
|
||||
mockGetCache.mockRejectedValueOnce(new Error('Cache error'));
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
message: {
|
||||
content: 'LLM response after cache failure',
|
||||
},
|
||||
}),
|
||||
};
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// The chat method does not handle cache errors, so it should propagate
|
||||
// However, the client should still be usable
|
||||
// Note: fetch is never reached because the cache throws first.
|
||||
// The chat method does not handle cache errors, so it should propagate.
|
||||
await expect(client.chat(mockMessages)).rejects.toThrow('Cache error');
|
||||
});
|
||||
|
||||
@@ -414,6 +407,18 @@ describe('OllamaClient with Semantic Cache', () => {
|
||||
expect(mockGetCache).toHaveBeenCalledWith('Second question');
|
||||
});
|
||||
|
||||
it('should call clearCache on the cache service', async () => {
|
||||
await client.clearCache();
|
||||
expect(mockClearCache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not throw when clearCache is called without a cache service', async () => {
|
||||
const noCacheClient = new OllamaClient('http://localhost:11434', 'llama3', mockFetch);
|
||||
jest.clearAllMocks();
|
||||
await expect(noCacheClient.clearCache()).resolves.toBeUndefined();
|
||||
expect(mockClearCache).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip cache when no user message found', async () => {
|
||||
const onlyAssistantMessages: OllamaMessage[] = [
|
||||
{ role: 'system', content: 'You are helpful.' },
|
||||
|
||||
@@ -7,12 +7,20 @@ import { CacheConfig } from '../src/types';
|
||||
jest.mock('chromadb', () => ({
|
||||
ChromaClient: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getOrCreateCollection: jest.fn().mockResolvedValue({
|
||||
getOrCreateCollection: jest.fn().mockReturnValue({
|
||||
query: jest.fn(),
|
||||
add: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
}),
|
||||
deleteCollection: jest.fn(),
|
||||
};
|
||||
}),
|
||||
IncludeEnum: {
|
||||
Documents: 'documents',
|
||||
Embeddings: 'embeddings',
|
||||
Metadatas: 'metadatas',
|
||||
Distances: 'distances',
|
||||
},
|
||||
}));
|
||||
|
||||
// Now import SemanticCacheService after mocking
|
||||
@@ -21,10 +29,12 @@ import { SemanticCacheService } from '../src/semantic-cache';
|
||||
jest.spyOn(global, 'fetch').mockImplementation(jest.fn());
|
||||
|
||||
const mockChromaClient = {
|
||||
getOrCreateCollection: jest.fn().mockResolvedValue({
|
||||
getOrCreateCollection: jest.fn().mockReturnValue({
|
||||
query: jest.fn(),
|
||||
add: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
}),
|
||||
deleteCollection: jest.fn(),
|
||||
};
|
||||
|
||||
// Set up mock instance
|
||||
@@ -44,6 +54,7 @@ describe('SemanticCacheService', () => {
|
||||
similarityThreshold: 0.85,
|
||||
collectionName: 'test_cache',
|
||||
embeddingModel: 'nomic-embed-text',
|
||||
chromaURL: 'http://localhost:8000',
|
||||
};
|
||||
|
||||
service = new SemanticCacheService('http://localhost:11434', config);
|
||||
@@ -211,32 +222,30 @@ describe('SemanticCacheService', () => {
|
||||
json: () => Promise.resolve({ embedding: [0.1, 0.2, 0.3] }),
|
||||
});
|
||||
|
||||
// crypto.randomUUID mock
|
||||
const mockUuid = 'mock-uuid-123' as any;
|
||||
jest.spyOn(crypto, 'randomUUID').mockReturnValue(mockUuid);
|
||||
|
||||
await service.setCache('test prompt', 'test response');
|
||||
|
||||
const mockCollection = mockChromaClient.getOrCreateCollection();
|
||||
expect(mockCollection.add).toHaveBeenCalledWith({
|
||||
ids: [mockUuid],
|
||||
embeddings: [[0.1, 0.2, 0.3]],
|
||||
metadatas: [{ fullResponse: 'test response' }],
|
||||
});
|
||||
expect(mockCollection.upsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ids: [expect.any(String)],
|
||||
embeddings: [[0.1, 0.2, 0.3]],
|
||||
metadatas: [{ fullResponse: 'test response' }],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add entry when prompt is empty', async () => {
|
||||
await service.setCache(' ', 'test response');
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(mockChromaClient.getOrCreateCollection().add).not.toHaveBeenCalled();
|
||||
expect(mockChromaClient.getOrCreateCollection().upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add entry when response is empty', async () => {
|
||||
await service.setCache('test prompt', ' ');
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(mockChromaClient.getOrCreateCollection().add).not.toHaveBeenCalled();
|
||||
expect(mockChromaClient.getOrCreateCollection().upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add entry when cache is disabled', async () => {
|
||||
@@ -255,13 +264,29 @@ describe('SemanticCacheService', () => {
|
||||
json: () => Promise.resolve({ embedding: [0.1, 0.2, 0.3] }),
|
||||
});
|
||||
|
||||
const mockUuid = 'mock-uuid-456' as any;
|
||||
jest.spyOn(crypto, 'randomUUID').mockReturnValue(mockUuid);
|
||||
|
||||
mockChromaClient.getOrCreateCollection().add.mockRejectedValueOnce(new Error('Add failed'));
|
||||
mockChromaClient
|
||||
.getOrCreateCollection()
|
||||
.upsert.mockRejectedValueOnce(new Error('Add failed'));
|
||||
|
||||
// Should not throw
|
||||
await expect(service.setCache('test prompt', 'test response')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await service.initialize();
|
||||
mockChromaClient.deleteCollection.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should delete the collection and re-initialize', async () => {
|
||||
|
||||
expect(mockChromaClient.deleteCollection).toHaveBeenCalledWith({ name: 'test_cache' });
|
||||
expect(mockChromaClient.getOrCreateCollection).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should propagate errors from deleteCollection', async () => {
|
||||
mockChromaClient.deleteCollection.mockRejectedValueOnce(new Error('Delete failed'));
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user