├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── brain-icon.svg ├── esbuild.config.mjs ├── main.js ├── main.ts ├── manifest.json ├── package.json ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luis Sobrecueva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧠 Obsidian brAIn plugin 2 | 3 | This plugin enables a ChatGPT powered chatbot specifically focused on question answering over your Obsidian vault notes(markdown files). 4 | 5 | 6 | https://user-images.githubusercontent.com/480507/236815106-2c38d137-9345-498e-8531-46494239d19a.mp4 7 | 8 | 9 | ### 🧰 Requirements 10 | 11 | - [Docker](https://docs.docker.com/get-docker/) 12 | 13 | ### 📖 How to use 14 | 15 | 1. Install the plugin in your Obsidian vault by going to `Settings -> Community Plugins -> Browse` and searching for `brAIn`. 16 | 2. After installation, In the settings section of the plugin, enter your OpenAI API key to enable the plugin to ingest your vault documents and to use the chat. 17 | 3. Feed the model with your notes your vault notes running `brAIn: Ingest vault docs` command in the command palette or by pressing `Ctrl/Cmd + P`. 18 | 4. From now on you can also talk with your brAIn by clicking on the brain button in the ribbon or running `brAIn: Open chat` command in the command palette or by pressing `Ctrl/Cmd + P`. 19 | 20 | *If you have new notes that you want to be indexed by the brAIn just run again `brAIn: Ingest vault docs`* 21 | 22 | ### 🛠 How it works 23 | 24 | The plugin uses a [brAIn docker container](https://hub.docker.com/repository/docker/lusob04/brain) running the [brAIn](https://github.com/lusob/brAIn) server to enable the chatbot functionality. The brAIn server reads in your Obsidian vault documents and uses the OpenAI GPT-3 API to enable the question answering functionality. Once the server is running, you can chat with the bot through the plugin's interface. 25 | 26 | ### 💬 Support 27 | 28 | If you have any issues or feature requests, please open an issue on [GitHub](https://github.com/). 29 | 30 | ### 📓 License 31 | 32 | This plugin is licensed under the [MIT License](https://github.com/lusob/obsidian-brain/blob/main/LICENSE). 33 | 34 | ### © Privacy 35 | 36 | By defauls the embeddings as generated locally so your docs are not leaving your machine, in case you check the OpenAI Embeddings check in settings, The OpenAI API is used by brAIn to generate the vector store during ingestion and to answer each question asked in the chat, but its [API usage policies](https://openai.com/policies/api-data-usage-policies ) guarantee to us that OpenAI will not use data submitted by customers via its API to train or improve their models and any data submitted via API will be deleted after 30 days. 37 | 38 | ### ⚠️ Limitations 39 | 40 | If you are generating the embeddings locally it could consume a lot of time and hardware resources (depending of your marchines and the number of documents) 41 | In case you want to generate the embeddings in OpenAI during the ingestion (embedding generation) a big amount of notes in your vault can lead to high expenses (~ 1000 notes = 1$), monitor your account and set API key limits to avoid scares 42 | 43 | ### 🐞 Known Issues 44 | 45 | brAIn may occasionally generate incorrect or irrelevant content based on the user's input. Additionally, it may encounter errors when calling the OpenAI API if the API key is invalid or if there are issues with the OpenAI API service. 46 | 47 | ### ⏭ Next 48 | 49 | The final idea is to create a complete offline chatbot using an LLM, but so far the hardware requirements for these models are too high. -------------------------------------------------------------------------------- /brain-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | artificial intelligence -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import builtins from "builtin-modules"; 2 | import esbuild from "esbuild"; 3 | import process from "process"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2020", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { Docker, Options } from 'docker-cli-js'; 3 | import { App, Modal, Notice, Plugin, PluginSettingTab, Setting, addIcon, requestUrl } from 'obsidian'; 4 | interface BrainSettings { 5 | openaiApiKey: string; 6 | useOpenAIEmbeddings: boolean; // Add this line 7 | } 8 | 9 | const DEFAULT_SETTINGS: BrainSettings = { 10 | openaiApiKey: '', 11 | useOpenAIEmbeddings: false // Add this line 12 | }; 13 | 14 | export default class Brain extends Plugin { 15 | settings: BrainSettings; 16 | loadingModal: Modal; 17 | 18 | async openBrain() { 19 | // Create a new Modal instance 20 | const modal = new Modal(this.app); 21 | 22 | // Set the title of the Modal 23 | modal.titleEl.setText('brAIn'); 24 | 25 | // Create a loading modal 26 | this.loadingModal = new Modal(this.app); 27 | this.loadingModal.titleEl.setText('Loading...'); 28 | this.loadingModal.contentEl.setText('Please wait while the docker is being loaded, the first time could take several minutes...'); 29 | 30 | // Add a loading bar animation to the loading modal 31 | const progressEl = this.loadingModal.contentEl.createDiv('progress'); 32 | progressEl.createDiv('bar'); 33 | 34 | // Show the loading modal 35 | this.loadingModal.open(); 36 | 37 | try { 38 | // Wait for the docker to be loaded 39 | await this.runBrainServer(this.settings.openaiApiKey); 40 | } catch (err) { 41 | // Remove the loading modal and show an error message if the docker container fails to start 42 | console.log('Failed to start brAIn: ' + err.message); 43 | } 44 | // Check if the brAIn web interface is available 45 | let isAvailable = false; 46 | let timeout = 10000; // 20-second timeout 47 | const interval = 2000; // Check every 2sec 48 | while (timeout > 0 && !isAvailable) { 49 | try { 50 | const response = await requestUrl({ 51 | url: 'http://localhost:9000' 52 | }); 53 | isAvailable = response.status == 200; 54 | if (!isAvailable) { 55 | await new Promise(resolve => setTimeout(resolve, interval)); 56 | } 57 | } catch (err) { 58 | // Remove the loading modal and show an error message if the docker container fails to start 59 | console.log('Error connecting container: ' + err.message); 60 | await new Promise(resolve => setTimeout(resolve, interval)); 61 | } 62 | timeout -= interval; 63 | } 64 | 65 | if (isAvailable) { 66 | // Remove the loading modal 67 | this.loadingModal.close(); 68 | // Create a new iframe element 69 | // Set the source of the iframe to your brAIn view 70 | modal.contentEl.createEl('iframe', { 71 | attr: { 72 | type: 'text/html', 73 | src: 'http://localhost:9000', 74 | width: '550', 75 | height: '750', 76 | allowFullscreen: 'true' 77 | } 78 | }); 79 | 80 | // Open the Modal 81 | modal.open(); 82 | } else { 83 | // Show an error message if the brAIn web interface is not available 84 | new Notice('brAIn is not running, check that you have run the ingest command first and also that no other services are using port 9000'); 85 | } 86 | this.loadingModal.close(); 87 | 88 | } 89 | 90 | async ingestDocs() { 91 | // Create a new Modal instance 92 | const modal = new Modal(this.app); 93 | 94 | // Set the title of the Modal 95 | modal.titleEl.setText('brAIn'); 96 | 97 | // Create a loading modal 98 | this.loadingModal = new Modal(this.app); 99 | this.loadingModal.titleEl.setText('Ingesting docs...'); 100 | this.loadingModal.contentEl.setText('Please wait while the docker is ingesting the docs of your vault, the fisrt time could take several minutes...'); 101 | 102 | // Add a loading bar animation to the loading modal 103 | const progressEl = this.loadingModal.contentEl.createDiv('progress'); 104 | progressEl.createDiv('bar'); 105 | 106 | // Show the loading modal 107 | this.loadingModal.open(); 108 | 109 | try { 110 | // Wait for the docker to be loaded 111 | await this.runIngestDocs(this.settings.openaiApiKey); 112 | } catch (err) { 113 | // Remove the loading modal and show an error message if the docker container fails to start 114 | console.log('Failed to ingest vault docs: ' + err.message); 115 | } 116 | this.loadingModal.close(); 117 | } 118 | 119 | async onload() { 120 | await this.loadSettings(); 121 | 122 | // This creates an icon in the left ribbon. 123 | addIcon("brAIn", ` `); 124 | 125 | const ribbonIconEl = this.addRibbonIcon('brAIn', 'brAIn', async (evt: MouseEvent) => { 126 | // Called when the user clicks the icon. 127 | try { 128 | this.openBrain(); 129 | } catch (err) { 130 | new Notice('Failed to open brAIn: ' + err.message); 131 | } 132 | }); 133 | // Perform additional things with the ribbon 134 | ribbonIconEl.addClass('my-plugin-ribbon-class'); 135 | 136 | this.addCommand({ 137 | id: 'open-brain', 138 | name: 'Open chat', 139 | callback: () => this.openBrain() 140 | }); 141 | 142 | this.addCommand({ 143 | id: 'ingest-docs', 144 | name: 'Ingest vault docs', 145 | callback: () => this.ingestDocs() 146 | }); 147 | 148 | // This adds a settings tab so the user can configure various aspects of the plugin 149 | this.addSettingTab(new BrainSettingTab(this.app, this)); 150 | 151 | // When registering intervals, this function will automatically clear the interval when the plugin is disabled. 152 | this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); 153 | 154 | try { 155 | // Check if Docker is installed by running "docker --version" command 156 | execSync('docker --version', { stdio: 'pipe', env: { PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' }}); 157 | } catch (error) { 158 | // Docker is not installed, show a dialog 159 | console.error('Docker is not installed in the system. Error: ' + error); 160 | // You can use a dialog library or show an alert using the browser's window object 161 | new Notice('Docker is not installed in the system, brAIn plugin need docker to run.'); 162 | } 163 | if (!this.settings.openaiApiKey) { 164 | // this.settings.openaiApiKey is empty or undefined 165 | new Notice('OpenAI api key not set. You need to add an OpenAI api key brAIn settings.'); 166 | } 167 | } 168 | 169 | onunload() { } 170 | 171 | async loadSettings() { 172 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 173 | } 174 | 175 | async saveSettings() { 176 | await this.saveData(this.settings); 177 | } 178 | 179 | 180 | async runBrainServer(openaiApiKey: string) { 181 | // Use Docker-CLI-JS to build the Dockerfile 182 | const options = new Options( 183 | /* machineName */ undefined, 184 | /* currentWorkingDirectory */ undefined, 185 | /* echo*/ true, 186 | {PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'} 187 | ); 188 | 189 | const docker = new Docker(options); 190 | const vaultPath = (this.app.vault.adapter as any).basePath 191 | console.log("Running brain container...") 192 | try { 193 | await docker.command(`rm brain`) 194 | } catch (err) { 195 | console.log('Failed removing container: ' + err.message); 196 | } 197 | try { 198 | let output = await docker.command(`run -d --name brain -p 9000:9000 -v ${vaultPath}:${vaultPath} -e MARKDOWN_FILES=${vaultPath} -e OPENAI_API_KEY=${openaiApiKey} -e IS_OBSIDIAN_VAULT=1 -t lusob04/brain`); 199 | console.log('Run brain output: ' + JSON.stringify(output)) 200 | } catch (err) { 201 | // Handle error during creation process 202 | console.log('Failed to start brAIn: ' + err.message); 203 | } 204 | 205 | } 206 | 207 | async runIngestDocs(openaiApiKey: string) { 208 | // Use Docker-CLI-JS to build the Dockerfile 209 | const options = new Options( 210 | /* machineName */ undefined, 211 | /* currentWorkingDirectory */ undefined, 212 | /* echo*/ true, 213 | {PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'} 214 | ); 215 | 216 | const docker = new Docker(options); 217 | const vaultPath = (this.app.vault.adapter as any).basePath 218 | 219 | try { 220 | await docker.command(`stop brain`) 221 | } catch (err) { 222 | console.log('Failed stopping container: ' + err.message); 223 | } 224 | try { 225 | await docker.command(`rm brain`) 226 | } catch (err) { 227 | console.log('Failed removing container: ' + err.message); 228 | } 229 | try { 230 | console.log('Ingesting docs...') 231 | let command = this.settings.useOpenAIEmbeddings ? 'make ingest-openai' : 'make ingest'; 232 | let output = await docker.command(`run --rm --name brain -v ${vaultPath}:${vaultPath} -e MARKDOWN_FILES=${vaultPath} -e OPENAI_API_KEY=${openaiApiKey} -t lusob04/brain ${command}`); 233 | console.log('Ingest output: ' + JSON.stringify(output)) 234 | } catch (err) { 235 | console.log('Failed ingesting: ' + err.message); 236 | } 237 | } 238 | } 239 | 240 | class BrainSettingTab extends PluginSettingTab { 241 | plugin: Brain; 242 | 243 | constructor(app: App, plugin: Brain) { 244 | super(app, plugin); 245 | this.plugin = plugin; 246 | } 247 | 248 | display(): void { 249 | const { containerEl } = this; 250 | 251 | containerEl.empty(); 252 | 253 | new Setting(containerEl) 254 | .setName('OpenAI api key') 255 | .setDesc('Set here your own OpenAI api key') 256 | .addText((text) => 257 | text 258 | .setPlaceholder('') 259 | .setValue(this.plugin.settings.openaiApiKey) 260 | .onChange(async (value) => { 261 | this.plugin.settings.openaiApiKey = value; 262 | await this.plugin.saveSettings(); 263 | })); 264 | 265 | new Setting(containerEl) 266 | .setName('Use OpenAI Embeddings') 267 | .setDesc('Check this if you want to use OpenAI embeddings') 268 | .addToggle((toggle) => 269 | toggle 270 | .setValue(this.plugin.settings.useOpenAIEmbeddings) 271 | .onChange(async (value) => { 272 | this.plugin.settings.useOpenAIEmbeddings = value; 273 | await this.plugin.saveSettings(); 274 | })); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "brain", 3 | "name": "brAIn", 4 | "version": "1.0.3", 5 | "minAppVersion": "0.15.0", 6 | "description": "This is a brAIn for Obsidian. This plugin implements a ChatGPT retrieval for your obsidian notes.", 7 | "author": "Luis Sobrecueva", 8 | "authorUrl": "https://github.com/lusob", 9 | "fundingUrl": "https://bmc.link/lusob", 10 | "isDesktopOnly": true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-brain", 3 | "version": "1.0.3", 4 | "description": "This is a brAIn for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "builtin-modules": "3.3.0", 19 | "esbuild": "0.17.3", 20 | "obsidian": "latest", 21 | "tslib": "2.4.0", 22 | "typescript": "4.7.4" 23 | }, 24 | "dependencies": { 25 | "docker-cli-js": "^2.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This CSS file will be included with your plugin, and 4 | available in the app when your plugin is enabled. 5 | 6 | If your plugin does not need CSS, delete this file. 7 | 8 | */ 9 | .progress { 10 | width: 100%; 11 | height: 4px; 12 | margin-top: 10px; 13 | border-radius: 4px; 14 | overflow: hidden; 15 | background-color: #f0f0f0; 16 | } 17 | 18 | .bar { 19 | height: 100%; 20 | width: 0%; 21 | border-radius: 4px; 22 | background-color: #4caf50; 23 | animation: progress 2s ease-in-out infinite; 24 | } 25 | 26 | @keyframes progress { 27 | 0% { 28 | width: 0%; 29 | } 30 | 50% { 31 | width: 50%; 32 | } 33 | 100% { 34 | width: 100%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": ".", 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "module": "ESNext", 9 | "target": "ES6", 10 | "allowJs": true, 11 | "noImplicitAny": true, 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "isolatedModules": true, 15 | "strictNullChecks": true, 16 | "lib": [ 17 | "DOM", 18 | "ES5", 19 | "ES6", 20 | "ES7" 21 | ] 22 | }, 23 | "include": [ 24 | "**/*.ts" 25 | ] 26 | } -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.3": "0.15.0" 6 | } --------------------------------------------------------------------------------