Merge pull request #1752 from janhq/dev

Release Cut v0.4.5
This commit is contained in:
Louis 2024-01-25 11:14:43 +07:00 committed by GitHub
commit 736ea8fc31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
224 changed files with 4717 additions and 3531 deletions

View File

@ -16,7 +16,6 @@ on:
jobs: jobs:
set-public-provider: set-public-provider:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
outputs: outputs:
public_provider: ${{ steps.set-public-provider.outputs.public_provider }} public_provider: ${{ steps.set-public-provider.outputs.public_provider }}
ref: ${{ steps.set-public-provider.outputs.ref }} ref: ${{ steps.set-public-provider.outputs.ref }}
@ -69,9 +68,9 @@ jobs:
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
with: with:
ref: ${{ needs.set-public-provider.outputs.ref }} ref: refs/heads/dev
build_reason: Nightly build_reason: Nightly
push_to_branch: main push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}
noti-discord-manual-and-update-url-readme: noti-discord-manual-and-update-url-readme:
@ -80,7 +79,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'cloudflare-r2' if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'cloudflare-r2'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
with: with:
ref: ${{ needs.set-public-provider.outputs.ref }} ref: refs/heads/dev
build_reason: Manual build_reason: Manual
push_to_branch: main push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}

View File

@ -46,7 +46,7 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
with: with:
ref: refs/heads/main ref: refs/heads/dev
build_reason: Nightly build_reason: Nightly
push_to_branch: main push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }} new_version: ${{ needs.get-update-version.outputs.new_version }}

View File

@ -58,7 +58,7 @@ jobs:
mv /tmp/package.json electron/package.json mv /tmp/package.json electron/package.json
jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json mv /tmp/package.json web/package.json
jq '.build.publish = [{"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json mv /tmp/package.json electron/package.json
cat electron/package.json cat electron/package.json

View File

@ -1,4 +1,4 @@
name: build-linux-x64 name: build-macos
on: on:
workflow_call: workflow_call:
inputs: inputs:
@ -70,7 +70,7 @@ jobs:
jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json mv /tmp/package.json web/package.json
jq '.build.publish = [{"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json mv /tmp/package.json electron/package.json
cat electron/package.json cat electron/package.json

View File

@ -71,7 +71,7 @@ jobs:
jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json mv /tmp/package.json web/package.json
jq '.build.publish = [{"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json mv /tmp/package.json electron/package.json
jq '.build.win.sign = "./sign.js"' electron/package.json > /tmp/package.json jq '.build.win.sign = "./sign.js"' electron/package.json > /tmp/package.json

4
.gitignore vendored
View File

@ -23,4 +23,6 @@ extensions/inference-nitro-extension/bin/*/*.metal
extensions/inference-nitro-extension/bin/*/*.exe extensions/inference-nitro-extension/bin/*/*.exe
extensions/inference-nitro-extension/bin/*/*.dll extensions/inference-nitro-extension/bin/*/*.dll
extensions/inference-nitro-extension/bin/*/*.exp extensions/inference-nitro-extension/bin/*/*.exp
extensions/inference-nitro-extension/bin/*/*.lib extensions/inference-nitro-extension/bin/*/*.lib
extensions/inference-nitro-extension/bin/saved-*
extensions/inference-nitro-extension/bin/*.tar.gz

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx pretty-quick --staged

View File

@ -39,6 +39,7 @@ lint: check-file-counts
# Testing # Testing
test: lint test: lint
yarn build:test yarn build:test
yarn test:unit
yarn test yarn test
# Builds and publishes the app # Builds and publishes the app

View File

@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center"> <tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td> <td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/0.4.4-159/jan-win-x64-0.4.4-159.exe'> <a href='https://delta.jan.ai/latest/jan-win-x64-0.4.4-170.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/0.4.4-159/jan-mac-x64-0.4.4-159.dmg'> <a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.4-170.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b> <b>Intel</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/0.4.4-159/jan-mac-arm64-0.4.4-159.dmg'> <a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.4-170.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b> <b>M1/M2</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/0.4.4-159/jan-linux-amd64-0.4.4-159.deb'> <a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.4-170.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/0.4.4-159/jan-linux-x86_64-0.4.4-159.AppImage'> <a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.4-170.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b> <b>jan.AppImage</b>
</a> </a>

3
core/.gitignore vendored
View File

@ -6,7 +6,4 @@ coverage
.vscode .vscode
.idea .idea
dist dist
compiled
.awcache
.rpt2_cache
docs docs

7
core/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto"
}

View File

@ -1,348 +1,69 @@
## @janhq/core ## @janhq/core
> The module includes functions for communicating with core APIs, registering plugin extensions, and exporting type definitions. > This module includes functions for communicating with core APIs, registering app extensions, and exporting type definitions.
## Usage ## Usage
### Import the package ### Import the package
```js ```js
// javascript // Web / extension runtime
const core = require("@janhq/core");
// typescript
import * as core from "@janhq/core"; import * as core from "@janhq/core";
// Node runtime
import * as node from "@janhq/core/node";
``` ```
### Register Plugin Extensions ## Build an Extension
Every plugin must define an `init` function in its main entry file to initialize the plugin and register its extensions with the Jan platform. 1. Download an extension template, for example, [https://github.com/janhq/extension-template](https://github.com/janhq/extension-template).
You can `register` any function as a plugin extension using `CoreServiceAPI` below. For example, the `DataService.GetConversations` entry name can be used to register a function that retrieves conversations. 2. Update the source code:
1. Open `index.ts` in your code editor.
Once the extension is registered, it can be used by other plugins or components in the Jan platform. For example, a UI component might use the DataService.GetConversations extension to retrieve a list of conversations to display to the user. 2. Rename the extension class from `SampleExtension` to your preferred extension name.
3. Import modules from the core package.
```js ```ts
import { RegisterExtensionPoint, DataService } from "@janhq/core"; import * as core from "@janhq/core";
```
function getConversations() { 4. In the `onLoad()` method, add your code:
// Your logic here ```ts
} // Example of listening to app events and providing customized inference logic:
import * as core from "@janhq/core";
export function init({ register }: { register: RegisterExtensionPoint }) {
register(DataService.GetConversations, getConversations.name, getConversations); export default class MyExtension extends BaseExtension {
} // On extension load
``` onLoad() {
core.events.on(MessageEvent.OnMessageSent, (data) => MyExtension.inference(data, this));
### Interact with Local Data Storage }
The Core API allows you to interact with local data storage. Here are a couple of examples of how you can use it: // Customized inference logic
private static inference(incomingMessage: MessageRequestData) {
#### Insert Data
// Prepare customized message content
You can use the store.insertOne function to insert data into a specific collection in the local data store. const content: ThreadContent = {
type: ContentType.Text,
```js text: {
import { store } from "@janhq/core"; value: "I'm Jan Assistant!",
annotations: [],
function insertData() { },
store.insertOne("conversations", { name: "meow" }); };
// Insert a new document with { name: "meow" } into the "conversations" collection.
} // Modify message and send out
``` const outGoingMessage: ThreadMessage = {
...incomingMessage,
#### Get Data content
};
To retrieve data from a collection in the local data store, you can use the `store.findOne` or `store.findMany` function. It allows you to filter and retrieve documents based on specific criteria. }
}
store.getOne(collectionName, key) retrieves a single document that matches the provided key in the specified collection. ```
store.getMany(collectionName, selector, sort) retrieves multiple documents that match the provided selector in the specified collection. 3. Build the extension:
1. Navigate to the extension directory.
```js 2. Install dependencies.
import { store } from "@janhq/core"; ```bash
yarn install
function getData() { ```
const selector = { name: "meow" }; 3. Compile the source code. The following command keeps running in the terminal and rebuilds the extension when you modify the source code.
const data = store.findMany("conversations", selector); ```bash
// Retrieve documents from the "conversations" collection that match the filter. yarn build
} ```
``` 4. Select the generated .tgz from Jan > Settings > Extension > Manual Installation.
#### Update Data
You can update data in the local store using these functions:
store.updateOne(collectionName, key, update) updates a single document that matches the provided key in the specified collection.
store.updateMany(collectionName, selector, update) updates multiple documents that match the provided selector in the specified collection.
```js
function updateData() {
const selector = { name: "meow" };
const update = { name: "newName" };
store.updateOne("conversations", selector, update);
// Update a document in the "conversations" collection.
}
```
#### Delete Data
You can delete data from the local data store using these functions:
store.deleteOne(collectionName, key) deletes a single document that matches the provided key in the specified collection.
store.deleteMany(collectionName, selector) deletes multiple documents that match the provided selector in the specified collection.
```js
function deleteData() {
const selector = { name: "meow" };
store.deleteOne("conversations", selector);
// Delete a document from the "conversations" collection.
}
```
### Events
You can subscribe to NewMessageRequest events by defining a function to handle the event and registering it with the events object:
```js
import { events } from "@janhq/core";
function handleMessageRequest(message: NewMessageRequest) {
// Your logic here. For example:
// const response = openai.createChatCompletion({...})
}
function registerListener() {
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
}
// Register the listener function with the relevant extension points.
export function init({ register }) {
registerListener();
}
```
In this example, we're defining a function called handleMessageRequest that takes a NewMessageRequest object as its argument. We're also defining a function called registerListener that registers the handleMessageRequest function as a listener for NewMessageRequest events using the on method of the events object.
```js
import { events } from "@janhq/core";
function handleMessageRequest(data: NewMessageRequest) {
// Your logic here. For example:
const response = openai.createChatCompletion({...})
const message: NewMessageResponse = {
...data,
message: response.data.choices[0].message.content
}
// Now emit event so the app can display in the conversation
events.emit(EventName.OnNewMessageResponse, message)
}
```
### Preferences
To register plugin preferences, you can use the preferences object from the @janhq/core package. Here's an example of how to register and retrieve plugin preferences:
```js
import { PluginService, preferences } from "@janhq/core";
const pluginName = "your-first-plugin";
const preferenceKey = "";
const preferenceName = "Your First Preference";
const preferenceDescription = "This is for example only";
const defaultValue = "";
export function init({ register }: { register: RegisterExtensionPoint }) {
// Register preference update handlers. E.g. update plugin instance with new configuration
register(PluginService.OnPreferencesUpdate, pluginName, onPreferencesUpdate);
// Register plugin preferences. E.g. Plugin need apiKey to connect to your service
preferences.registerPreferences <
string >
(register, pluginName, preferenceKey, preferenceName, preferenceDescription, defaultValue);
}
```
In this example, we're registering preference update handlers and plugin preferences using the preferences object. We're also defining a PluginName constant to use as the name of the plugin.
To retrieve the values of the registered preferences, we're using the get method of the preferences object and passing in the name of the plugin and the name of the preference.
```js
import { preferences } from "@janhq/core";
const pluginName = "your-first-plugin";
const preferenceKey = "apiKey";
const setup = async () => {
// Retrieve apiKey
const apiKey: string = (await preferences.get(pluginName, preferenceKey)) ?? "";
};
```
### Access Core API
To access the Core API in your plugin, you can follow the code examples and explanations provided below.
##### Import Core API and Store Module
In your main entry code (e.g., `index.ts`), start by importing the necessary modules and functions from the `@janhq/core` library.
```js
// index.ts
import * as core from "@janhq/core";
```
#### Perform File Operations
The Core API also provides functions to perform file operations. Here are a couple of examples:
#### Download a File
You can download a file from a specified URL and save it with a given file name using the core.downloadFile function.
```js
function downloadModel(url: string, fileName: string) {
core.downloadFile(url, fileName);
}
```
#### Delete a File
To delete a file, you can use the core.deleteFile function, providing the path to the file you want to delete.
```js
function deleteModel(filePath: string) {
core.deleteFile(path);
}
```
#### Execute plugin module in main process
To execute a plugin module in the main process of your application, you can follow the steps outlined below.
##### Import the `core` Object
In your main process code (e.g., `index.ts`), start by importing the `core` object from the `@janhq/core` library.
```js
// index.ts
import * as core from "@janhq/core";
```
##### Define the Module Path
Specify the path to the plugin module you want to execute. This path should lead to the module file (e.g., module.js) that contains the functions you wish to call.
```js
// index.ts
const MODULE_PATH = "data-plugin/dist/module.js";
```
##### Define the Function to Execute
Create a function that will execute a function defined in your plugin module. In the example provided, the function `getConversationMessages` is created to invoke the `getConvMessages` function from the plugin module.
```js
// index.ts
function getConversationMessages(id: number) {
return core.invokePluginFunc(MODULE_PATH, "getConvMessages", id);
}
export function init({ register }: { register: RegisterExtensionPoint }) {
register(DataService.GetConversationMessages, getConversationMessages.name, getConversationMessages);
}
```
##### Define Your Plugin Module
In your plugin module (e.g., module.ts), define the logic for the function you wish to execute. In the example, the function getConvMessages is defined with a placeholder comment indicating where your logic should be implemented.
```js
// module.ts
function getConvMessages(id: number) {
// Your logic here
}
module.exports = {
getConvMessages,
};
```
## CoreService API
The `CoreService` type is an exported union type that includes:
- `StoreService`
- `DataService`
- `InferenceService`
- `ModelManagementService`
- `SystemMonitoringService`
- `PreferenceService`
## StoreService
The `StoreService` enum represents available methods for managing the database store. It includes the following methods:
- `CreateCollection`: Creates a new collection in the data store.
- `DeleteCollection`: Deletes an existing collection from the data store.
- `InsertOne`: Inserts a new value into an existing collection in the data store.
- `UpdateOne`: Updates an existing value in an existing collection in the data store.
- `UpdateMany`: Updates multiple records in a collection in the data store.
- `DeleteOne`: Deletes an existing value from an existing collection in the data store.
- `DeleteMany`: Deletes multiple records in a collection in the data store.
- `FindMany`: Retrieves multiple records from a collection in the data store.
- `FindOne`: Retrieves a single record from a collection in the data store.
## DataService
The `DataService` enum represents methods related to managing conversations and messages. It includes the following methods:
- `GetConversations`: Gets a list of conversations from the data store.
- `CreateConversation`: Creates a new conversation in the data store.
- `DeleteConversation`: Deletes an existing conversation from the data store.
- `CreateMessage`: Creates a new message in an existing conversation in the data store.
- `UpdateMessage`: Updates an existing message in an existing conversation in the data store.
- `GetConversationMessages`: Gets a list of messages for an existing conversation from the data store.
## InferenceService
The `InferenceService` enum exports:
- `InitModel`: Initializes a model for inference.
- `StopModel`: Stops a running inference model.
## ModelManagementService
The `ModelManagementService` enum provides methods for managing models:
- `GetDownloadedModels`: Gets a list of downloaded models.
- `GetAvailableModels`: Gets a list of available models from data store.
- `DeleteModel`: Deletes a downloaded model.
- `DownloadModel`: Downloads a model from the server.
- `SearchModels`: Searches for models on the server.
- `GetConfiguredModels`: Gets configured models from the data store.
- `StoreModel`: Stores a model in the data store.
- `UpdateFinishedDownloadAt`: Updates the finished download time for a model in the data store.
- `GetUnfinishedDownloadModels`: Gets a list of unfinished download models from the data store.
- `GetFinishedDownloadModels`: Gets a list of finished download models from the data store.
- `DeleteDownloadModel`: Deletes a downloaded model from the data store.
- `GetModelById`: Gets a model by its ID from the data store.
## PreferenceService
The `PreferenceService` enum provides methods for managing plugin preferences:
- `ExperimentComponent`: Represents the UI experiment component for a testing function.
## SystemMonitoringService
The `SystemMonitoringService` enum includes methods for monitoring system resources:
- `GetResourcesInfo`: Gets information about system resources.
- `GetCurrentLoad`: Gets the current system load.
## PluginService
The `PluginService` enum includes plugin cycle handlers:
- `OnStart`: Handler for starting. E.g. Create a collection.
- `OnPreferencesUpdate`: Handler for preferences update. E.g. Update instances with new configurations.
For more detailed information on each of these components, please refer to the source code.

7
core/jest.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
}

View File

@ -12,16 +12,10 @@
"module": "dist/core.es5.js", "module": "dist/core.es5.js",
"typings": "dist/types/index.d.ts", "typings": "dist/types/index.d.ts",
"files": [ "files": [
"dist" "dist",
"types"
], ],
"author": "Jan <service@jan.ai>", "author": "Jan <service@jan.ai>",
"repository": {
"type": "git",
"url": ""
},
"engines": {
"node": ">=6.0.0"
},
"exports": { "exports": {
".": "./dist/core.umd.js", ".": "./dist/core.umd.js",
"./sdk": "./dist/core.umd.js", "./sdk": "./dist/core.umd.js",
@ -45,66 +39,23 @@
}, },
"scripts": { "scripts": {
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"test": "jest",
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "tsc --module commonjs && rollup -c rollup.config.ts", "build": "tsc --module commonjs && rollup -c rollup.config.ts",
"start": "rollup -c rollup.config.ts -w" "start": "rollup -c rollup.config.ts -w"
}, },
"lint-staged": {
"{src,test}/**/*.ts": [
"prettier --write",
"git add"
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
},
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/test/"
],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverageFrom": [
"src/*.{js,ts}"
]
},
"prettier": {
"semi": false,
"singleQuote": true
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"devDependencies": { "devDependencies": {
"jest": "^25.4.0",
"@types/jest": "^29.5.11",
"@types/node": "^12.0.2", "@types/node": "^12.0.2",
"eslint-plugin-jest": "^23.8.2",
"rollup": "^2.38.5", "rollup": "^2.38.5",
"rollup-plugin-commonjs": "^9.1.8", "rollup-plugin-commonjs": "^9.1.8",
"rollup-plugin-json": "^3.1.0", "rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0", "rollup-plugin-typescript2": "^0.36.0",
"ts-node": "^7.0.1", "ts-jest": "^26.1.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }

View File

@ -7,12 +7,16 @@ export enum AppRoute {
openExternalUrl = 'openExternalUrl', openExternalUrl = 'openExternalUrl',
openAppDirectory = 'openAppDirectory', openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer', openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
getAppConfigurations = 'getAppConfigurations',
updateAppConfiguration = 'updateAppConfiguration',
relaunch = 'relaunch', relaunch = 'relaunch',
joinPath = 'joinPath', joinPath = 'joinPath',
baseName = 'baseName', baseName = 'baseName',
startServer = 'startServer', startServer = 'startServer',
stopServer = 'stopServer', stopServer = 'stopServer',
log = 'log' log = 'log',
logServer = 'logServer',
} }
export enum AppEvent { export enum AppEvent {
@ -55,7 +59,7 @@ export enum FileSystemRoute {
} }
export enum FileManagerRoute { export enum FileManagerRoute {
syncFile = 'syncFile', syncFile = 'syncFile',
getUserSpace = 'getUserSpace', getJanDataFolderPath = 'getJanDataFolderPath',
getResourcePath = 'getResourcePath', getResourcePath = 'getResourcePath',
fileStat = 'fileStat', fileStat = 'fileStat',
} }

View File

@ -19,10 +19,12 @@ const executeOnMain: (extension: string, method: string, ...args: any[]) => Prom
* Downloads a file from a URL and saves it to the local file system. * Downloads a file from a URL and saves it to the local file system.
* @param {string} url - The URL of the file to download. * @param {string} url - The URL of the file to download.
* @param {string} fileName - The name to use for the downloaded file. * @param {string} fileName - The name to use for the downloaded file.
* @param {object} network - Optional object to specify proxy/whether to ignore SSL certificates.
* @returns {Promise<any>} A promise that resolves when the file is downloaded. * @returns {Promise<any>} A promise that resolves when the file is downloaded.
*/ */
const downloadFile: (url: string, fileName: string) => Promise<any> = (url, fileName) => const downloadFile: (url: string, fileName: string, network?: { proxy?: string, ignoreSSL?: boolean }) => Promise<any> = (url, fileName, network) => {
global.core?.api?.downloadFile(url, fileName) return global.core?.api?.downloadFile(url, fileName, network)
}
/** /**
* Aborts the download of a specific file. * Aborts the download of a specific file.
@ -33,10 +35,11 @@ const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
global.core.api?.abortDownload(fileName) global.core.api?.abortDownload(fileName)
/** /**
* Gets the user space path. * Gets Jan's data folder path.
* @returns {Promise<any>} A Promise that resolves with the user space path. *
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
*/ */
const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace() const getJanDataFolderPath = (): Promise<string> => global.core.api?.getJanDataFolderPath()
/** /**
* Opens the file explorer at a specific path. * Opens the file explorer at a specific path.
@ -101,12 +104,12 @@ export {
executeOnMain, executeOnMain,
downloadFile, downloadFile,
abortDownload, abortDownload,
getUserSpace, getJanDataFolderPath,
openFileExplorer, openFileExplorer,
getResourcePath, getResourcePath,
joinPath, joinPath,
openExternalUrl, openExternalUrl,
baseName, baseName,
log, log,
FileStat FileStat,
} }

View File

@ -1,28 +1,3 @@
// TODO: refactor EventName to use the events defined in /types
/**
* The `EventName` enumeration contains the names of all the available events in the Jan platform.
*/
export enum EventName {
/** The `OnMessageSent` event is emitted when a message is sent. */
OnMessageSent = 'OnMessageSent',
/** The `OnMessageResponse` event is emitted when a message is received. */
OnMessageResponse = 'OnMessageResponse',
/** The `OnMessageUpdate` event is emitted when a message is updated. */
OnMessageUpdate = 'OnMessageUpdate',
/** The `OnModelInit` event is emitted when a model inits. */
OnModelInit = 'OnModelInit',
/** The `OnModelReady` event is emitted when a model ready. */
OnModelReady = 'OnModelReady',
/** The `OnModelFail` event is emitted when a model fails loading. */
OnModelFail = 'OnModelFail',
/** The `OnModelStop` event is emitted when a model start to stop. */
OnModelStop = 'OnModelStop',
/** The `OnModelStopped` event is emitted when a model stopped ok. */
OnModelStopped = 'OnModelStopped',
/** The `OnInferenceStopped` event is emitted when a inference is stopped. */
OnInferenceStopped = 'OnInferenceStopped',
}
/** /**
* Adds an observer for an event. * Adds an observer for an event.
* *

View File

@ -1,4 +1,4 @@
export enum ExtensionType { export enum ExtensionTypeEnum {
Assistant = "assistant", Assistant = "assistant",
Conversational = "conversational", Conversational = "conversational",
Inference = "inference", Inference = "inference",
@ -6,17 +6,22 @@ export enum ExtensionType {
SystemMonitoring = "systemMonitoring", SystemMonitoring = "systemMonitoring",
} }
export interface ExtensionType {
type(): ExtensionTypeEnum | undefined;
}
/** /**
* Represents a base extension. * Represents a base extension.
* This class should be extended by any class that represents an extension. * This class should be extended by any class that represents an extension.
*/ */
export abstract class BaseExtension { export abstract class BaseExtension implements ExtensionType {
/** /**
* Returns the type of the extension. * Returns the type of the extension.
* @returns {ExtensionType} The type of the extension * @returns {ExtensionType} The type of the extension
* Undefined means its not extending any known extension by the application. * Undefined means its not extending any known extension by the application.
*/ */
abstract type(): ExtensionType | undefined; type(): ExtensionTypeEnum | undefined {
return undefined;
}
/** /**
* Called when the extension is loaded. * Called when the extension is loaded.
* Any initialization logic for the extension should be put here. * Any initialization logic for the extension should be put here.

View File

@ -1,12 +1,19 @@
import { Assistant, AssistantInterface } from '../index' import { Assistant, AssistantInterface } from "../index";
import { BaseExtension } from '../extension' import { BaseExtension, ExtensionTypeEnum } from "../extension";
/** /**
* Assistant extension for managing assistants. * Assistant extension for managing assistants.
* @extends BaseExtension * @extends BaseExtension
*/ */
export abstract class AssistantExtension extends BaseExtension implements AssistantInterface { export abstract class AssistantExtension extends BaseExtension implements AssistantInterface {
abstract createAssistant(assistant: Assistant): Promise<void> /**
abstract deleteAssistant(assistant: Assistant): Promise<void> * Assistant extension type.
abstract getAssistants(): Promise<Assistant[]> */
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.Assistant;
}
abstract createAssistant(assistant: Assistant): Promise<void>;
abstract deleteAssistant(assistant: Assistant): Promise<void>;
abstract getAssistants(): Promise<Assistant[]>;
} }

View File

@ -1,5 +1,5 @@
import { Thread, ThreadInterface, ThreadMessage, MessageInterface } from '../index' import { Thread, ThreadInterface, ThreadMessage, MessageInterface } from '../index'
import { BaseExtension } from '../extension' import { BaseExtension, ExtensionTypeEnum } from '../extension'
/** /**
* Conversational extension. Persists and retrieves conversations. * Conversational extension. Persists and retrieves conversations.
@ -10,6 +10,13 @@ export abstract class ConversationalExtension
extends BaseExtension extends BaseExtension
implements ThreadInterface, MessageInterface implements ThreadInterface, MessageInterface
{ {
/**
* Conversation extension type.
*/
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.Conversational;
}
abstract getThreads(): Promise<Thread[]> abstract getThreads(): Promise<Thread[]>
abstract saveThread(thread: Thread): Promise<void> abstract saveThread(thread: Thread): Promise<void>
abstract deleteThread(threadId: string): Promise<void> abstract deleteThread(threadId: string): Promise<void>

View File

@ -1,9 +1,16 @@
import { InferenceInterface, MessageRequest, ThreadMessage } from '../index' import { InferenceInterface, MessageRequest, ThreadMessage } from "../index";
import { BaseExtension } from '../extension' import { BaseExtension, ExtensionTypeEnum } from "../extension";
/** /**
* Inference extension. Start, stop and inference models. * Inference extension. Start, stop and inference models.
*/ */
export abstract class InferenceExtension extends BaseExtension implements InferenceInterface { export abstract class InferenceExtension extends BaseExtension implements InferenceInterface {
abstract inference(data: MessageRequest): Promise<ThreadMessage> /**
* Inference extension type.
*/
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.Inference;
}
abstract inference(data: MessageRequest): Promise<ThreadMessage>;
} }

View File

@ -1,14 +1,24 @@
import { BaseExtension } from '../extension' import { BaseExtension, ExtensionTypeEnum } from "../extension";
import { Model, ModelInterface } from '../index' import { Model, ModelInterface } from "../index";
/** /**
* Model extension for managing models. * Model extension for managing models.
*/ */
export abstract class ModelExtension extends BaseExtension implements ModelInterface { export abstract class ModelExtension extends BaseExtension implements ModelInterface {
abstract downloadModel(model: Model): Promise<void> /**
abstract cancelModelDownload(modelId: string): Promise<void> * Model extension type.
abstract deleteModel(modelId: string): Promise<void> */
abstract saveModel(model: Model): Promise<void> type(): ExtensionTypeEnum | undefined {
abstract getDownloadedModels(): Promise<Model[]> return ExtensionTypeEnum.Model;
abstract getConfiguredModels(): Promise<Model[]> }
abstract downloadModel(
model: Model,
network?: { proxy: string; ignoreSSL?: boolean },
): Promise<void>;
abstract cancelModelDownload(modelId: string): Promise<void>;
abstract deleteModel(modelId: string): Promise<void>;
abstract saveModel(model: Model): Promise<void>;
abstract getDownloadedModels(): Promise<Model[]>;
abstract getConfiguredModels(): Promise<Model[]>;
} }

View File

@ -1,11 +1,18 @@
import { BaseExtension } from '../extension' import { BaseExtension, ExtensionTypeEnum } from "../extension";
import { MonitoringInterface } from '../index' import { MonitoringInterface } from "../index";
/** /**
* Monitoring extension for system monitoring. * Monitoring extension for system monitoring.
* @extends BaseExtension * @extends BaseExtension
*/ */
export abstract class MonitoringExtension extends BaseExtension implements MonitoringInterface { export abstract class MonitoringExtension extends BaseExtension implements MonitoringInterface {
abstract getResourcesInfo(): Promise<any> /**
abstract getCurrentLoad(): Promise<any> * Monitoring extension type.
*/
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.SystemMonitoring;
}
abstract getResourcesInfo(): Promise<any>;
abstract getCurrentLoad(): Promise<any>;
} }

View File

@ -2,13 +2,10 @@ import fs from 'fs'
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
import { join } from 'path' import { join } from 'path'
import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index' import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index'
import { getJanDataFolderPath } from '../../utils'
const os = require('os')
const path = join(os.homedir(), 'jan')
export const getBuilder = async (configuration: RouteConfiguration) => { export const getBuilder = async (configuration: RouteConfiguration) => {
const directoryPath = join(path, configuration.dirName) const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
try { try {
if (!fs.existsSync(directoryPath)) { if (!fs.existsSync(directoryPath)) {
console.debug('model folder not found') console.debug('model folder not found')
@ -72,7 +69,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin
} }
} }
const directoryPath = join(path, configuration.dirName) const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
try { try {
const data = await retrieveBuilder(configuration, id) const data = await retrieveBuilder(configuration, id)
if (!data) { if (!data) {
@ -94,7 +91,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin
} }
export const getMessages = async (threadId: string): Promise<ThreadMessage[]> => { export const getMessages = async (threadId: string): Promise<ThreadMessage[]> => {
const threadDirPath = join(path, 'threads', threadId) const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId)
const messageFile = 'messages.jsonl' const messageFile = 'messages.jsonl'
try { try {
const files: string[] = fs.readdirSync(threadDirPath) const files: string[] = fs.readdirSync(threadDirPath)
@ -155,7 +152,7 @@ export const createThread = async (thread: any) => {
created: Date.now(), created: Date.now(),
updated: Date.now(), updated: Date.now(),
} }
const threadDirPath = join(path, 'threads', updatedThread.id) const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id)
const threadJsonPath = join(threadDirPath, threadMetadataFileName) const threadJsonPath = join(threadDirPath, threadMetadataFileName)
if (!fs.existsSync(threadDirPath)) { if (!fs.existsSync(threadDirPath)) {
@ -189,7 +186,7 @@ export const updateThread = async (threadId: string, thread: any) => {
updated: Date.now(), updated: Date.now(),
} }
try { try {
const threadDirPath = join(path, 'threads', updatedThread.id) const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id)
const threadJsonPath = join(threadDirPath, threadMetadataFileName) const threadJsonPath = join(threadDirPath, threadMetadataFileName)
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2)) await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
@ -231,7 +228,7 @@ export const createMessage = async (threadId: string, message: any) => {
], ],
} }
const threadDirPath = join(path, 'threads', threadId) const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId)
const threadMessagePath = join(threadDirPath, threadMessagesFileName) const threadMessagePath = join(threadDirPath, threadMessagesFileName)
if (!fs.existsSync(threadDirPath)) { if (!fs.existsSync(threadDirPath)) {
@ -246,7 +243,12 @@ export const createMessage = async (threadId: string, message: any) => {
} }
} }
export const downloadModel = async (modelId: string) => { export const downloadModel = async (
modelId: string,
network?: { proxy?: string; ignoreSSL?: boolean }
) => {
const strictSSL = !network?.ignoreSSL
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId) const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
if (!model || model.object !== 'model') { if (!model || model.object !== 'model') {
return { return {
@ -254,7 +256,7 @@ export const downloadModel = async (modelId: string) => {
} }
} }
const directoryPath = join(path, 'models', modelId) const directoryPath = join(getJanDataFolderPath(), 'models', modelId)
if (!fs.existsSync(directoryPath)) { if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath) fs.mkdirSync(directoryPath)
} }
@ -263,7 +265,7 @@ export const downloadModel = async (modelId: string) => {
const modelBinaryPath = join(directoryPath, modelId) const modelBinaryPath = join(directoryPath, modelId)
const request = require('request') const request = require('request')
const rq = request(model.source_url) const rq = request({ url: model.source_url, strictSSL, proxy })
const progress = require('request-progress') const progress = require('request-progress')
progress(rq, {}) progress(rq, {})
.on('progress', function (state: any) { .on('progress', function (state: any) {
@ -314,7 +316,8 @@ export const chatCompletions = async (request: any, reply: any) => {
reply.raw.writeHead(200, { reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
Connection: 'keep-alive', 'Connection': 'keep-alive',
"Access-Control-Allow-Origin": "*"
}) })
const headers: Record<string, any> = { const headers: Record<string, any> = {
@ -345,7 +348,7 @@ const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai') { if (engineId !== 'openai') {
return undefined return undefined
} }
const directoryPath = join(path, 'engines') const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`) const filePath = join(directoryPath, `${engineId}.json`)
const data = await fs.readFileSync(filePath, 'utf-8') const data = await fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data) return JSON.parse(data)

View File

@ -27,7 +27,7 @@ export const commonRouter = async (app: HttpServer) => {
// Download Model Routes // Download Model Routes
app.get(`/models/download/:modelId`, async (request: any) => app.get(`/models/download/:modelId`, async (request: any) =>
downloadModel(request.params.modelId), downloadModel(request.params.modelId, { ignoreSSL: request.query.ignoreSSL === 'true', proxy: request.query.proxy }),
) )
// Chat Completion Routes // Chat Completion Routes

View File

@ -1,56 +1,58 @@
import { DownloadRoute } from '../../../api' import { DownloadRoute } from '../../../api'
import { join } from 'path' import { join } from 'path'
import { userSpacePath } from '../../extension/manager'
import { DownloadManager } from '../../download' import { DownloadManager } from '../../download'
import { HttpServer } from '../HttpServer' import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs' import { createWriteStream } from 'fs'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from "../../path";
export const downloadRouter = async (app: HttpServer) => { export const downloadRouter = async (app: HttpServer) => {
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => { app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
const body = JSON.parse(req.body as any) const strictSSL = !(req.query.ignoreSSL === "true");
const proxy = req.query.proxy?.startsWith("http") ? req.query.proxy : undefined;
const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => { const normalizedArgs = body.map((arg: any) => {
if (typeof arg === 'string' && arg.includes('file:/')) { if (typeof arg === "string") {
return join(userSpacePath, arg.replace('file:/', '')) return join(getJanDataFolderPath(), normalizeFilePath(arg));
} }
return arg return arg;
}) });
const localPath = normalizedArgs[1] const localPath = normalizedArgs[1];
const fileName = localPath.split('/').pop() ?? '' const fileName = localPath.split("/").pop() ?? "";
const request = require('request') const request = require("request");
const progress = require('request-progress') const progress = require("request-progress");
const rq = request(normalizedArgs[0]) const rq = request({ url: normalizedArgs[0], strictSSL, proxy });
progress(rq, {}) progress(rq, {})
.on('progress', function (state: any) { .on("progress", function (state: any) {
console.log('download onProgress', state) console.log("download onProgress", state);
}) })
.on('error', function (err: Error) { .on("error", function (err: Error) {
console.log('download onError', err) console.log("download onError", err);
}) })
.on('end', function () { .on("end", function () {
console.log('download onEnd') console.log("download onEnd");
}) })
.pipe(createWriteStream(normalizedArgs[1])) .pipe(createWriteStream(normalizedArgs[1]));
DownloadManager.instance.setRequest(fileName, rq) DownloadManager.instance.setRequest(fileName, rq);
}) });
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => { app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
const body = JSON.parse(req.body as any) const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => { const normalizedArgs = body.map((arg: any) => {
if (typeof arg === 'string' && arg.includes('file:/')) { if (typeof arg === "string") {
return join(userSpacePath, arg.replace('file:/', '')) return join(getJanDataFolderPath(), normalizeFilePath(arg));
} }
return arg return arg;
}) });
const localPath = normalizedArgs[0] const localPath = normalizedArgs[0];
const fileName = localPath.split('/').pop() ?? '' const fileName = localPath.split("/").pop() ?? "";
console.debug('fileName', fileName) const rq = DownloadManager.instance.networkRequests[fileName];
const rq = DownloadManager.instance.networkRequests[fileName] DownloadManager.instance.networkRequests[fileName] = undefined;
DownloadManager.instance.networkRequests[fileName] = undefined rq?.abort();
rq?.abort() });
}) };
}

View File

@ -1,20 +1,20 @@
import { join, extname } from 'path' import { join, extname } from 'path'
import { ExtensionRoute } from '../../../api/index' import { ExtensionRoute } from '../../../api/index'
import { userSpacePath } from '../../extension/manager'
import { ModuleManager } from '../../module' import { ModuleManager } from '../../module'
import { getActiveExtensions, installExtensions } from '../../extension/store' import { getActiveExtensions, installExtensions } from '../../extension/store'
import { HttpServer } from '../HttpServer' import { HttpServer } from '../HttpServer'
import { readdirSync } from 'fs' import { readdirSync } from 'fs'
import { getJanExtensionsPath } from '../../utils'
export const extensionRouter = async (app: HttpServer) => { export const extensionRouter = async (app: HttpServer) => {
// TODO: Share code between node projects // TODO: Share code between node projects
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => { app.post(`/${ExtensionRoute.getActiveExtensions}`, async (_req, res) => {
const activeExtensions = await getActiveExtensions() const activeExtensions = await getActiveExtensions()
res.status(200).send(activeExtensions) res.status(200).send(activeExtensions)
}) })
app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => { app.post(`/${ExtensionRoute.baseExtensions}`, async (_req, res) => {
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install') const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
const extensions = readdirSync(baseExtensionPath) const extensions = readdirSync(baseExtensionPath)
.filter((file) => extname(file) === '.tgz') .filter((file) => extname(file) === '.tgz')
@ -23,7 +23,7 @@ export const extensionRouter = async (app: HttpServer) => {
res.status(200).send(extensions) res.status(200).send(extensions)
}) })
app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => { app.post(`/${ExtensionRoute.installExtension}`, async (req) => {
const extensions = req.body as any const extensions = req.body as any
const installed = await installExtensions(JSON.parse(extensions)[0]) const installed = await installExtensions(JSON.parse(extensions)[0])
return JSON.parse(JSON.stringify(installed)) return JSON.parse(JSON.stringify(installed))
@ -32,7 +32,7 @@ export const extensionRouter = async (app: HttpServer) => {
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => { app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
const args = JSON.parse(req.body as any) const args = JSON.parse(req.body as any)
console.debug(args) console.debug(args)
const module = await import(join(userSpacePath, 'extensions', args[0])) const module = await import(join(getJanExtensionsPath(), args[0]))
ModuleManager.instance.setModule(args[0], module) ModuleManager.instance.setModule(args[0], module)
const method = args[1] const method = args[1]

View File

@ -4,7 +4,7 @@ import { HttpServer } from '../../index'
export const fsRouter = async (app: HttpServer) => { export const fsRouter = async (app: HttpServer) => {
app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {}) app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
app.post(`/app/${FileManagerRoute.getUserSpace}`, async (request: any, reply: any) => {}) app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {})
app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {}) app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})

View File

@ -1,7 +1,7 @@
import { FileSystemRoute } from '../../../api' import { FileSystemRoute } from '../../../api'
import { join } from 'path' import { join } from 'path'
import { HttpServer } from '../HttpServer' import { HttpServer } from '../HttpServer'
import { userSpacePath } from '../../extension/manager' import { getJanDataFolderPath } from '../../utils'
export const fsRouter = async (app: HttpServer) => { export const fsRouter = async (app: HttpServer) => {
const moduleName = 'fs' const moduleName = 'fs'
@ -14,7 +14,7 @@ export const fsRouter = async (app: HttpServer) => {
return mdl[route]( return mdl[route](
...body.map((arg: any) => ...body.map((arg: any) =>
typeof arg === 'string' && arg.includes('file:/') typeof arg === 'string' && arg.includes('file:/')
? join(userSpacePath, arg.replace('file:/', '')) ? join(getJanDataFolderPath(), arg.replace('file:/', ''))
: arg, : arg,
), ),
) )

View File

@ -103,7 +103,7 @@ export default class Extension {
const pacote = await import('pacote') const pacote = await import('pacote')
await pacote.extract( await pacote.extract(
this.specifier, this.specifier,
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
this.installOptions, this.installOptions,
) )
@ -166,9 +166,9 @@ export default class Extension {
* @returns the latest available version if a new version is available or false if not. * @returns the latest available version if a new version is available or false if not.
*/ */
async isUpdateAvailable() { async isUpdateAvailable() {
return import('pacote').then((pacote) => { return import('pacote').then((pacote) => {
if (this.origin) { if (this.origin) {
return pacote.manifest(this.origin).then((mnf) => { return pacote.manifest(this.origin).then((mnf) => {
return mnf.version !== this.version ? mnf.version : false return mnf.version !== this.version ? mnf.version : false
}) })
} }
@ -179,8 +179,9 @@ export default class Extension {
* Remove extension and refresh renderers. * Remove extension and refresh renderers.
* @returns {Promise} * @returns {Promise}
*/ */
async uninstall() { async uninstall(): Promise<void> {
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '') const path = ExtensionManager.instance.getExtensionsPath()
const extPath = resolve(path ?? '', this.name ?? '')
await rmdirSync(extPath, { recursive: true }) await rmdirSync(extPath, { recursive: true })
this.emitUpdate() this.emitUpdate()

View File

@ -35,17 +35,17 @@ async function registerExtensionProtocol() {
let electron: any = undefined let electron: any = undefined
try { try {
const moduleName = "electron" const moduleName = 'electron'
electron = await import(moduleName) electron = await import(moduleName)
} catch (err) { } catch (err) {
console.error('Electron is not available') console.error('Electron is not available')
} }
const extensionPath = ExtensionManager.instance.getExtensionsPath()
if (electron) { if (electron) {
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => { return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
const entry = request.url.substr('extension://'.length - 1) const entry = request.url.substr('extension://'.length - 1)
const url = normalize(ExtensionManager.instance.extensionsPath + entry) const url = normalize(extensionPath + entry)
callback({ path: url }) callback({ path: url })
}) })
} }
@ -120,7 +120,7 @@ function loadExtension(ext: any) {
* @returns {extensionManager} A set of functions used to manage the extension lifecycle. * @returns {extensionManager} A set of functions used to manage the extension lifecycle.
*/ */
export function getStore() { export function getStore() {
if (!ExtensionManager.instance.extensionsPath) { if (!ExtensionManager.instance.getExtensionsFile()) {
throw new Error( throw new Error(
'The extension path has not yet been set up. Please run useExtensions before accessing the store', 'The extension path has not yet been set up. Please run useExtensions before accessing the store',
) )
@ -133,4 +133,4 @@ export function getStore() {
getActiveExtensions, getActiveExtensions,
removeExtension, removeExtension,
} }
} }

View File

@ -1,44 +1,45 @@
import { join, resolve } from "path"; import { join, resolve } from 'path'
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { homedir } from "os"
/** /**
* Manages extension installation and migration. * Manages extension installation and migration.
*/ */
export const userSpacePath = join(homedir(), "jan");
export class ExtensionManager { export class ExtensionManager {
public static instance: ExtensionManager = new ExtensionManager(); public static instance: ExtensionManager = new ExtensionManager()
extensionsPath: string | undefined = join(userSpacePath, "extensions"); private extensionsPath: string | undefined
constructor() { constructor() {
if (ExtensionManager.instance) { if (ExtensionManager.instance) {
return ExtensionManager.instance; return ExtensionManager.instance
} }
} }
getExtensionsPath(): string | undefined {
return this.extensionsPath
}
setExtensionsPath(extPath: string) { setExtensionsPath(extPath: string) {
// Create folder if it does not exist // Create folder if it does not exist
let extDir; let extDir
try { try {
extDir = resolve(extPath); extDir = resolve(extPath)
if (extDir.length < 2) throw new Error(); if (extDir.length < 2) throw new Error()
if (!existsSync(extDir)) mkdirSync(extDir); if (!existsSync(extDir)) mkdirSync(extDir)
const extensionsJson = join(extDir, "extensions.json"); const extensionsJson = join(extDir, 'extensions.json')
if (!existsSync(extensionsJson)) if (!existsSync(extensionsJson)) writeFileSync(extensionsJson, '{}')
writeFileSync(extensionsJson, "{}");
this.extensionsPath = extDir; this.extensionsPath = extDir
} catch (error) { } catch (error) {
throw new Error("Invalid path provided to the extensions folder"); throw new Error('Invalid path provided to the extensions folder')
} }
} }
getExtensionsFile() { getExtensionsFile() {
return join(this.extensionsPath ?? "", "extensions.json"); return join(this.extensionsPath ?? '', 'extensions.json')
} }
} }

View File

@ -6,3 +6,5 @@ export * from './download'
export * from './module' export * from './module'
export * from './api' export * from './api'
export * from './log' export * from './log'
export * from './utils'
export * from './path'

View File

@ -1,22 +1,35 @@
import fs from 'fs' import fs from 'fs'
import util from 'util' import util from 'util'
import path from 'path' import { getAppLogPath, getServerLogPath } from './utils'
import os from 'os'
export const logDir = path.join(os.homedir(), 'jan', 'logs') export const log = function (message: string) {
const appLogPath = getAppLogPath()
export const log = function (message: string, fileName: string = 'app.log') {
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
if (!message.startsWith('[')) { if (!message.startsWith('[')) {
message = `[APP]::${message}` message = `[APP]::${message}`
} }
message = `${new Date().toISOString()} ${message}` message = `${new Date().toISOString()} ${message}`
if (fs.existsSync(logDir)) { if (fs.existsSync(appLogPath)) {
var log_file = fs.createWriteStream(path.join(logDir, fileName), { var log_file = fs.createWriteStream(appLogPath, {
flags: 'a',
})
log_file.write(util.format(message) + '\n')
log_file.close()
console.debug(message)
}
}
export const logServer = function (message: string) {
const serverLogPath = getServerLogPath()
if (!message.startsWith('[')) {
message = `[SERVER]::${message}`
}
message = `${new Date().toISOString()} ${message}`
if (fs.existsSync(serverLogPath)) {
var log_file = fs.createWriteStream(serverLogPath, {
flags: 'a', flags: 'a',
}) })
log_file.write(util.format(message) + '\n') log_file.write(util.format(message) + '\n')

9
core/src/node/path.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* Normalize file path
* Remove all file protocol prefix
* @param path
* @returns
*/
export function normalizeFilePath(path: string): string {
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2");
}

View File

@ -0,0 +1,103 @@
import { AppConfiguration } from "../../types";
import { join } from "path";
import fs from "fs";
import os from "os";
// TODO: move this to core
const configurationFileName = "settings.json";
// TODO: do no specify app name in framework module
const defaultJanDataFolder = join(os.homedir(), "jan");
const defaultAppConfig: AppConfiguration = {
data_folder: defaultJanDataFolder,
};
/**
* Getting App Configurations.
*
* @returns {AppConfiguration} The app configurations.
*/
export const getAppConfigurations = (): AppConfiguration => {
// Retrieve Application Support folder path
// Fallback to user home directory if not found
const configurationFile = getConfigurationFilePath();
if (!fs.existsSync(configurationFile)) {
// create default app config if we don't have one
console.debug(`App config not found, creating default config at ${configurationFile}`);
fs.writeFileSync(configurationFile, JSON.stringify(defaultAppConfig));
return defaultAppConfig;
}
try {
const appConfigurations: AppConfiguration = JSON.parse(
fs.readFileSync(configurationFile, "utf-8"),
);
return appConfigurations;
} catch (err) {
console.error(`Failed to read app config, return default config instead! Err: ${err}`);
return defaultAppConfig;
}
};
const getConfigurationFilePath = () =>
join(
global.core?.appPath() || process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"],
configurationFileName,
);
export const updateAppConfiguration = (configuration: AppConfiguration): Promise<void> => {
const configurationFile = getConfigurationFilePath();
console.debug("updateAppConfiguration, configurationFile: ", configurationFile);
fs.writeFileSync(configurationFile, JSON.stringify(configuration));
return Promise.resolve();
};
/**
* Utility function to get server log path
*
* @returns {string} The log path.
*/
export const getServerLogPath = (): string => {
const appConfigurations = getAppConfigurations();
const logFolderPath = join(appConfigurations.data_folder, "logs");
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true });
}
return join(logFolderPath, "server.log");
};
/**
* Utility function to get app log path
*
* @returns {string} The log path.
*/
export const getAppLogPath = (): string => {
const appConfigurations = getAppConfigurations();
const logFolderPath = join(appConfigurations.data_folder, "logs");
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true });
}
return join(logFolderPath, "app.log");
};
/**
* Utility function to get data folder path
*
* @returns {string} The data folder path.
*/
export const getJanDataFolderPath = (): string => {
const appConfigurations = getAppConfigurations();
return appConfigurations.data_folder;
};
/**
* Utility function to get extension path
*
* @returns {string} The extensions path.
*/
export const getJanExtensionsPath = (): string => {
const appConfigurations = getAppConfigurations();
return join(appConfigurations.data_folder, "extensions");
};

View File

@ -0,0 +1,3 @@
export type AppConfiguration = {
data_folder: string
}

View File

@ -0,0 +1 @@
export * from './appConfigEntity'

View File

@ -5,3 +5,4 @@ export * from './message'
export * from './inference' export * from './inference'
export * from './monitoring' export * from './monitoring'
export * from './file' export * from './file'
export * from './config'

View File

@ -104,6 +104,9 @@ export type ModelSettingParams = {
n_parallel?: number n_parallel?: number
cpu_threads?: number cpu_threads?: number
prompt_template?: string prompt_template?: string
system_prompt?: string
ai_prompt?: string
user_prompt?: string
} }
/** /**

View File

@ -7,9 +7,10 @@ export interface ModelInterface {
/** /**
* Downloads a model. * Downloads a model.
* @param model - The model to download. * @param model - The model to download.
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
* @returns A Promise that resolves when the model has been downloaded. * @returns A Promise that resolves when the model has been downloaded.
*/ */
downloadModel(model: Model): Promise<void> downloadModel(model: Model, network?: { ignoreSSL?: boolean, proxy?: string }): Promise<void>
/** /**
* Cancels the download of a specific model. * Cancels the download of a specific model.

View File

@ -0,0 +1,12 @@
import { normalizeFilePath } from "../../src/node/path";
describe("Test file normalize", () => {
test("returns no file protocol prefix on Unix", async () => {
expect(normalizeFilePath("file://test.txt")).toBe("test.txt");
expect(normalizeFilePath("file:/test.txt")).toBe("test.txt");
});
test("returns no file protocol prefix on Windows", async () => {
expect(normalizeFilePath("file:\\\\test.txt")).toBe("test.txt");
expect(normalizeFilePath("file:\\test.txt")).toBe("test.txt");
});
});

View File

@ -13,7 +13,7 @@
"declarationDir": "dist/types", "declarationDir": "dist/types",
"outDir": "dist/lib", "outDir": "dist/lib",
"importHelpers": true, "importHelpers": true,
"typeRoots": ["node_modules/@types"] "types": ["@types/jest"]
}, },
"include": ["src"] "include": ["src"]
} }

View File

@ -1,98 +0,0 @@
---
title: Cultivating Culture in Open Source Contributions Beyond Code
description: Discover how to contribute to open-source projects without coding skills. This Jan post guides you through the essentials of non-code contributions, from documentation to community engagement. Learn practical ways to impact open-source initiatives, even if you're new to the field.
slug: contributions
keywords:
[
Jan framework,
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
how to contribute to opensource,
open source,
contributing to open source,
]
authors:
- name: Jan Team
tags: [contribution, jan]
hide_table_of_contents: false
---
Ever wondered what contribution really means in open-source communities? Calling back to John Picozzis presentation at DrupalCon 2022 and his article on [Opensource.com,](https://opensource.com/article/22/8/non-code-contribution-powers-open-source) we look at how his views on contributions align with ours at [Jan](https://jan.ai)
![how to contribute opensource non-code](./assets/01-how-to-contribute-to-open-source-non-code.png)
[https://jan.ai/about/#open-source](https://jan.ai/about/#open-source)
#### Broadening the Definition of Contribution - how it benefits everyone
Contribution in open source isnt just about coding, and Picozzi nails it with this point.
At Jan, were all about embracing different skillsbecause thats what fuels innovation and growth.
> Contribution? Its when you take time to push forward ideas, vision, and awareness. Here are some solid ideas and how theyve been executed before.
#### Documentation - One Pillar of Contribution
Remember those ancient storytellers and their crucial role in history? Thats what modern documentation does in tech. Its the backbone of knowledge transfer. Without it, wed be lost in translating what our products are all about.
![](./assets/01-credit-janhq-repo.png)credit- Janhq/jan repo
#### Writing and Editing
Got a knack for words? Awesome! Making complex stuff easy and friendly is a real game-changer. Search around and see if your favourite open-source project has a tone of voice guide or style guide. (Were currently making one!)
#### Translating
Speak another language? You could jump in and help translate documents. Youll be flinging open doors for heaps of non-native language speakers and will scale awareness and adoption in a pretty big way.
#### Creating Tutorials and Guides
Projects thrive on collective knowledge being shared. Maybe your way of contributing is helping newbies find their feet by showcasing how you use the product?
### Spreading the Word- Storytelling and Marketing
In the OS world, marketing is more about storytelling and community building than selling.
Heres what can make an impact that isnt code-
- Social media, blogs, forums- When the folks running open source projects are knee-deep in code, spreading the word might slip their minds. Your tweets, blogs, and comments? Theyre more impactful than you think.
* Content creation -If youre into making videos or infographics, your skills can tell the projects story in ways that really stick.
#### Event Planning and Meetups
Hosting online meetups or webinars? Its a great way to share knowledge, connect people, and gather feedback as to how people are connecting with the project.
### Contributing via Dot Connecting
Know people who might be interested or can chip in? Bringing in new energy and ideas is what keeps open source buzzing. Spot an opportunity for a partnership? Go for it!
Linking up with educational bodies, non-profits, or businesses can work wonders depending on the projects end goals.
### Why People Contribute to Open Source
As Picozzi points out, its all about embracing every kind of skill. At Jan, we celebrate each team members unique contributions.
Its about being part of something bigger, honing new skills, and feeling that sense of belonging.
#### What the Company/Project Gets Out of It
By valuing non-code contributions, open-source projects, and companies tap into a broader skill set, leading to more well-rounded development and problem-solving. A diverse community is key to Jans success, just like Picozzi emphasizes.
### Challenges in Non-Code Contributing
Imposter syndrome? We get it, and were tackling it by creating a welcoming space for all contributions. Jans big on balancing work, life, and contributionintegrating it into our culture without overwhelming the team.
#### How Do You Start Contributing to OpenSource?
Just like Nikes “just do it”, were all about making it easy for our team to dive into areas theyre passionate about. Heres our [labelled issues for contributors](https://github.com/janhq/jan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
#### Heres the [Jan readme](https://github.com/janhq/jan?tab=readme-ov-file#readme)
#### [Join us on Discord](https://discord.gg/JPcNRaJyzJ)

1
docs/blog/README.md Normal file
View File

@ -0,0 +1 @@
# TODO

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

View File

@ -1,5 +1,6 @@
--- ---
title: About Jan title: About Jan
slug: /about
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords: keywords:
[ [

View File

@ -1,6 +1,7 @@
--- ---
title: Onboarding title: Onboarding
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /onboarding
keywords: keywords:
[ [
Jan AI, Jan AI,
@ -14,6 +15,8 @@ keywords:
] ]
--- ---
# Onboarding
Welcome to Jan! Were really excited to bring you onboard. Welcome to Jan! Were really excited to bring you onboard.
## Expectations ## Expectations

View File

@ -0,0 +1,10 @@
---
title: CI & CD
slug: /engineering/ci-cd
---
## Gitflow
Previously we were trunk based. Now we use the following Gitflow:
TODO: @van to include her Mermaid diagram

View File

@ -0,0 +1,80 @@
---
title: QA
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /engineering/qa
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
]
---
### Phase 1: Planning
#### Definition of Ready (DoR):
- [ ] **Scope Defined:** The features to be implemented are clearly defined and scoped out.
- [ ] **Requirements Gathered:** Gather and document all the necessary requirements for the feature.
- [ ] **Stakeholder Input:** Ensure relevant stakeholders have provided input on the document scope and content.
#### Definition of Done (DoD):
- [ ] **Document Complete:** All sections of the document are filled out with relevant information.
- [ ] **Reviewed by Stakeholders:** The document has been reviewed and approved by stakeholders.
- [ ] **Ready for Development:** The document is in a state where developers can use it to begin implementation.
### Phase 2: Development
#### Definition of Ready (DoR):
- [ ] **Task Breakdown:** The development team has broken down tasks based on the document.
- [ ] **Communication Plan:** A plan is in place for communication between developers and writers if clarification is needed during implementation.
- [ ] **Developer Understanding:** Developers have a clear understanding of the document content.
#### Definition of Done (DoD):
- [ ] **Code Implementation:** The feature is implemented according to the document specifications.
- [ ] **Developer Testing:**
- Unit tests and basic integration tests are completed
- Developer also completed self-testing for the feature (please add this as a comment in the ticket, with the tested OS and as much info as possible to reduce overlaping effort).
- (AC -> Code Changes -> Impacted scenarios)
- [ ] **Communication with Writers:** Developers have communicated any changes or challenges to the writers, and necessary adjustments are made in the document. (Can be through a note in the PR of the feature for writers to take care, or create a separate PR with the change you made for the docs, for writers to review)
### Phase 3: QA for feature
#### Definition of Ready (DoR):
- [ ] **Test Note Defined:** The test note is prepared outlining the testing items.
- [ ] **Environment Ready:** PR merged to nightly build, Nightly build notes updated (automatically from pipeline after merged).
- [ ] **Status:** Ticket moved to the column Testing and assigning to QA/writers to review.
- [ ] **Test Data Prepared:** Relevant test data is prepared for testing the scenarios.
#### Definition of Done (DoD):
- [ ] **Test Executed:** All identified test items are executed on different OS, along with exploratory testing.
- [ ] **Defects Logged:** Any defects found during testing are resolved / appropriately logged (and approved for future fix).
- [ ] **Test Sign-Off:** QA team provides sign-off indicating the completion of testing.
### Phase 4: Release (DoR)
- [ ] **Pre-release wait time:** Code change to pre-release version should be frozen for at least X (hrs/days) for Regression testing purpose.
- Pre-release cut off on Thu morning for the team to regression test.
- Release to production (Stable) during working hour on Mon morning (if no blocker) or Tue morning.
- During the release cut off, the nightly build will be paused, to leave room for pre-release build. The build version used for regression test will be notified.
- [ ] **Pre-release testing:** A review of the implemented feature has been conducted, a long with regression test (check-list) by the team.
- Release checklist cloned from the templat for different OS (with hackMD link)
- New key test items from new feature added to the checklist.
- Split 3 OS to different team members for testing.
- [ ] **Document Updated:** The document is updated based on the review and feedback on any discrepancies or modification needed for this release.
- [ ] **Reviewed by Stakeholders:** New feature and the updated document is reviewed and approved by stakeholders. The document is in its final version, reflecting the implemented feature accurately.
### Notes (WIP)
- [ ] **API collection run:** to run along with nightly build daily, for critical API validation
- [ ] **Automation run:** for regression testing purpose, to reduce manual testing effort for the same items each release on multiple OS.

View File

@ -1,6 +1,7 @@
--- ---
title: Engineering title: MLOps
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /engineering/mlops
keywords: keywords:
[ [
Jan AI, Jan AI,
@ -16,6 +17,8 @@ keywords:
## Connecting to Rigs ## Connecting to Rigs
We have a small data rig you can remote into for R&D and CI.
### Pritunl Setup ### Pritunl Setup
1. **Install Pritunl**: [Download here](https://client.pritunl.com/#install) 1. **Install Pritunl**: [Download here](https://client.pritunl.com/#install)
@ -137,7 +140,7 @@ cd examples/llama && rm -rf ./llama/7B && mkdir -p ./llama/7B && git clone https
python build.py --model_dir ./llama/7B/ --dtype float16 --remove_input_padding --use_gpt_attention_plugin float16 --enable_context_fmha --use_gemm_plugin float16 --use_weight_only --output_dir ./llama/7B/trt_engines/weight_only/1-gpu/ python build.py --model_dir ./llama/7B/ --dtype float16 --remove_input_padding --use_gpt_attention_plugin float16 --enable_context_fmha --use_gemm_plugin float16 --use_weight_only --output_dir ./llama/7B/trt_engines/weight_only/1-gpu/
``` ```
4. Run Inference: 4. **Run Inference:**
```bash ```bash
python3 run.py --max_output_len=2048 --tokenizer_dir ./llama/7B/ --engine_dir=./llama/7B/trt_engines/weight_only/1-gpu/ --input_text "Writing a thesis proposal can be done in 10 simple steps:\nStep 1:" python3 run.py --max_output_len=2048 --tokenizer_dir ./llama/7B/ --engine_dir=./llama/7B/trt_engines/weight_only/1-gpu/ --input_text "Writing a thesis proposal can be done in 10 simple steps:\nStep 1:"

View File

@ -1,7 +1,7 @@
--- ---
title: Overview title: R&D
slug: /handbook
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /engineering/research
keywords: keywords:
[ [
Jan AI, Jan AI,
@ -15,4 +15,6 @@ keywords:
] ]
--- ---
Welcome to Jan Handbook! Were really excited to bring you onboard. ## Foundry Best Practices
@alan/rex TODO

View File

@ -0,0 +1,136 @@
---
title: |
10/1/24: Bitdefender False Positive Flag (Resolved)
slug: /postmortems/january-10-2024-bitdefender-false-positive-flag
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
postmortem,
incident,
flagging issue,
]
---
Following the recent incident related to Jan version 0.4.4 triggering Bitdefender on Windows with Gen:Variant.Tedy.258323 on January 10, 2024, we wanted to provide a comprehensive postmortem and outline the necessary follow-up actions.
## Incident Overview
### Bug Description
Jan 0.4.4 installation on Windows triggered Bitdefender to flag it as infected with Gen:Variant.Tedy.258323, leading to automatic quarantine.
### Affected Antivirus
- McAfee / Microsoft Defender was unaffected
- Bitdefender consistently flagged the issue.
### Incident Timeline
- _10 Jan, 2:18 am SGT:_ Hawke flags up Malware antivirus errors for 0.4.4 installation on Windows computers.
- _10 Jan, 2:21 am SGT:_ @0xSage responds in Discord.
- _10 Jan, 2:35 am SGT:_ Hawke confirms multiple people have experienced this error on fresh installs.
- _10 Jan, 2:41 am SGT:_ @louis-jan and @dan-jan revert 0.4.4 out of an abundance of caution.
- _Incident ongoing:_ To triage and investigate the next day.
- _10 Jan, 11:36 am SGT:_ @Hien has investigated all versions of Nitro and conducted scans using Bitdefender. Only the 2 latest versions raised warnings (0.2.7, 0.2.8).
- _10 Jan, 12:44 pm SGT:_ @Hien tested again for the 0.2.6 and suggested using 0.2.6 for now, the 2 remaining Nitro version (0.2.7, 0.2.8) will under further investigation.
- The team started testing on the fixed build.
- _10 Jan, 3:22 pm SGT:_ Diagnosis found that it's most likely a false positive. @Hien has only found a solution by attempting to build Nitro Windows CPU on a GitHub-hosted runner and hasn't identified the root cause yet.
- _10 Jan, 5:24 pm SGT:_ @Hien testing two scenarios and still trying to understand the workings of Bitdefender.
- _11 Jan, 5:46 pm SGT:_ Postmortem meeting
## Investigation Update
- @Hien has investigated all versions of Nitro and conducted scans using Bitdefender. and only the 2 latest versions raised warnings from Bitdefender. Nitro 0.2.6, which is the highest version without the issue, was tested again, and it no longer triggers a warning from Bitdefender.
- We have observed that Nitro versions up to 0.2.6 remain unaffected. However, Bitdefender flags versions 0.2.7 and 0.2.8 as infected, leading to the deletion. In order to proceed with the current release, Hien suggests downgrading Nitro to version 0.2.6 and conducting tests with this version. Simultaneously, he will investigate why Bitdefender is flagging versions 0.2.7 and 0.2.8.
- It's essential to note that between versions 0.2.6, 0.2.7, and 0.2.8, only minor changes were made, which should not trigger a malicious code warning. We can refer to the changelog between 0.2.7 and 0.2.8 to pinpoint these changes.
- Our primary message is to convey that we did not introduce malicious code into Jan (indicating a false positive), and the investigation aims to understand the root cause behind Bitdefender flagging versions 0.2.7 and 0.2.8.
- The current diagnosis looks like a false positive but it's still under investigation. Reference link: [here](https://stackoverflow.com/questions/75886428/fake-positive-bit-defender-problem-genvariant-tedy-304469), [here](https://stackoverflow.com/questions/58010466/bitdefender-detects-my-console-application-as-genvariant-ursu-56053), and [here](https://www.cisa.gov/sites/default/files/2023-06/mar-10365227.r1.v1.clear_.pdf).
- @Hien testing two scenarios and still trying to understand the workings of Bitdefender. Still under investigation: is the issue with the code or the CI?
- In Case 1, using the same CI agent for tags 0.2.6 and 0.2.8, after PRs by Alan and myself, Bitdefender flagged the Nitro CPU binary build. Naturally, one would conclude this is due to the code.
- However, I proceeded with a further experiment: for the 0.2.8 code, instead of using our CI agent, I used a GitHub hosted agent. This time, Bitdefender did not flag our binary build.
- We've identified the Bitdefender warning was not an attack. There is no malicious code
- We've isolated the event to originate from a CI agent, which resulted in a BitDefender false positive alert.
## Follow-ups and Action Items
1. **Reproduce Bitdefender Flag in Controlled Environment [Done]:**
- _Objective:_ To replicate the issue in a controlled environment to understand the triggers and specifics of Bitdefender's detection.
2. **Investigate Malicious Code or False Positive:**
- _Objective:_ Determine whether the flagged issue is a result of actual malicious code or a false positive. If it's a false positive, work towards resolution while communicating with Bitdefender.
3. **Supply Chain Attack Assessment:**
- _Objective:_ Evaluate the possibility of a supply chain attack. Investigate whether the Nitro 0.4.4 distribution was compromised or tampered with during the release process.
4. **Testing after the Hotfix:**
- _Objective:_ In addition to verifying the issue after the fix, it is essential to conduct comprehensive testing across related areas, ensuring compatibility across different operating systems and antivirus software (latest version / free version only).
5. **Process Improvement for Future Releases:**
- _Objective:_ Identify and implement improvements to our release process to prevent similar incidents in the future. This may include enhanced testing procedures, code analysis, and collaboration with antivirus software providers during the pre-release phase. Additionally, we should add verifying the latest antivirus software in the release checklist.
6. **Documentation of Tested Antivirus Versions:**
- _Objective:_ Create a document that outlines the testing conducted, including a matrix that correlates Jan versions with the tested antivirus versions.
- _Sample list:_ for consideration purpose
- Bitdefender
- McAfee
- Avira
- Kaspersky
- Norton
- Microsoft defender
- AVG
- TotalAV
## Next Steps
- The team should follow up on each action item with clear ownership priority, and deadlines.
- Communicate progress transparently with the community and clients through appropriate channels. If any insights or suggestions, share them within the dedicated channels.
- Update internal documentation and procedures based on the lessons learned from this incident.
## Lessons Learned
1. **Antivirus Compatibility Awareness:**
- _Observation:_ The incident underscored the significance of recognizing and testing for antivirus compatibility, particularly with widely-used solutions like Bitdefender.
- _Lesson Learned:_ In the future, we will integrate comprehensive checks for compatibility with various antivirus software, including both antivirus and "Malicious Code Detection," into our CI or QA checklist. This proactive measure aims to minimize false positive detections during the release and testing processes.
2. **Cross-Platform Testing:**
- _Observation:_ The problem did not occur on MacOS and Linux systems, implying a potential oversight in cross-platform testing during our release procedures.
- _Lesson Learned:_ Clarification — This observation is not directly related to antivirus testing. Instead, it underscores the necessity to improve our testing protocols, encompassing multiple operating systems. This ensures a thorough evaluation of potential issues on diverse platforms, considering the various antivirus software and differences in architectures on Mac and Linux systems.
3. **User Communication and Documentation:**
- _Observation:_ Due to the timely response from Nicole, who was still active on Discord and Github at 2 am, this quick response facilitated our ability to assess the impact accurately.
- _Lesson Learned:_ While our communication with users was effective in this instance, it was mainly due to Nicole's presence during the incident. To improve our overall response capability, we should prioritize "24/7 rapid triage and response." This involves ensuring continuous availability or establishing a reliable rotation of team members for swift user communication and issue documentation, further enhancing our incident response efficiency.
4. **Proactive Incident Response:**
- _Observation:_ The incident response, while involving a prompt version rollback, experienced a slight delay due to the release occurring at midnight. This delay postponed the initiation of the investigation until the next working hours.
- _Lesson Learned:_ Recognizing the importance of swift incident response, particularly in time-sensitive situations, we acknowledge that releasing updates during off-hours can impact the immediacy of our actions. Moving forward, we will strive to optimize our release schedules to minimize delays and ensure that investigations can commence promptly regardless of the time of day. This may involve considering alternative release windows or implementing automated responses to critical incidents, ensuring a more proactive and timely resolution.
5. **Supply Chain Security Measures:**
- _Observation:_ While the incident prompted consideration of a potential supply chain attack, it's crucial to emphasize that this was not the case. Nonetheless, the incident underscored the importance of reviewing our supply chain security measures.
- _Lesson Learned:_ Going forward, we should strengthen supply chain security by introducing additional verification steps to uphold the integrity of our release process. Collaborating with distribution channels is essential for enhancing security checks and ensuring a robust supply chain.
- _Longer-term:_ Exploring options for checking Jan for malicious code and incorporating antivirus as part of our CI/CD pipeline should be considered for a more comprehensive and proactive approach.
6. **User Education on False Positives:**
- _Observation:_ Users reported Bitdefender automatically "disinfecting" the flagged Nitro version without allowing any user actions.
- _Lesson Learned:_ Educate users about the possibility of false positives and guide them on how to whitelist or report such incidents to their antivirus provider (if possible). Provide clear communication on steps users can take in such situations.
These lessons learned will serve as a foundation for refining our processes and ensuring a more resilient release and incident response framework in the future. Continuous improvement is key to maintaining the reliability and security of our software.
Thank you for your dedication and cooperation in resolving this matter promptly.

View File

@ -1,7 +1,7 @@
--- ---
title: What We Do title: Postmortems
slug: /handbook/what-we-do
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /postmortems
keywords: keywords:
[ [
Jan AI, Jan AI,
@ -12,10 +12,9 @@ keywords:
conversational AI, conversational AI,
no-subscription fee, no-subscription fee,
large language model, large language model,
handbook,
] ]
--- ---
import DocCardList from "@theme/DocCardList"; import DocCardList from "@theme/DocCardList";
<DocCardList className="DocCardList--no-description" /> <DocCardList />

View File

@ -1,7 +1,7 @@
--- ---
title: Our Products and Innovations title: Engineering
slug: /handbook/products-and-innovations
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /engineering
keywords: keywords:
[ [
Jan AI, Jan AI,
@ -12,10 +12,10 @@ keywords:
conversational AI, conversational AI,
no-subscription fee, no-subscription fee,
large language model, large language model,
handbook,
] ]
--- ---
import DocCardList from "@theme/DocCardList"; ## Prerequisites
<DocCardList className="DocCardList--no-description" /> - [Requirements](https://github.com/janhq/jan?tab=readme-ov-file#requirements-for-running-jan)
- [Setting up local env](https://github.com/janhq/jan?tab=readme-ov-file#contributing)

View File

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -1,8 +1,9 @@
--- ---
title: Product title: Project Management
slug: /product/management
--- ---
We use the [Jan Monorepo Project](https://github.com/orgs/janhq/projects/5) in Github for 100% of our product / project management. We use the [Jan Monorepo Project](https://github.com/orgs/janhq/projects/5) in Github to manage our roadmap and sprint Kanbans.
As much as possible, everyone owns their respective `epics` and `tasks`. As much as possible, everyone owns their respective `epics` and `tasks`.

View File

@ -0,0 +1,9 @@
---
title: Product
slug: /product
---
## Prerequisites
- [Figma](https://figma.com)
- [ScreenStudio](https://www.screen.studio/)

View File

@ -0,0 +1,31 @@
---
title: Community
slug: /community
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
]
---
## Socials
- [Discord](https://discord.gg/SH3DGmUs6b)
- [X](https://twitter.com/janframework)
- [HuggingFace](https://huggingface.co/janhq)
- [LinkedIn](https://www.linkedin.com/company/janframework/)
## Community Run
- [Reddit](https://www.reddit.com/r/janframework/)
## Careers
- [Jobs](https://janai.bamboohr.com/careers)

View File

@ -1,6 +1,6 @@
--- ---
title: Meet Jan title: Events
slug: /handbook/meet-jan slug: /events
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords: keywords:
[ [
@ -12,10 +12,9 @@ keywords:
conversational AI, conversational AI,
no-subscription fee, no-subscription fee,
large language model, large language model,
handbook,
] ]
--- ---
import DocCardList from "@theme/DocCardList"; import DocCardList from "@theme/DocCardList";
<DocCardList className="DocCardList--no-description" /> <DocCardList />

View File

@ -1,6 +1,6 @@
--- ---
title: Our Contributors title: Careers
slug: /handbook/core-contributors slug: /careers
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords: keywords:
[ [
@ -12,10 +12,9 @@ keywords:
conversational AI, conversational AI,
no-subscription fee, no-subscription fee,
large language model, large language model,
handbook,
] ]
--- ---
import DocCardList from "@theme/DocCardList"; ## We're hiring
<DocCardList className="DocCardList--no-description" /> [Careers on Bamboo](https://janai.bamboohr.com/careers)

View File

@ -1,7 +0,0 @@
---
title: Community
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords: [Jan AI, Jan, ChatGPT alternative, local AI, private AI, conversational AI, no-subscription fee, large language model ]
---
- [ ] Social media links

View File

@ -20,3 +20,69 @@ keywords:
:::caution :::caution
This is currently under development. This is currently under development.
::: :::
In this guide, we'll walk you through the process of building your first extension and integrating it into Jan.
## Steps to Create Your First Extension
To create your own extension, you can follow the steps below:
1. Click the **Use this template** button at the top of the [extension-template repository](https://github.com/janhq/extension-template).
2. Select **Create a new repository**.
3. Choose an owner and name for your new repository.
4. Click **Create repository**.
5. Clone your new repository to your local machine.
## Initial Setup
After you have cloned the repository to your local machine or codespace, you will need to perform some initial setup steps before you can develop your extension.
:::info
You will 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`](https://github.com/janhq/extension-template/blob/main/package.json). Otherwise, 20.x or later should work!
:::
1. :hammer_and_wrench: Install the dependencies
```bash
npm install
```
2. :building_construction: Package the TypeScript for distribution
```bash
npm run bundle
```
3. :white_check_mark: Check your artifact
There will be a `.tgz` file in your extension directory now. This is the file you will need to import into Jan. You can import this file into Jan by following the instructions in the [Import Extension](https://jan.ai/guides/using-extensions/import-extensions/) guide.
## Update the Extension Metadata
The [`package.json`](https://github.com/janhq/extension-template/blob/main/package.json) file defines metadata about your extension, such as extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, and description for your extension.
## Update the Extension Code
The [`src/`](https://github.com/janhq/extension-template/tree/main/src) directory is the heart of your extension! This contains the source code that will be run when your extension 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 extension code:
- Most Jan 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 Extension Core module, see the [documentation](https://github.com/janhq/jan/blob/main/core/README.md).
Now, go ahead and start customizing your extension! Happy coding!

View File

@ -1,5 +1,6 @@
--- ---
title: Hardware Requirements title: Hardware Requirements
slug: /guides/install/hardware
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords: keywords:
[ [

View File

@ -0,0 +1,33 @@
---
title: Antivirus Compatibility Testing
slug: /guides/install/antivirus-compatibility-testing
description: Antivirus compatibility testing documentation for the Jan App v0.4.4 release.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
antivirus compatibility,
]
---
This documentation outlines the antivirus compatibility testing conducted for the Jan App v0.4.4 release. This documentation includes a matrix that correlates the Jan App version with the tested antivirus versions.
## Tested Antivirus Versions
The Jan App v0.4.4 release has undergone automatic testing through CI with a selection of popular antivirus software to ensure compatibility and safety. The following summarizes the testing results:
| Antivirus | Version | Result |
| ------------------ | ------------ | -------------------------------- |
| Bitdefender | 27.0.27.125 | Scanned and 0 threat(s) detected |
| McAfee | 4.21.0.0 | Scanned and 0 threat(s) detected |
| Microsoft Defender | 1.403.2259.0 | Scanned and 0 threat(s) detected |
## Conclusion
The testing indicates that Jan App v0.4.4 is compatible with Bitdefender, Microsoft Defender, and McAfee. Any updates or changes to compatibility status will be promptly documented.

View File

@ -0,0 +1,133 @@
---
title: Extension Settings
slug: /guides/using-extensions/extension-settings/
description: Configure settings for extensions.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
extension settings,
]
---
The current Jan Desktop Client has some default extensions built on top of this framework to enhance the user experience. In this guide, we will show you the list of default extensions and how to configure extension settings.
## Default Extensions
You can find the default extensions in the `Settings` > `Extensions`.
![Extensions](./assets/01-extension-settings.png)
### List of Default Extensions
| Extension Name | Version | Description | Source Code Link |
| ---------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| Assistant Extension | v1.0.0 | This extension enables assistants, including Jan, a default assistant that can call all downloaded models. | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/assistant-extension) |
| Conversational Extension | v1.0.0 | This extension enables conversations and state persistence via your filesystem. | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/conversational-extension) |
| Inference Nitro Extension | v1.0.0 | This extension embeds Nitro, a lightweight (3mb) inference engine written in C++. See [nitro.jan.ai](nitro.jan.ai) | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/inference-nitro-extension) |
| Inference Openai Extension | v1.0.0 | This extension enables OpenAI chat completion API calls | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/inference-openai-extension) |
| Inference Triton Trt Llm Extension | v1.0.0 | This extension enables Nvidia's TensorRT-LLM as an inference engine option. | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/inference-triton-trtllm-extension) |
| Model Extension | v1.0.22 | Model Management Extension provides model exploration and seamless downloads. | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/model-extension) |
| Monitoring Extension | v1.0.9 | This extension provides system health and OS level data. | [Link to Source](https://github.com/janhq/jan/tree/main/extensions/monitoring-extension) |
## Configure Extension Settings
You can configure the extension settings by modifying the `extensions.json` file under the `~/jan/extensions` directory including the following configurations:
- `_active`: true means the extension is enabled. If you want to disable an extension, you can set it to false.
- `listeners`: {} is the default value for listeners.
- `origin`: the path to the extension file.
- `installOptions`: configure the installOptions with version and fullMetadata.
- `name`: the name of the extension.
- `version`: the version of the extension.
- `main`: the path to the main file of the extension.
- `description`: the description of the extension.
- `url`: the url of the extension.
```json title="~/jan/extensions/extensions.json"
{
"@janhq/assistant-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-assistant-extension-1.0.0.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/assistant-extension",
"version": "1.0.0",
"main": "dist/index.js",
"description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models",
"url": "extension://@janhq/assistant-extension/dist/index.js"
},
"@janhq/conversational-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-conversational-extension-1.0.0.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/conversational-extension",
"version": "1.0.0",
"main": "dist/index.js",
"description": "This extension enables conversations and state persistence via your filesystem",
"url": "extension://@janhq/conversational-extension/dist/index.js"
},
"@janhq/inference-nitro-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-inference-nitro-extension-1.0.0.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/inference-nitro-extension",
"version": "1.0.0",
"main": "dist/index.js",
"description": "This extension embeds Nitro, a lightweight (3mb) inference engine written in C++. See nitro.jan.ai",
"url": "extension://@janhq/inference-nitro-extension/dist/index.js"
},
"@janhq/inference-openai-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-inference-openai-extension-1.0.0.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/inference-openai-extension",
"version": "1.0.0",
"main": "dist/index.js",
"description": "This extension enables OpenAI chat completion API calls",
"url": "extension://@janhq/inference-openai-extension/dist/index.js"
},
"@janhq/inference-triton-trt-llm-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-inference-triton-trt-llm-extension-1.0.0.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/inference-triton-trt-llm-extension",
"version": "1.0.0",
"main": "dist/index.js",
"description": "This extension enables Nvidia's TensorRT-LLM as an inference engine option",
"url": "extension://@janhq/inference-triton-trt-llm-extension/dist/index.js"
},
"@janhq/model-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-model-extension-1.0.22.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/model-extension",
"version": "1.0.22",
"main": "dist/index.js",
"description": "Model Management Extension provides model exploration and seamless downloads",
"url": "extension://@janhq/model-extension/dist/index.js"
},
"@janhq/monitoring-extension": {
"_active": true,
"listeners": {},
"origin": "/Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-monitoring-extension-1.0.9.tgz",
"installOptions": { "version": false, "fullMetadata": false },
"name": "@janhq/monitoring-extension",
"version": "1.0.9",
"main": "dist/index.js",
"description": "This extension provides system health and OS level data",
"url": "extension://@janhq/monitoring-extension/dist/index.js"
}
}
```

View File

@ -1,17 +0,0 @@
---
title: Import Extensions
slug: /guides/using-extensions/import-extensions/
description: Import extensions into Jan.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-models,
]
---

View File

@ -1,17 +0,0 @@
---
title: Extension Settings
slug: /guides/using-extensions/extension-settings/
description: Configure settings for extensions.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-models,
]
---

View File

@ -0,0 +1,29 @@
---
title: Import Extensions
slug: /guides/using-extensions/import-extensions/
description: Import extensions into Jan.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
import extensions,
]
---
Beside default extensions, you can import extensions into Jan by navigate to `Settings` > `Extensions` > `Manual Installation`. Then, the `~/jan/extensions/extensions.json` file will be updated automatically.
:::caution
You need to prepare the extension file in `.tgz` format to install.
:::
![Manual Installation](./assets/02-import-extensions.gif)
If you want to build your own extension, please refer to the [Build Your First Extension | Developer Documentation](/developer/build-extension/your-first-extension/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

View File

@ -37,7 +37,7 @@ You can find your API keys in the [OpenRouter API Key](https://openrouter.ai/key
} }
``` ```
### 2. Mofidy a Model JSON ### 2. Modify a Model JSON
Navigate to the `~/jan/models` folder. Create a folder named `<openrouter-modelname>`, for example, `openrouter-dolphin-mixtral-8x7b` and create a `model.json` file inside the folder including the following configurations: Navigate to the `~/jan/models` folder. Create a folder named `<openrouter-modelname>`, for example, `openrouter-dolphin-mixtral-8x7b` and create a `model.json` file inside the folder including the following configurations:

View File

@ -0,0 +1,91 @@
---
title: Integrate Azure OpenAI Service with Jan
slug: /guides/integrations/azure-openai-service
description: Guide to integrate Azure OpenAI Service with Jan
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
integration,
Azure OpenAI Service,
]
---
## Quick Introduction
[Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview?source=docs) provides a set of powerful APIs that enable you to easily integrate the OpenAI's language models.
In this guide, we will show you how to integrate Azure OpenAI Service with Jan.
## Steps to Integrate Azure OpenAI Service with Jan
### 1. Configure Azure OpenAI Service API key
Once you completed setting up and deploying the Azure OpenAI Service, you can find the endpoint and API key in the [Azure OpenAI Studio](https://oai.azure.com/) by navigating to `Chat` > `View code`.
![View-code](./assets/03-viewcode.png)
<br> </br>
![AzureOpenAIKeyandEndpoint](./assets/03-azureopenai-endpoint-key.png)
Set the Azure OpenAI Service endpoint and API key in the `~/jan/engines/openai.json` file.
```json title="~/jan/engines/openai.json"
{
// https://hieujan.openai.azure.com/openai/deployments/gpt-35-hieu-jan/chat/completions?api-version=2023-07-01-preview
// highlight-start
"full_url": "https://<your-resource-name>.openai.azure.com/openai/deployments/<your-deployment-name>/chat/completions?api-version=<api-version>",
"api_key": "<your-api-key>"
// highlight-end
}
```
### 2. Modify a Model JSON
Navigate to the `~/jan/models` folder. Create a folder named `<your-deployment-name>`, for example, `gpt-35-hieu-jan` and create a `model.json` file inside the folder including the following configurations:
- Ensure the filename must be `model.json`.
- Ensure the `id` property is set to the same as the folder name and your deployment name.
- Ensure the `format` property is set to `api`.
- Ensure the `engine` property is set to `openai`.
- Ensure the `state` property is set to `ready`.
```json title="~/jan/models/gpt-35-hieu-jan/model.json"
{
"source_url": "https://hieujan.openai.azure.com",
// highlight-next-line
"id": "gpt-35-hieu-jan",
"object": "model",
"name": "Azure OpenAI GPT 3.5",
"version": "1.0",
"description": "Azure Open AI GPT 3.5 model is extremely good",
// highlight-next-line
"format": "api",
"settings": {},
"parameters": {},
"metadata": {
"author": "OpenAI",
"tags": ["General", "Big Context Length"]
},
// highlight-start
"engine": "openai",
"state": "ready"
// highlight-end
}
```
### 3. Start the Model
Restart Jan and navigate to the Hub. Locate your model and click the Use button.
![StartModel](./assets/03-start-model.png)
### 4. Try Out the Integration of Jan and Azure OpenAI Service
![Integration Demo](./assets/03-azureopenai-integration-demo.gif)

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

View File

@ -1,8 +1,11 @@
--- ---
title: Something's amiss title: Something's Amiss
slug: /troubleshooting/somethings-amiss slug: /troubleshooting/somethings-amiss
description: Troubleshooting "Something's amiss". description: Troubleshooting "Something's amiss".
keywords: [ keywords: [
jan ai failed to fetch,
failed to fetch error,
jan ai error,
Jan AI, Jan AI,
Jan, Jan,
ChatGPT alternative, ChatGPT alternative,
@ -16,6 +19,12 @@ keywords: [
] ]
--- ---
{/* Imports */}
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
Previously labelled "Failed to fetch" error.
You may receive a "Something's amiss" response when you first start chatting with a selected model. You may receive a "Something's amiss" response when you first start chatting with a selected model.
This may occur due to several reasons. Please follow these steps to resolve it: This may occur due to several reasons. Please follow these steps to resolve it:
@ -30,8 +39,45 @@ This may occur due to several reasons. Please follow these steps to resolve it:
3. Install the latest [Nightly release](https://jan.ai/install/nightly/) 3. Install the latest [Nightly release](https://jan.ai/install/nightly/)
- If you are re-installing Jan, it can help to [clear the application cache](https://jan.ai/troubleshooting/stuck-on-broken-build/) - If you are re-installing Jan, it can help to [clear the application cache](https://jan.ai/troubleshooting/stuck-on-broken-build/).
4. Ensure your V/RAM is accessible by the application (some people have virtual RAM) 4. Ensure your V/RAM is accessible by the application (some people have virtual RAM).
5. If you are on Nvidia GPUs, please download [Cuda](https://developer.nvidia.com/cuda-downloads) 5. If you are on Nvidia GPUs, please download [Cuda](https://developer.nvidia.com/cuda-downloads).
6. When [checking app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/), if you encounter the error log `Bind address failed at 127.0.0.1:3928`, it indicates that the port used by Nitro might already be in use. Use the following commands to check the port status:
<Tabs groupId="operating-systems">
<TabItem value="mac" label="macOS">
```bash
netstat -an | grep 3928
```
</TabItem>
<TabItem value="win" label="Windows">
```sh
netstat -ano | find "3928"
tasklist /fi "PID eq 3928"
```
</TabItem>
<TabItem value="linux" label="Linux">
```sh
netstat -anpe | grep "3928"
```
</TabItem>
</Tabs>
:::tip
Jan uses the following ports:
- Nitro: 3928
- Jan API Server: 1337
- Jan Documentation: 3001
:::

View File

@ -0,0 +1,32 @@
---
title: Permission Denied
slug: /troubleshooting/permission-denied
description: Permission denied.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
troubleshooting,
permission denied,
]
---
When you run Jan, you may encounter the following error:
```bash
Uncaught (in promise) Error: Error invoking layout-480796bff433a3a3.js:538 remote method 'installExtension':
Error Package /Applications/Jan.app/Contents/Resources/app.asar.unpacked/pre-install/janhq-assistant-extension-1.0.0.tgz does not contain a valid manifest:
Error EACCES: permission denied, mkdtemp '/Users/username/.npm/_cacache/tmp/ueCMn4'
```
This error indicates a permission issue during the installation process. To fix this issue, you can run the following command to change ownership of the `~/.npm` directory to the current user:
```bash
sudo chown -R $(whoami) ~/.npm
```

View File

@ -0,0 +1,20 @@
---
title: Unexpected Token
slug: /troubleshooting/unexpected-token
description: Unexpected token is not a valid JSON
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
troubleshooting,
unexpected token,
]
---
1. You may receive an error response `Error occurred: Unexpected token '<', "<!DOCTYPE"...is not valid JSON`, when you start a chat with OpenAI models. Using a VPN may help fix the issue.

View File

@ -1,17 +0,0 @@
---
title: Why we exist
slug: /handbook/meet-jan/why-we-exist
description: Why we exist
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Vision and Mission
slug: /handbook/meet-jan/vision-and-mission
description: Vision and mission of Jan
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Overview of Jan Framework and Its Applications
slug: /handbook/products-and-innovations/overview-of-jan-framework-and-its-applications
description: Overview of Jan Framework and Its Applications
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Philosophy Behind Product Development
slug: /handbook/products-and-innovations/philosophy-behind-product-development
description: Philosophy Behind Product Development
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Roadmap - Present and Future Directions
slug: /handbook/products-and-innovations/roadmap-present-and-future-directions
description: Roadmap - Present and Future Directions
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,90 +0,0 @@
---
title: How We Hire at Jan
slug: /handbook/core-contributors/how-we-hire
description: See how open-source LLM AI company Jan.ai (maker of Jan framework) approaches team building and hiring.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
how-we-hire,
]
---
_The purpose - To conduct fair, unbiased interviews that focus on identifying candidates with initiative, scrappiness, and a good fit for a core contributor role at Jan._
## Our Key Principles
### Avoiding Bias
Tempting as it is to judge someone based on the fact that they _also_ binge-watched Severance, every role at Jan is a key hire and should be interviewed to the same standard.
What does fair interviewing look like at Jan?
At Jan, the team assesses candidates based on their skills, experiences, and fit for the role and company culture. We expect an in turn assessment and healthy curiosity.
Were striving for a middle ground between hiring exclusively within our network (like PayPal in the early days) and broader, more open hiring practices that reduce bias.
[Learn more](./assets/01-learn-more.png)
What the candidate is looking for is just as important as what we at Jan are looking for.
Value alignment isnt something thats often spoken about when hiring for a new role. At Jan, we believe that value alignment is the stuff folks really mean when they say “culture fit”.
![Value Alignment](./assets/01-value-alignment.png)
## We Care About
### Initiative and Scrappiness
We want people on the team who demonstrate resourcefulness, problem-solving skills, and the ability to thrive in a bootstrapped, open-source environment. (This isnt a mandated “do more with less!” statement, but rather, we appreciate those who _get_ where were coming from.)
### Diversity, Equity, and Inclusion (DEI)
While we don't have a large DEI program, we are committed to inclusivity and respect for diverse opinions and backgrounds. Our focus is on individual merit and cultural fit, rather than ticking DEI checkboxes.
### Values
The stuff thats hard to put into words but you can always tell when its missing in your company. Our values play out in our everyday behaviors.
## The Kinds of Questions You Could Expect From an Interview
- Can you describe a time when you had to be resourceful or "scrappy" to solve a problem? What was the outcome?
- How do you stay motivated and productive in environments with limited resources or guidance?
- Can you give an example of a successful project you contributed to in an open-source community?
- How do you handle disagreements or differing opinions in a professional setting?
- Describe a situation where you took the initiative in a project or task. What drove you to take charge?
- In your view, what's the most challenging aspect of working in a bootstrapped company, and how would you navigate this challenge?
- How do you prioritize tasks?
- Can you discuss a time when you had to learn a new skill or technology quickly to complete a task? How did you approach this?
- What does 'cultural fit' mean to you, and why do you think it's important in a workplace?
- How do you balance the need for collaboration with independent work in your professional life?
## The (Ideal) Process
### Pre-Interview
Review the candidate's resume and portfolio without preconceived notions. Focus on their skills and experiences relevant to the role.
### During the Interview (internal notes)
Use the questions as a guide but be flexible to explore interesting points raised by the candidate. Ask the same questions of everyone if possible.
Observe not just what they say, but how they think and approach problems.
### Post-Interview
Evaluate candidates based on their responses, skills, potential cultural fit, and ability to contribute to Jan's goals within their proposed function. Avoid making snap judgments based on first impressions or personal biases.
### Decision Making
Combine feedback from everyone involved in hiring. Consider how the new hire would engage with others in their pod and the wider team.
Hiring isnt easy, and at Jan, were aiming to be as transparent and inclusive as we can, with the knowledge that as a small core contributor team, we need to be extremely selective about who we onboard.
The intention is theyre here for the long haul!

View File

@ -1,58 +0,0 @@
---
title: Embracing Pod Structure
slug: /handbook/core-contributors/embracing-pod-structure
description: How we move fast in engineering and cross-departments by owning functions, clear prioritization and sprints.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
embracing pod structure,
]
---
How do you operate fully remotely? How do you run an open source company without everyone going ape? This post aims to answer questions the Jan team answers regularly in Discord.
### Small Pods = Big Impact
At Jan, we operate in small pods- like nerdy dolphins. These are compact, agile teams, each taking ownership of specific aspects of our product. This structure allows us to ship features at an impressive speed, maintain a clear focus on our business goals, and experience minimal roadblocks.
### Our Approach to the Jan Roadmap
How do we decide what gets prioritized when there are so many potentials?
![Jan Roadmap](./assets/02-jan-roadmap.png)
[Our product handbook](https://jan.ai/handbook/product/) is not just a document; it makes up part of our larger open company handbook. The aim is for everything to be outlined - from our high-level roadmap, which is used for strategic alignment and reflects our engineering implementation cycles, to our Standup Kanban, the go-to view for our daily standups.
### How We Label
We use Roadmap Labels to tag large, long-term projects that often span multiple teams and sprints. For example, a label like 'roadmap: Jan has Mobile' signifies a significant strategic initiative.
These roadmaps contain epics large stories that typically span 1-2 weeks, complete with specs, architecture decisions, and designs. Each epic has one owner, ensuring clear responsibility and accountability.
### Milestones & Tasks
Our milestones, aligned with semantic versioning, track release versions and fit neatly into our 2-week sprint cycles. Tasks, whether they're features, bugs, or chores, are designed to be completed within a few days. They always belong to an epic, tying back to our larger roadmap.
### Kanban - Because a Cluttered Brain Doesnt Make Decisions
Our Kanban board is a dynamic snapshot of our workflow. It ranges from 'no status' for issues needing triage, to 'done' for completed tasks. This system helps us efficiently manage our workflow and ensures that every team member is aware of their responsibilities and the current state of our projects.
### How We Triage Bugs
Our Triage Standard Operating Procedure (SOP) is crucial for handling urgent bugs by assigning them to an owner and tagging them to the current sprint and milestone. For all other tasks, we assign the correct roadmap label(s) and owner. Determining what fires we let burn is a continuous learning and practice to build.
### Our Rhythm- Standups, sprints, demos, and TGIFs
Our day kicks off with a standup at 11 AM, where we align our tasks and address any immediate concerns. We run in sprints, which keeps us focused and fast. Fridays are special at Jan not just because it's the end of the week, but because we have demos and TGIF sessions. Not just for kicking back with questionable Google Meet filters but also for celebrating our achievements and learning from our experiences.
### Small Pods, Big Dreams
At Jan our small pod structure is more than just an organizational choice; it's a reflection of our belief in empowerment, agility, and clear focus. Hierarchical management and top-down decision making are something that goes against our core - and weve found that by being small and owning functions - our objectives arent just goals - theyre becoming reality.

View File

@ -1,17 +0,0 @@
---
title: The Art of Conflict
slug: /handbook/core-contributors/the-art-of-conflict
description: The Art of Conflict
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: OpSec
slug: /handbook/core-contributors/opsec
description: OpSec
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: See a Problem, Own a Problem
slug: /handbook/core-contributors/see-a-problem-own-a-problem
description: See a Problem, Own a Problem - How we function without management
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

View File

@ -1,17 +0,0 @@
---
title: Our Support Methodology - Open Source, Collaborative, and Self-serve
slug: /handbook/what-we-do/our-support-methodology
description: Our Support Methodology - Open Source, Collaborative, and Self-serve
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Our Approach to Design
slug: /handbook/what-we-do/our-approach-to-design
description: Our Approach to Design
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Shipping Now, Shipping Later
slug: /handbook/what-we-do/shipping-now-shipping-later
description: Shipping Now, Shipping Later
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

View File

@ -1,17 +0,0 @@
---
title: Trial by Fire
slug: /handbook/what-we-do/trial-by-fire
description: Trial by Fire
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
handbook,
]
---

Some files were not shown because too many files have changed in this diff Show More