├── .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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------