feat: chat with documents plugin (#417)
* chore: vectordb driver plugin # Conflicts: # plugins/monitoring-plugin/package.json * chore: add langchain & index documents * feat: chat with documents plugin * chore: correct build step --------- Co-authored-by: namvuong <22463238+vuonghoainam@users.noreply.github.com>
This commit is contained in:
parent
0f5ef6473b
commit
d9e32dcc02
@ -47,15 +47,6 @@ function updateFinishedDownloadAt(_id: string): Promise<any> {
|
|||||||
return store.updateMany("models", { _id }, { time: Date.now(), finishDownloadAt: 1 });
|
return store.updateMany("models", { _id }, { time: Date.now(), finishDownloadAt: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves all unfinished models from the database.
|
|
||||||
*
|
|
||||||
* @returns A promise that resolves with an array of unfinished models.
|
|
||||||
*/
|
|
||||||
function getUnfinishedDownloadModels(): Promise<any> {
|
|
||||||
return store.findMany("models", { finishDownloadAt: -1 }, [{ startDownloadAt: "desc" }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all finished models from the database.
|
* Retrieves all finished models from the database.
|
||||||
*
|
*
|
||||||
|
|||||||
2
plugins/retrieval-plugin/@types/global.d.ts
vendored
Normal file
2
plugins/retrieval-plugin/@types/global.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
declare const PLUGIN_NAME: string;
|
||||||
|
declare const MODULE_PATH: string;
|
||||||
73
plugins/retrieval-plugin/README.md
Normal file
73
plugins/retrieval-plugin/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Create a Jan Plugin using Typescript
|
||||||
|
|
||||||
|
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
|
||||||
|
|
||||||
|
## Create Your Own Plugin
|
||||||
|
|
||||||
|
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
|
||||||
|
|
||||||
|
1. Click the Use this template button at the top of the repository
|
||||||
|
2. Select Create a new repository
|
||||||
|
3. Select an owner and name for your new repository
|
||||||
|
4. Click Create repository
|
||||||
|
5. Clone your new repository
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> You'll need to have a reasonably modern version of
|
||||||
|
> [Node.js](https://nodejs.org) handy. If you are using a version manager like
|
||||||
|
> [`nodenv`](https://github.com/nodenv/nodenv) or
|
||||||
|
> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
|
||||||
|
> root of your repository to install the version specified in
|
||||||
|
> [`package.json`](./package.json). Otherwise, 20.x or later should work!
|
||||||
|
|
||||||
|
1. :hammer_and_wrench: Install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
1. :building_construction: Package the TypeScript for distribution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
1. :white_check_mark: Check your artifact
|
||||||
|
|
||||||
|
There will be a tgz file in your plugin directory now
|
||||||
|
|
||||||
|
## Update the Plugin Metadata
|
||||||
|
|
||||||
|
The [`package.json`](package.json) file defines metadata about your plugin, such as
|
||||||
|
plugin name, main entry, description and version.
|
||||||
|
|
||||||
|
When you copy this repository, update `package.json` with the name, description for your plugin.
|
||||||
|
|
||||||
|
## Update the Plugin Code
|
||||||
|
|
||||||
|
The [`src/`](./src/) directory is the heart of your plugin! This contains the
|
||||||
|
source code that will be run when your plugin extension functions are invoked. You can replace the
|
||||||
|
contents of this directory with your own code.
|
||||||
|
|
||||||
|
There are a few things to keep in mind when writing your plugin code:
|
||||||
|
|
||||||
|
- Most Jan Plugin Extension functions are processed asynchronously.
|
||||||
|
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { core } from "@janhq/core";
|
||||||
|
|
||||||
|
function onStart(): Promise<any> {
|
||||||
|
return core.invokePluginFunc(MODULE_PATH, "run", 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about the Jan Plugin Core module, see the
|
||||||
|
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
|
||||||
|
|
||||||
|
So, what are you waiting for? Go ahead and start customizing your plugin!
|
||||||
43
plugins/retrieval-plugin/package.json
Normal file
43
plugins/retrieval-plugin/package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "retrieval-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Retrieval plugin for Jan app",
|
||||||
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/module.js",
|
||||||
|
"author": "Jan <service@jan.ai>",
|
||||||
|
"license": "MIT",
|
||||||
|
"activationPoints": [
|
||||||
|
"init"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
|
"bundle": "npm pack"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.88.2",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@janhq/core": "^0.1.1",
|
||||||
|
"faiss-node": "^0.5.1",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"langchain": "^0.0.169",
|
||||||
|
"npm": "^10.2.0",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
|
"ts-loader": "^9.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"package.json",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"bundleDependencies": [
|
||||||
|
"pdf-parse",
|
||||||
|
"langchain",
|
||||||
|
"faiss-node"
|
||||||
|
]
|
||||||
|
}
|
||||||
172
plugins/retrieval-plugin/src/index.ts
Normal file
172
plugins/retrieval-plugin/src/index.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* The entrypoint for the plugin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventName,
|
||||||
|
NewMessageRequest,
|
||||||
|
PluginService,
|
||||||
|
RegisterExtensionPoint,
|
||||||
|
invokePluginFunc,
|
||||||
|
events,
|
||||||
|
preferences,
|
||||||
|
store,
|
||||||
|
} from "@janhq/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register event listener.
|
||||||
|
*/
|
||||||
|
const registerListener = () => {
|
||||||
|
events.on(EventName.OnNewMessageRequest, inferenceRequest);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the `ingest` function from the `module.js` file using the `invokePluginFunc` method.
|
||||||
|
* "ingest" is the name of the function to invoke.
|
||||||
|
* @returns {Promise<any>} A promise that resolves with the result of the `run` function.
|
||||||
|
*/
|
||||||
|
function onStart(): Promise<void> {
|
||||||
|
registerListener();
|
||||||
|
ingest();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the document ingestion directory path from the `preferences` module and invokes the `ingest` function
|
||||||
|
* from the specified module with the directory path and additional options.
|
||||||
|
* The additional options are retrieved from the `preferences` module using the `PLUGIN_NAME` constant.
|
||||||
|
*/
|
||||||
|
async function ingest() {
|
||||||
|
const path = await preferences.get(PLUGIN_NAME, "ingestDocumentDirectoryPath");
|
||||||
|
|
||||||
|
// TODO: Hiro - Add support for custom embeddings
|
||||||
|
const customizedEmbedding = undefined;
|
||||||
|
|
||||||
|
if (path && path.length > 0) {
|
||||||
|
const openAPIKey = await preferences.get(PLUGIN_NAME, "openAIApiKey");
|
||||||
|
const azureOpenAIBasePath = await preferences.get(PLUGIN_NAME, "azureOpenAIBasePath");
|
||||||
|
const azureOpenAIApiInstanceName = await preferences.get(PLUGIN_NAME, "azureOpenAIApiInstanceName");
|
||||||
|
invokePluginFunc(MODULE_PATH, "ingest", path, customizedEmbedding, {
|
||||||
|
openAIApiKey: openAPIKey?.length > 0 ? openAPIKey : undefined,
|
||||||
|
azureOpenAIApiKey: await preferences.get(PLUGIN_NAME, "azureOpenAIApiKey"),
|
||||||
|
azureOpenAIApiVersion: await preferences.get(PLUGIN_NAME, "azureOpenAIApiVersion"),
|
||||||
|
azureOpenAIApiInstanceName: azureOpenAIApiInstanceName?.length > 0 ? azureOpenAIApiInstanceName : undefined,
|
||||||
|
azureOpenAIApiDeploymentName: await preferences.get(PLUGIN_NAME, "azureOpenAIApiDeploymentNameRag"),
|
||||||
|
azureOpenAIBasePath: azureOpenAIBasePath?.length > 0 ? azureOpenAIBasePath : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the document ingestion directory path from the `preferences` module and invokes the `ingest` function
|
||||||
|
* from the specified module with the directory path and additional options.
|
||||||
|
* The additional options are retrieved from the `preferences` module using the `PLUGIN_NAME` constant.
|
||||||
|
*/
|
||||||
|
async function inferenceRequest(data: NewMessageRequest): Promise<any> {
|
||||||
|
// TODO: Hiro - Add support for custom embeddings
|
||||||
|
const customLLM = undefined;
|
||||||
|
const message = {
|
||||||
|
...data,
|
||||||
|
message: "",
|
||||||
|
user: "RAG",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
_id: undefined,
|
||||||
|
};
|
||||||
|
const id = await store.insertOne("messages", message);
|
||||||
|
message._id = id;
|
||||||
|
events.emit(EventName.OnNewMessageResponse, message);
|
||||||
|
|
||||||
|
const openAPIKey = await preferences.get(PLUGIN_NAME, "openAIApiKey");
|
||||||
|
const azureOpenAIBasePath = await preferences.get(PLUGIN_NAME, "azureOpenAIBasePath");
|
||||||
|
const azureOpenAIApiInstanceName = await preferences.get(PLUGIN_NAME, "azureOpenAIApiInstanceName");
|
||||||
|
invokePluginFunc(MODULE_PATH, "chatWithDocs", data.message, customLLM, {
|
||||||
|
openAIApiKey: openAPIKey?.length > 0 ? openAPIKey : undefined,
|
||||||
|
azureOpenAIApiKey: await preferences.get(PLUGIN_NAME, "azureOpenAIApiKey"),
|
||||||
|
azureOpenAIApiVersion: await preferences.get(PLUGIN_NAME, "azureOpenAIApiVersion"),
|
||||||
|
azureOpenAIApiInstanceName: azureOpenAIApiInstanceName?.length > 0 ? azureOpenAIApiInstanceName : undefined,
|
||||||
|
azureOpenAIApiDeploymentName: await preferences.get(PLUGIN_NAME, "azureOpenAIApiDeploymentNameChat"),
|
||||||
|
azureOpenAIBasePath: azureOpenAIBasePath?.length > 0 ? azureOpenAIBasePath : undefined,
|
||||||
|
modelName: "gpt-3.5-turbo-16k",
|
||||||
|
temperature: 0.2,
|
||||||
|
}).then(async (text) => {
|
||||||
|
console.log("RAG Response:", text);
|
||||||
|
message.message = text;
|
||||||
|
|
||||||
|
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initializes the plugin by registering the extension functions with the given register function.
|
||||||
|
* @param {Function} options.register - The function to use for registering the extension functions
|
||||||
|
*/
|
||||||
|
export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||||
|
register(PluginService.OnStart, PLUGIN_NAME, onStart);
|
||||||
|
register(PluginService.OnPreferencesUpdate, PLUGIN_NAME, ingest);
|
||||||
|
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"ingestDocumentDirectoryPath",
|
||||||
|
"Document Ingest Directory Path",
|
||||||
|
"The URL of the directory containing the documents to ingest",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"openAIApiKey",
|
||||||
|
"Open API Key",
|
||||||
|
"OpenAI API Key",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"azureOpenAIApiKey",
|
||||||
|
"Azure API Key",
|
||||||
|
"Azure Project API Key",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"azureOpenAIApiVersion",
|
||||||
|
"Azure API Version",
|
||||||
|
"Azure Project API Version",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"azureOpenAIApiInstanceName",
|
||||||
|
"Azure Instance Name",
|
||||||
|
"Azure Project Instance Name",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"azureOpenAIApiDeploymentNameChat",
|
||||||
|
"Azure Chat Model Deployment Name",
|
||||||
|
"Azure Project Chat Model Deployment Name (e.g. gpt-3.5-turbo-16k)",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"azureOpenAIApiDeploymentNameRag",
|
||||||
|
"Azure Text Embedding Model Deployment Name",
|
||||||
|
"Azure Project Text Embedding Model Deployment Name (e.g. text-embedding-ada-002)",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
preferences.registerPreferences<string>(
|
||||||
|
register,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
"azureOpenAIBasePath",
|
||||||
|
"Azure Base Path",
|
||||||
|
"Azure Project Base Path",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
58
plugins/retrieval-plugin/src/module.ts
Normal file
58
plugins/retrieval-plugin/src/module.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const { app } = require("electron");
|
||||||
|
const { DirectoryLoader } = require("langchain/document_loaders/fs/directory");
|
||||||
|
const { OpenAIEmbeddings } = require("langchain/embeddings/openai");
|
||||||
|
const { PDFLoader } = require("langchain/document_loaders/fs/pdf");
|
||||||
|
const { CharacterTextSplitter } = require("langchain/text_splitter");
|
||||||
|
const { FaissStore } = require("langchain/vectorstores/faiss");
|
||||||
|
const { ChatOpenAI } = require("langchain/chat_models/openai");
|
||||||
|
const { RetrievalQAChain } = require("langchain/chains");
|
||||||
|
|
||||||
|
var db: any | undefined = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ingests documents from the specified directory
|
||||||
|
* If an `embedding` object is not provided, uses OpenAIEmbeddings.
|
||||||
|
* The resulting embeddings are stored in the database using Faiss.
|
||||||
|
* @param docDir - The directory containing the documents to ingest.
|
||||||
|
* @param embedding - An optional object used to generate embeddings for the documents.
|
||||||
|
* @param config - An optional configuration object used to create a new `OpenAIEmbeddings` object.
|
||||||
|
*/
|
||||||
|
async function ingest(docDir: string, embedding?: any, config?: any) {
|
||||||
|
const loader = new DirectoryLoader(docDir, {
|
||||||
|
".pdf": (path) => new PDFLoader(path),
|
||||||
|
});
|
||||||
|
const docs = await loader.load();
|
||||||
|
const textSplitter = new CharacterTextSplitter();
|
||||||
|
const docsQA = await textSplitter.splitDocuments(docs);
|
||||||
|
const embeddings = embedding ?? new OpenAIEmbeddings({ ...config });
|
||||||
|
db = await FaissStore.fromDocuments(await docsQA, embeddings);
|
||||||
|
console.log("Documents are ingested");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an answer to a given question using the specified `llm` or a new `ChatOpenAI`.
|
||||||
|
* The function uses the `RetrievalQAChain` class to retrieve the most relevant document from the database and generate an answer.
|
||||||
|
* @param question - The question to generate an answer for.
|
||||||
|
* @param llm - An optional object used to generate the answer.
|
||||||
|
* @param config - An optional configuration object used to create a new `ChatOpenAI` object, can be ignored if llm is specified.
|
||||||
|
* @returns A Promise that resolves with the generated answer.
|
||||||
|
*/
|
||||||
|
async function chatWithDocs(question: string, llm?: any, config?: any): Promise<any> {
|
||||||
|
const llm_question_answer =
|
||||||
|
llm ??
|
||||||
|
new ChatOpenAI({
|
||||||
|
temperature: 0.2,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
const qa = RetrievalQAChain.fromLLM(llm_question_answer, db.asRetriever(), {
|
||||||
|
verbose: true,
|
||||||
|
});
|
||||||
|
const answer = await qa.run(question);
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ingest,
|
||||||
|
chatWithDocs,
|
||||||
|
};
|
||||||
13
plugins/retrieval-plugin/tsconfig.json
Normal file
13
plugins/retrieval-plugin/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "ES6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
35
plugins/retrieval-plugin/webpack.config.js
Normal file
35
plugins/retrieval-plugin/webpack.config.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const packageJson = require("./package.json");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
experiments: { outputModule: true },
|
||||||
|
entry: "./src/index.ts",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||||
|
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
filename: "index.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
library: { type: "module" },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js"],
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -149,6 +149,7 @@ export const Preferences = () => {
|
|||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
|
if(extensionPoints.get(PluginService.OnPreferencesUpdate))
|
||||||
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100)
|
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user