Merge branch 'dev' into main

This commit is contained in:
Louis 2024-04-25 14:14:54 +07:00 committed by GitHub
commit 63a2f22414
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 512 additions and 30 deletions

View File

@ -25,16 +25,13 @@ If applicable, add screenshots to help explain your issue.
**Environment details** **Environment details**
- Operating System: [Specify your OS. e.g., MacOS Sonoma 14.2.1, Windows 11, Ubuntu 22, etc] - Operating System: [Specify your OS. e.g., MacOS Sonoma 14.2.1, Windows 11, Ubuntu 22, etc]
- Jan Version: [e.g., 0.4.3] - Jan Version: [e.g., 0.4.xxx nightly or manual]
- Processor: [e.g., Apple M1, Intel Core i7, AMD Ryzen 5, etc] - Processor: [e.g., Apple M1, Intel Core i7, AMD Ryzen 5, etc]
- RAM: [e.g., 8GB, 16GB] - RAM: [e.g., 8GB, 16GB]
- Any additional relevant hardware specifics: [e.g., Graphics card, SSD/HDD] - Any additional relevant hardware specifics: [e.g., Graphics card, SSD/HDD]
**Logs** **Logs**
If the cause of the error is not clear, kindly provide your usage logs: If the cause of the error is not clear, kindly provide your usage logs: https://jan.ai/docs/troubleshooting#how-to-get-error-logs
- `tail -n 50 ~/jan/logs/app.log` if you are using the UI
- `tail -n 50 ~/jan/logs/server.log` if you are using the local api server
Making sure to redact any private information.
**Additional context** **Additional context**
Add any other context or information that could be helpful in diagnosing the problem. Add any other context or information that could be helpful in diagnosing the problem.

View File

@ -1,12 +1,6 @@
name: Jan Build Electron App Nightly or Manual name: Electron Builder - Nightly / Manual
on: on:
push:
branches:
- main
paths-ignore:
- 'README.md'
- 'docs/**'
schedule: schedule:
- cron: '0 20 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 3 AM UTC+7 Tuesday, Wednesday, and Thursday - cron: '0 20 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 3 AM UTC+7 Tuesday, Wednesday, and Thursday
workflow_dispatch: workflow_dispatch:

View File

@ -1,4 +1,4 @@
name: Jan Build Electron App name: Electron Builder - Tag
on: on:
push: push:

View File

@ -1,4 +1,4 @@
name: Jan Electron Linter & Test name: Test - Linter & Playwright
on: on:
workflow_dispatch: workflow_dispatch:
push: push:

View File

@ -0,0 +1,90 @@
name: Test - OpenAI API Pytest collection
on:
workflow_dispatch:
push:
branches:
- main
- dev
- release/**
paths:
- "docs/**"
pull_request:
branches:
- main
- dev
- release/**
paths:
- "docs/**"
jobs:
openai-python-tests:
runs-on: [self-hosted, Linux, ubuntu-desktop]
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
with:
node-version: 20
- name: "Cleanup cache"
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: install dependencies
run: |
npm install -g @stoplight/prism-cli
- name: create python virtual environment and run test
run: |
python3 -m venv /tmp/jan
source /tmp/jan/bin/activate
# Clone openai-api-python repo
OPENAI_API_PYTHON_TAG=$(cat docs/openapi/version.txt)
git clone https://github.com/openai/openai-python.git
cd openai-python
git checkout $OPENAI_API_PYTHON_TAG
python3 -m venv /tmp/jan
source /tmp/jan/bin/activate
pip install -r requirements-dev.lock
pip install pytest-reportportal pytest-html
# Create pytest.ini file with content
cat ../docs/tests/pytest.ini >> pytest.ini
echo "rp_api_key = ${{ secrets.REPORT_PORTAL_API_KEY }}" >> pytest.ini
echo "rp_endpoint = ${{ secrets.REPORT_PORTAL_URL_PYTEST }}" >> pytest.ini
cat pytest.ini
# Append to conftest.py
cat ../docs/tests/conftest.py >> tests/conftest.py
# start mock server and run test then stop mock server
prism mock ../docs/openapi/jan.yaml > prism.log & prism_pid=$! && pytest --reportportal --html=report.html && kill $prism_pid
deactivate
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: report
path: |
openai-python/report.html
openai-python/assets
openai-python/prism.log
- name: clean up
if: always()
run: |
rm -rf /tmp/jan
rm -rf openai-python
rm -rf report.html
rm -rf report.zip

View File

@ -1,4 +1,4 @@
name: Jan Build Docker Nightly or Manual name: Docker Builder - Nightly / Manual
on: on:
push: push:

View File

@ -1,4 +1,4 @@
name: Jan Build Docker name: Docker Builder - Tag
on: on:
push: push:

View File

@ -31,6 +31,12 @@ export abstract class OAIEngine extends AIEngine {
// The loaded model instance // The loaded model instance
loadedModel: Model | undefined loadedModel: Model | undefined
// Transform the payload
transformPayload?: Function
// Transform the response
transformResponse?: Function
/** /**
* On extension load, subscribe to events. * On extension load, subscribe to events.
*/ */
@ -78,13 +84,23 @@ export abstract class OAIEngine extends AIEngine {
} }
const header = await this.headers() const header = await this.headers()
let requestBody = {
messages: data.messages ?? [],
model: model.id,
stream: true,
...model.parameters,
}
if (this.transformPayload) {
requestBody = this.transformPayload(requestBody)
}
requestInference( requestInference(
this.inferenceUrl, this.inferenceUrl,
data.messages ?? [], requestBody,
model, model,
this.controller, this.controller,
header header,
this.transformResponse
).subscribe({ ).subscribe({
next: (content: any) => { next: (content: any) => {
const messageContent: ThreadContent = { const messageContent: ThreadContent = {

View File

@ -7,21 +7,16 @@ import { ErrorCode, ModelRuntimeParams } from '../../../../types'
*/ */
export function requestInference( export function requestInference(
inferenceUrl: string, inferenceUrl: string,
recentMessages: any[], requestBody: any,
model: { model: {
id: string id: string
parameters: ModelRuntimeParams parameters: ModelRuntimeParams
}, },
controller?: AbortController, controller?: AbortController,
headers?: HeadersInit headers?: HeadersInit,
transformResponse?: Function
): Observable<string> { ): Observable<string> {
return new Observable((subscriber) => { return new Observable((subscriber) => {
const requestBody = JSON.stringify({
messages: recentMessages,
model: model.id,
stream: true,
...model.parameters,
})
fetch(inferenceUrl, { fetch(inferenceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -30,17 +25,17 @@ export function requestInference(
'Accept': model.parameters.stream ? 'text/event-stream' : 'application/json', 'Accept': model.parameters.stream ? 'text/event-stream' : 'application/json',
...headers, ...headers,
}, },
body: requestBody, body: JSON.stringify(requestBody),
signal: controller?.signal, signal: controller?.signal,
}) })
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
const data = await response.json() const data = await response.json()
let errorCode = ErrorCode.Unknown; let errorCode = ErrorCode.Unknown
if (data.error) { if (data.error) {
errorCode = data.error.code ?? data.error.type ?? ErrorCode.Unknown errorCode = data.error.code ?? data.error.type ?? ErrorCode.Unknown
} else if (response.status === 401) { } else if (response.status === 401) {
errorCode = ErrorCode.InvalidApiKey; errorCode = ErrorCode.InvalidApiKey
} }
const error = { const error = {
message: data.error?.message ?? 'Error occurred.', message: data.error?.message ?? 'Error occurred.',
@ -52,7 +47,11 @@ export function requestInference(
} }
if (model.parameters.stream === false) { if (model.parameters.stream === false) {
const data = await response.json() const data = await response.json()
if (transformResponse) {
subscriber.next(transformResponse(data))
} else {
subscriber.next(data.choices[0]?.message?.content ?? '') subscriber.next(data.choices[0]?.message?.content ?? '')
}
} else { } else {
const stream = response.body const stream = response.body
const decoder = new TextDecoder('utf-8') const decoder = new TextDecoder('utf-8')

View File

@ -1,3 +1,5 @@
import { ChatCompletionMessage } from '../inference'
/** /**
* Native Route APIs * Native Route APIs
* @description Enum of all the routes exposed by the app * @description Enum of all the routes exposed by the app
@ -154,3 +156,8 @@ export const APIEvents = [
...Object.values(DownloadEvent), ...Object.values(DownloadEvent),
...Object.values(LocalImportModelEvent), ...Object.values(LocalImportModelEvent),
] ]
export type PayloadType = {
messages: ChatCompletionMessage[]
model: string
stream: Boolean
}

1
docs/openapi/version.txt Normal file
View File

@ -0,0 +1 @@
v1.23.2

6
docs/tests/conftest.py Normal file
View File

@ -0,0 +1,6 @@
def pytest_collection_modifyitems(items):
for item in items:
# add the name of the file (without extension) as a marker
filename = item.nodeid.split("::")[0].split("/")[-1].replace(".py", "")
marker = pytest.mark.file(filename)
item.add_marker(marker)

8
docs/tests/pytest.ini Normal file
View File

@ -0,0 +1,8 @@
[pytest]
rp_project = openai-api-test
rp_launch = OpenAI Collection Test
rp_launch_description = Full collection to ensure compatibility with OpenAI API
rp_launch_attributes = 'CI'
filterwarnings = ignore::pytest.PytestUnknownMarkWarning
log_format = %(asctime)s %(levelname)s %(message)s
log_date_format = %Y-%m-%d %H:%M:%S

View File

@ -0,0 +1,79 @@
# Cohere Engine Extension
Created using Jan extension example
# Create a Jan Extension using Typescript
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
## Create Your Own Extension
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
3. Select an owner and name for your new repository
4. Click Create repository
5. Clone your new repository
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
> You'll need to have a reasonably modern version of
> [Node.js](https://nodejs.org) handy. If you are using a version manager like
> [`nodenv`](https://github.com/nodenv/nodenv) or
> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
> root of your repository to install the version specified in
> [`package.json`](./package.json). Otherwise, 20.x or later should work!
1. :hammer_and_wrench: Install the dependencies
```bash
npm install
```
1. :building_construction: Package the TypeScript for distribution
```bash
npm run bundle
```
1. :white_check_mark: Check your artifact
There will be a tgz file in your extension directory now
## Update the Extension Metadata
The [`package.json`](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, description for your extension.
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your 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 { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -0,0 +1,43 @@
{
"name": "@janhq/inference-cohere-extension",
"productName": "Cohere Inference Engine",
"version": "1.0.0",
"description": "This extension enables Cohere chat completion API calls",
"main": "dist/index.js",
"module": "dist/module.js",
"engine": "cohere",
"author": "Jan <service@jan.ai>",
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"build:publish": "rimraf *.tgz --glob && yarn build && npm pack && cpx *.tgz ../../pre-install",
"sync:core": "cd ../.. && yarn build:core && cd extensions && rm yarn.lock && cd inference-cohere-extension && yarn && yarn build:publish"
},
"exports": {
".": "./dist/index.js",
"./main": "./dist/module.js"
},
"devDependencies": {
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
"fetch-retry": "^5.0.6",
"ulidx": "^2.3.0"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/*",
"package.json",
"README.md"
],
"bundleDependencies": [
"fetch-retry"
]
}

View File

@ -0,0 +1,26 @@
[
{
"sources": [
{
"url": "https://cohere.com"
}
],
"id": "command-r-plus",
"object": "model",
"name": "Command R+",
"version": "1.0",
"description": "Command R+ is an instruction-following conversational model that performs language tasks at a higher quality, more reliably, and with a longer context than previous models. It is best suited for complex RAG workflows and multi-step tool use.",
"format": "api",
"settings": {},
"parameters": {
"max_tokens": 128000,
"temperature": 0.7,
"stream": false
},
"metadata": {
"author": "Cohere",
"tags": ["General", "Big Context Length"]
},
"engine": "cohere"
}
]

View File

@ -0,0 +1,23 @@
[
{
"key": "chat-completions-endpoint",
"title": "Chat Completions Endpoint",
"description": "The endpoint to use for chat completions. See the [Cohere API documentation](https://docs.cohere.com/reference/chat) for more information.",
"controllerType": "input",
"controllerProps": {
"placeholder": "https://api.cohere.ai/v1/chat",
"value": "https://api.cohere.ai/v1/chat"
}
},
{
"key": "cohere-api-key",
"title": "API Key",
"description": "The Cohere API uses API keys for authentication. Visit your [API Keys](https://platform.openai.com/account/api-keys) page to retrieve the API key you'll use in your requests.",
"controllerType": "input",
"controllerProps": {
"placeholder": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"value": "",
"type": "password"
}
}
]

View File

@ -0,0 +1,110 @@
/**
* @file This file exports a class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
* @version 1.0.0
* @module inference-openai-extension/src/index
*/
import { RemoteOAIEngine } from '@janhq/core'
import { PayloadType } from '@janhq/core'
import { ChatCompletionRole } from '@janhq/core'
declare const SETTINGS: Array<any>
declare const MODELS: Array<any>
enum Settings {
apiKey = 'cohere-api-key',
chatCompletionsEndPoint = 'chat-completions-endpoint',
}
enum RoleType {
user = 'USER',
chatbot = 'CHATBOT',
system = 'SYSTEM',
}
type CoherePayloadType = {
chat_history?: Array<{ role: RoleType; message: string }>
message?: string,
preamble?: string,
}
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceCohereExtension extends RemoteOAIEngine {
inferenceUrl: string = ''
provider: string = 'cohere'
override async onLoad(): Promise<void> {
super.onLoad()
// Register Settings
this.registerSettings(SETTINGS)
this.registerModels(MODELS)
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
if (this.inferenceUrl.length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
}
}
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.apiKey) {
this.apiKey = value as string
} else if (key === Settings.chatCompletionsEndPoint) {
if (typeof value !== 'string') return
if (value.trim().length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
} else {
this.inferenceUrl = value
}
}
}
transformPayload = (payload: PayloadType): CoherePayloadType => {
if (payload.messages.length === 0) {
return {}
}
const convertedData:CoherePayloadType = {
chat_history: [],
message: '',
}
payload.messages.forEach((item, index) => {
// Assign the message of the last item to the `message` property
if (index === payload.messages.length - 1) {
convertedData.message = item.content as string
return
}
if (item.role === ChatCompletionRole.User) {
convertedData.chat_history.push({ role: RoleType.user, message: item.content as string})
} else if (item.role === ChatCompletionRole.Assistant) {
convertedData.chat_history.push({
role: RoleType.chatbot,
message: item.content as string,
})
} else if (item.role === ChatCompletionRole.System) {
convertedData.preamble = item.content as string
}
})
return convertedData
}
transformResponse = (data: any) => data.text
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ES6",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -0,0 +1,37 @@
const webpack = require('webpack')
const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
const modelsJson = require('./resources/models.json')
module.exports = {
experiments: { outputModule: true },
entry: './src/index.ts', // Adjust the entry point to match your project's main file
mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new webpack.DefinePlugin({
MODELS: JSON.stringify(modelsJson),
SETTINGS: JSON.stringify(settingJson),
ENGINE: JSON.stringify(packageJson.engine),
}),
],
output: {
filename: 'index.js', // Adjust the output file name as needed
library: { type: 'module' }, // Specify ESM output format
},
resolve: {
extensions: ['.ts', '.js'],
},
optimization: {
minimize: false,
},
// Add loaders and other configuration as needed for your project
}

View File

@ -0,0 +1,32 @@
{
"sources": [
{
"url": "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf",
"filename": "Phi-3-mini-4k-instruct-q4.gguf"
}
],
"id": "phi3-3.8b",
"object": "model",
"name": "Phi-3 Mini",
"version": "1.0",
"description": "Phi-3 Mini is Microsoft's newest, compact model designed for mobile use.",
"format": "gguf",
"settings": {
"ctx_len": 4096,
"prompt_template": "<|system|>\n{system_message}<|end|>\n<|user|>\n{prompt}<|end|>\n<|assistant|>\n",
"llama_model_path": "Phi-3-mini-4k-instruct-q4.gguf"
},
"parameters": {
"max_tokens": 4096,
"stop": ["<|end|>"]
},
"metadata": {
"author": "Microsoft",
"tags": [
"3B",
"Finetuned"
],
"size": 2320000000
},
"engine": "nitro"
}