1ccd637149
- src/semantic-cache.ts: Add robust URL sanitization in initialize(). Trim whitespace and reject malformed URLs (e.g. empty host like 'http://:8666') before passing to ChromaClient. - src/main.ts: Validate ChromaDB URL in the settings tab onChange. Fall back to 'http://localhost:8000' if the value is empty or lacks '://'. - docker-compose.yml: Add CHROMA_SERVER_CORS_ALLOW_ORIGINS=["*"] to allow cross-origin requests from Obsidian's app://obsidian.md origin.
250 lines
8.0 KiB
TypeScript
Executable File
250 lines
8.0 KiB
TypeScript
Executable File
import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab } from 'obsidian';
|
||
import { ChatView } from './chat-view';
|
||
import { DEFAULT_SETTINGS } from './constants';
|
||
import { SemanticCacheService } from './semantic-cache';
|
||
import { PluginSettings } from './types';
|
||
|
||
export default class OllamaPlugin extends Plugin {
|
||
settings: PluginSettings = DEFAULT_SETTINGS;
|
||
semanticCache?: SemanticCacheService;
|
||
|
||
async onload() {
|
||
await this.loadSettings();
|
||
|
||
// Register the chat view
|
||
this.registerView(
|
||
'ollama-chat-view',
|
||
(leaf: WorkspaceLeaf) => new ChatView(leaf, this.settings)
|
||
);
|
||
|
||
// Add a ribbon icon in the left sidebar
|
||
this.addRibbonIcon('bot', 'Open Ollama Chat', async () => {
|
||
await this.activateChatView();
|
||
});
|
||
|
||
// Add a command to open the chat view
|
||
this.addCommand({
|
||
id: 'open-ollama-chat',
|
||
name: 'Open Ollama Chat',
|
||
callback: async () => {
|
||
await this.activateChatView();
|
||
},
|
||
});
|
||
|
||
// Add a command to clear the semantic cache
|
||
this.addCommand({
|
||
id: 'clear-semantic-cache',
|
||
name: 'Clear Semantic Cache',
|
||
callback: async () => {
|
||
await this.clearSemanticCache();
|
||
new Notice('Semantic cache cleared.');
|
||
},
|
||
});
|
||
|
||
// Add a settings tab
|
||
this.addSettingTab(new OllamaSettingTab(this.app, this));
|
||
|
||
// Initialize the semantic cache
|
||
if (this.settings.cacheConfig) {
|
||
this.semanticCache = new SemanticCacheService(
|
||
this.settings.ollamaUrl,
|
||
this.settings.cacheConfig
|
||
);
|
||
try {
|
||
await this.semanticCache.initialize();
|
||
} catch {
|
||
new Notice('Semantic cache initialization failed. Check console for details.');
|
||
}
|
||
}
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||
onunload() {
|
||
// Clean up any active semantic cache resources on plugin unload
|
||
// Using fire-and-forget pattern since onunload cannot be async per Obsidian API
|
||
if (this.semanticCache) {
|
||
void this.semanticCache.clearCache();
|
||
}
|
||
// No explicit unregisterView needed; relying on Obsidian lifecycle management.
|
||
}
|
||
|
||
async loadSettings() {
|
||
const loadedSettings = ((await this.loadData()) ?? {}) as Partial<PluginSettings>;
|
||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings);
|
||
}
|
||
|
||
async saveSettings() {
|
||
await this.saveData(this.settings);
|
||
}
|
||
|
||
async activateChatView() {
|
||
const existing = this.app.workspace.getLeavesOfType('ollama-chat-view');
|
||
if (existing.length > 0) {
|
||
await this.app.workspace.revealLeaf(existing[0]);
|
||
} else {
|
||
const leaf = this.app.workspace.getRightLeaf(false);
|
||
if (leaf) {
|
||
await leaf.setViewState({
|
||
type: 'ollama-chat-view',
|
||
active: true,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
async clearSemanticCache() {
|
||
if (this.semanticCache) {
|
||
await this.semanticCache.clearCache();
|
||
}
|
||
}
|
||
|
||
notifyChatViews() {
|
||
const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view');
|
||
leaves.forEach((leaf) => {
|
||
if (leaf.view instanceof ChatView) {
|
||
leaf.view.updateSettings(this.settings);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
class OllamaSettingTab extends PluginSettingTab {
|
||
plugin: OllamaPlugin;
|
||
|
||
constructor(app: App, plugin: OllamaPlugin) {
|
||
super(app, plugin);
|
||
this.plugin = plugin;
|
||
}
|
||
|
||
display() {
|
||
const { containerEl } = this;
|
||
containerEl.empty();
|
||
containerEl.createEl('h2', { text: 'Ollama Settings' });
|
||
|
||
new Setting(containerEl)
|
||
.setName('Ollama URL')
|
||
.setDesc('URL for your Ollama instance (default: http://localhost:11434)')
|
||
.addText((text) =>
|
||
text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => {
|
||
this.plugin.settings.ollamaUrl = value;
|
||
await this.plugin.saveSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Model')
|
||
.setDesc('Ollama model to use (default: llama3)')
|
||
.addText((text) =>
|
||
text.setValue(this.plugin.settings.model).onChange(async (value) => {
|
||
this.plugin.settings.model = value;
|
||
await this.plugin.saveSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Vault Search Limit')
|
||
.setDesc('Maximum number of vault entries to include in context (default: 3)')
|
||
.addText((text) =>
|
||
text.setValue(String(this.plugin.settings.vaultSearchLimit)).onChange(async (value) => {
|
||
const parsed = parseInt(value);
|
||
if (!isNaN(parsed) && parsed > 0) {
|
||
this.plugin.settings.vaultSearchLimit = parsed;
|
||
await this.plugin.saveSettings();
|
||
} else {
|
||
new Notice('Vault search limit must be a positive integer.');
|
||
}
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Max Message History')
|
||
.setDesc('Maximum number of messages to keep in conversation history (default: 50)')
|
||
.addText((text) =>
|
||
text.setValue(String(this.plugin.settings.maxMessageHistory)).onChange(async (value) => {
|
||
const parsed = parseInt(value);
|
||
if (!isNaN(parsed) && parsed > 0) {
|
||
this.plugin.settings.maxMessageHistory = parsed;
|
||
await this.plugin.saveSettings();
|
||
} else {
|
||
new Notice('Max message history must be a positive integer.');
|
||
}
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Enable Semantic Cache')
|
||
.setDesc('Use semantic cache to store and retrieve previous responses')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.cacheConfig.enabled).onChange(async (value) => {
|
||
this.plugin.settings.cacheConfig.enabled = value;
|
||
await this.plugin.saveSettings();
|
||
this.plugin.notifyChatViews();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.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) => {
|
||
const trimmed = value.trim();
|
||
// Ensure a valid-looking URL; fall back to default if empty or malformed
|
||
this.plugin.settings.cacheConfig.chromaURL =
|
||
trimmed && trimmed.includes('://') ? trimmed : 'http://localhost:8000';
|
||
await this.plugin.saveSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.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(containerEl)
|
||
.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(containerEl)
|
||
.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() {
|
||
// Clear the container to prevent duplicate elements
|
||
this.containerEl.empty();
|
||
}
|
||
}
|