Merge branch 'dev-web' into stag-web
This commit is contained in:
commit
39917920bd
4
.github/workflows/jan-server-web-ci-dev.yml
vendored
4
.github/workflows/jan-server-web-ci-dev.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
build-and-preview:
|
build-and-preview:
|
||||||
runs-on: [ubuntu-24-04-docker]
|
runs-on: [ubuntu-24-04-docker]
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: "https://api-dev.menlo.ai/v1"
|
MENLO_PLATFORM_BASE_URL: "https://api-dev.menlo.ai/v1"
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: write
|
contents: write
|
||||||
@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
run: |
|
run: |
|
||||||
docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
||||||
|
|
||||||
- name: Push docker image
|
- name: Push docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|||||||
4
.github/workflows/jan-server-web-ci-prod.yml
vendored
4
.github/workflows/jan-server-web-ci-prod.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
deployments: write
|
deployments: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: "https://api.menlo.ai/v1"
|
MENLO_PLATFORM_BASE_URL: "https://api.menlo.ai/v1"
|
||||||
GA_MEASUREMENT_ID: "G-YK53MX8M8M"
|
GA_MEASUREMENT_ID: "G-YK53MX8M8M"
|
||||||
CLOUDFLARE_PROJECT_NAME: "jan-server-web"
|
CLOUDFLARE_PROJECT_NAME: "jan-server-web"
|
||||||
steps:
|
steps:
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: make config-yarn && yarn install && yarn build:core && make build-web-app
|
run: make config-yarn && yarn install && yarn build:core && make build-web-app
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: ${{ env.JAN_API_BASE }}
|
MENLO_PLATFORM_BASE_URL: ${{ env.MENLO_PLATFORM_BASE_URL }}
|
||||||
GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }}
|
GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }}
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages Production
|
- name: Publish to Cloudflare Pages Production
|
||||||
|
|||||||
4
.github/workflows/jan-server-web-ci-stag.yml
vendored
4
.github/workflows/jan-server-web-ci-stag.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
build-and-preview:
|
build-and-preview:
|
||||||
runs-on: [ubuntu-24-04-docker]
|
runs-on: [ubuntu-24-04-docker]
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: "https://api-stag.menlo.ai/v1"
|
MENLO_PLATFORM_BASE_URL: "https://api-stag.menlo.ai/v1"
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: write
|
contents: write
|
||||||
@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
run: |
|
run: |
|
||||||
docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
||||||
|
|
||||||
- name: Push docker image
|
- name: Push docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|||||||
@ -49,6 +49,8 @@ jobs:
|
|||||||
# Update tauri.conf.json
|
# Update tauri.conf.json
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = false' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = false' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
||||||
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
||||||
|
jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
|
||||||
|
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
|
||||||
jq '.bundle.windows.signCommand = "echo External build - skipping signature: %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
|
jq '.bundle.windows.signCommand = "echo External build - skipping signature: %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
|
||||||
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
|
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
|
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
|
||||||
@ -80,6 +82,36 @@ jobs:
|
|||||||
echo "---------./src-tauri/Cargo.toml---------"
|
echo "---------./src-tauri/Cargo.toml---------"
|
||||||
cat ./src-tauri/Cargo.toml
|
cat ./src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
generate_build_version() {
|
||||||
|
### Examble
|
||||||
|
### input 0.5.6 output will be 0.5.6 and 0.5.6.0
|
||||||
|
### input 0.5.6-rc2-beta output will be 0.5.6 and 0.5.6.2
|
||||||
|
### input 0.5.6-1213 output will be 0.5.6 and and 0.5.6.1213
|
||||||
|
local new_version="$1"
|
||||||
|
local base_version
|
||||||
|
local t_value
|
||||||
|
# Check if it has a "-"
|
||||||
|
if [[ "$new_version" == *-* ]]; then
|
||||||
|
base_version="${new_version%%-*}" # part before -
|
||||||
|
suffix="${new_version#*-}" # part after -
|
||||||
|
# Check if it is rcX-beta
|
||||||
|
if [[ "$suffix" =~ ^rc([0-9]+)-beta$ ]]; then
|
||||||
|
t_value="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
t_value="$suffix"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
base_version="$new_version"
|
||||||
|
t_value="0"
|
||||||
|
fi
|
||||||
|
# Export two values
|
||||||
|
new_base_version="$base_version"
|
||||||
|
new_build_version="${base_version}.${t_value}"
|
||||||
|
}
|
||||||
|
generate_build_version ${{ inputs.new_version }}
|
||||||
|
sed -i "s/jan_version/$new_base_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
sed -i "s/jan_build/$new_build_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
|
||||||
if [ "${{ inputs.channel }}" != "stable" ]; then
|
if [ "${{ inputs.channel }}" != "stable" ]; then
|
||||||
jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
||||||
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
||||||
@ -103,7 +135,14 @@ jobs:
|
|||||||
chmod +x .github/scripts/rename-workspace.sh
|
chmod +x .github/scripts/rename-workspace.sh
|
||||||
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
|
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
|
||||||
cat ./package.json
|
cat ./package.json
|
||||||
|
sed -i "s/jan_productname/Jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
sed -i "s/jan_mainbinaryname/jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
else
|
||||||
|
sed -i "s/jan_productname/Jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
sed -i "s/jan_mainbinaryname/jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
fi
|
fi
|
||||||
|
echo "---------nsis.template---------"
|
||||||
|
cat ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
- name: Build app
|
- name: Build app
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@ -98,9 +98,15 @@ jobs:
|
|||||||
# Update tauri.conf.json
|
# Update tauri.conf.json
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
||||||
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
||||||
|
jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
|
||||||
|
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
|
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
|
||||||
mv /tmp/package.json web-app/package.json
|
mv /tmp/package.json web-app/package.json
|
||||||
|
|
||||||
|
# Add sign commands to tauri.windows.conf.json
|
||||||
|
jq '.bundle.windows.signCommand = "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
|
||||||
|
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
|
||||||
|
|
||||||
# Update tauri plugin versions
|
# Update tauri plugin versions
|
||||||
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json
|
jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json
|
||||||
@ -127,9 +133,35 @@ jobs:
|
|||||||
echo "---------./src-tauri/Cargo.toml---------"
|
echo "---------./src-tauri/Cargo.toml---------"
|
||||||
cat ./src-tauri/Cargo.toml
|
cat ./src-tauri/Cargo.toml
|
||||||
|
|
||||||
# Add sign commands to tauri.windows.conf.json
|
generate_build_version() {
|
||||||
jq '.bundle.windows.signCommand = "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
|
### Example
|
||||||
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
|
### input 0.5.6 output will be 0.5.6 and 0.5.6.0
|
||||||
|
### input 0.5.6-rc2-beta output will be 0.5.6 and 0.5.6.2
|
||||||
|
### input 0.5.6-1213 output will be 0.5.6 and and 0.5.6.1213
|
||||||
|
local new_version="$1"
|
||||||
|
local base_version
|
||||||
|
local t_value
|
||||||
|
# Check if it has a "-"
|
||||||
|
if [[ "$new_version" == *-* ]]; then
|
||||||
|
base_version="${new_version%%-*}" # part before -
|
||||||
|
suffix="${new_version#*-}" # part after -
|
||||||
|
# Check if it is rcX-beta
|
||||||
|
if [[ "$suffix" =~ ^rc([0-9]+)-beta$ ]]; then
|
||||||
|
t_value="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
t_value="$suffix"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
base_version="$new_version"
|
||||||
|
t_value="0"
|
||||||
|
fi
|
||||||
|
# Export two values
|
||||||
|
new_base_version="$base_version"
|
||||||
|
new_build_version="${base_version}.${t_value}"
|
||||||
|
}
|
||||||
|
generate_build_version ${{ inputs.new_version }}
|
||||||
|
sed -i "s/jan_version/$new_base_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
sed -i "s/jan_build/$new_build_version/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
|
||||||
echo "---------tauri.windows.conf.json---------"
|
echo "---------tauri.windows.conf.json---------"
|
||||||
cat ./src-tauri/tauri.windows.conf.json
|
cat ./src-tauri/tauri.windows.conf.json
|
||||||
@ -163,7 +195,14 @@ jobs:
|
|||||||
chmod +x .github/scripts/rename-workspace.sh
|
chmod +x .github/scripts/rename-workspace.sh
|
||||||
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
|
.github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }}
|
||||||
cat ./package.json
|
cat ./package.json
|
||||||
|
sed -i "s/jan_productname/Jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
sed -i "s/jan_mainbinaryname/jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
else
|
||||||
|
sed -i "s/jan_productname/Jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
sed -i "s/jan_mainbinaryname/jan/g" ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
fi
|
fi
|
||||||
|
echo "---------nsis.template---------"
|
||||||
|
cat ./src-tauri/tauri.bundle.windows.nsis.template
|
||||||
|
|
||||||
- name: Install AzureSignTool
|
- name: Install AzureSignTool
|
||||||
run: |
|
run: |
|
||||||
@ -234,8 +273,6 @@ jobs:
|
|||||||
# Upload for tauri updater
|
# Upload for tauri updater
|
||||||
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}
|
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}
|
||||||
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }}.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}.sig
|
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }}.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}.sig
|
||||||
|
|
||||||
aws s3 cp ./src-tauri/target/release/bundle/msi/${{ steps.metadata.outputs.MSI_FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.MSI_FILE_NAME }}
|
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
||||||
@ -252,13 +289,3 @@ jobs:
|
|||||||
asset_path: ./src-tauri/target/release/bundle/nsis/${{ steps.metadata.outputs.FILE_NAME }}
|
asset_path: ./src-tauri/target/release/bundle/nsis/${{ steps.metadata.outputs.FILE_NAME }}
|
||||||
asset_name: ${{ steps.metadata.outputs.FILE_NAME }}
|
asset_name: ${{ steps.metadata.outputs.FILE_NAME }}
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
- name: Upload release assert if public provider is github
|
|
||||||
if: inputs.public_provider == 'github'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
|
||||||
with:
|
|
||||||
upload_url: ${{ inputs.upload_url }}
|
|
||||||
asset_path: ./src-tauri/target/release/bundle/msi/${{ steps.metadata.outputs.MSI_FILE_NAME }}
|
|
||||||
asset_name: ${{ steps.metadata.outputs.MSI_FILE_NAME }}
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# Stage 1: Build stage with Node.js and Yarn v4
|
# Stage 1: Build stage with Node.js and Yarn v4
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
ARG JAN_API_BASE=https://api-dev.jan.ai/v1
|
ARG MENLO_PLATFORM_BASE_URL=https://api-dev.menlo.ai/v1
|
||||||
ENV JAN_API_BASE=$JAN_API_BASE
|
ENV MENLO_PLATFORM_BASE_URL=$MENLO_PLATFORM_BASE_URL
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
|||||||
1
Makefile
1
Makefile
@ -117,7 +117,6 @@ lint: install-and-build
|
|||||||
test: lint
|
test: lint
|
||||||
yarn download:bin
|
yarn download:bin
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
yarn download:windows-installer
|
|
||||||
endif
|
endif
|
||||||
yarn test
|
yarn test
|
||||||
yarn copy:assets:tauri
|
yarn copy:assets:tauri
|
||||||
|
|||||||
@ -2,7 +2,17 @@
|
|||||||
|
|
||||||
Internal tracker for web component changes and features.
|
Internal tracker for web component changes and features.
|
||||||
|
|
||||||
## v0.0.12 (Current)
|
## v0.0.13 (current)
|
||||||
|
**Release Date**: 2025-10-24
|
||||||
|
**Commit SHA**: 22645549cea48b1ae24b5b9dc70411fd3bfc9935
|
||||||
|
|
||||||
|
**Main Features**:
|
||||||
|
- Migrate auth to platform menlo
|
||||||
|
- Remove conv prefix
|
||||||
|
- Disable Project for web
|
||||||
|
- Model capabilites are fetched correctly from model catalog
|
||||||
|
|
||||||
|
## v0.0.12
|
||||||
**Release Date**: 2025-10-02
|
**Release Date**: 2025-10-02
|
||||||
**Commit SHA**: df145d63a93bd27336b5b539ce0719fe9c7719e3
|
**Commit SHA**: df145d63a93bd27336b5b539ce0719fe9c7719e3
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,8 @@ export RANLIB_aarch64_linux_android="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x
|
|||||||
# Additional environment variables for Rust cross-compilation
|
# Additional environment variables for Rust cross-compilation
|
||||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
|
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
|
||||||
|
|
||||||
# Only set global CC and AR for Android builds (when TAURI_ANDROID_BUILD is set)
|
# Only set global CC and AR for Android builds (when IS_ANDROID is set)
|
||||||
if [ "$TAURI_ANDROID_BUILD" = "true" ]; then
|
if [ "$IS_ANDROID" = "true" ]; then
|
||||||
export CC="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
|
export CC="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
|
||||||
export AR="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar"
|
export AR="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar"
|
||||||
echo "Global CC and AR set for Android build"
|
echo "Global CC and AR set for Android build"
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
"@vitest/coverage-v8": "^2.1.8",
|
"@vitest/coverage-v8": "^2.1.8",
|
||||||
"@vitest/ui": "^2.1.8",
|
"@vitest/ui": "^2.1.8",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"happy-dom": "^15.11.6",
|
"happy-dom": "^20.0.0",
|
||||||
"pacote": "^21.0.0",
|
"pacote": "^21.0.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
|||||||
@ -11,6 +11,8 @@ export enum ExtensionTypeEnum {
|
|||||||
HuggingFace = 'huggingFace',
|
HuggingFace = 'huggingFace',
|
||||||
Engine = 'engine',
|
Engine = 'engine',
|
||||||
Hardware = 'hardware',
|
Hardware = 'hardware',
|
||||||
|
RAG = 'rag',
|
||||||
|
VectorDB = 'vectorDB',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionType {
|
export interface ExtensionType {
|
||||||
|
|||||||
@ -182,6 +182,7 @@ export interface SessionInfo {
|
|||||||
port: number // llama-server output port (corrected from portid)
|
port: number // llama-server output port (corrected from portid)
|
||||||
model_id: string //name of the model
|
model_id: string //name of the model
|
||||||
model_path: string // path of the loaded model
|
model_path: string // path of the loaded model
|
||||||
|
is_embedding: boolean
|
||||||
api_key: string
|
api_key: string
|
||||||
mmproj_path?: string
|
mmproj_path?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,3 +23,8 @@ export { MCPExtension } from './mcp'
|
|||||||
* Base AI Engines.
|
* Base AI Engines.
|
||||||
*/
|
*/
|
||||||
export * from './engines'
|
export * from './engines'
|
||||||
|
|
||||||
|
export { RAGExtension, RAG_INTERNAL_SERVER } from './rag'
|
||||||
|
export type { AttachmentInput, IngestAttachmentsResult } from './rag'
|
||||||
|
export { VectorDBExtension } from './vector-db'
|
||||||
|
export type { SearchMode, VectorDBStatus, VectorChunkInput, VectorSearchResult, AttachmentFileInfo, VectorDBFileInput, VectorDBIngestOptions } from './vector-db'
|
||||||
|
|||||||
36
core/src/browser/extensions/rag.ts
Normal file
36
core/src/browser/extensions/rag.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
|
import type { MCPTool, MCPToolCallResult } from '../../types'
|
||||||
|
import type { AttachmentFileInfo } from './vector-db'
|
||||||
|
|
||||||
|
export interface AttachmentInput {
|
||||||
|
path: string
|
||||||
|
name?: string
|
||||||
|
type?: string
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestAttachmentsResult {
|
||||||
|
filesProcessed: number
|
||||||
|
chunksInserted: number
|
||||||
|
files: AttachmentFileInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RAG_INTERNAL_SERVER = 'rag-internal'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RAG extension base: exposes RAG tools and orchestration API.
|
||||||
|
*/
|
||||||
|
export abstract class RAGExtension extends BaseExtension {
|
||||||
|
type(): ExtensionTypeEnum | undefined {
|
||||||
|
return ExtensionTypeEnum.RAG
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getTools(): Promise<MCPTool[]>
|
||||||
|
/**
|
||||||
|
* Lightweight list of tool names for quick routing/lookup.
|
||||||
|
*/
|
||||||
|
abstract getToolNames(): Promise<string[]>
|
||||||
|
abstract callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult>
|
||||||
|
|
||||||
|
abstract ingestAttachments(threadId: string, files: AttachmentInput[]): Promise<IngestAttachmentsResult>
|
||||||
|
}
|
||||||
82
core/src/browser/extensions/vector-db.ts
Normal file
82
core/src/browser/extensions/vector-db.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
|
|
||||||
|
export type SearchMode = 'auto' | 'ann' | 'linear'
|
||||||
|
|
||||||
|
export interface VectorDBStatus {
|
||||||
|
ann_available: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorChunkInput {
|
||||||
|
text: string
|
||||||
|
embedding: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorSearchResult {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
score?: number
|
||||||
|
file_id: string
|
||||||
|
chunk_file_order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachmentFileInfo {
|
||||||
|
id: string
|
||||||
|
name?: string
|
||||||
|
path?: string
|
||||||
|
type?: string
|
||||||
|
size?: number
|
||||||
|
chunk_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-level input types for file ingestion
|
||||||
|
export interface VectorDBFileInput {
|
||||||
|
path: string
|
||||||
|
name?: string
|
||||||
|
type?: string
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorDBIngestOptions {
|
||||||
|
chunkSize: number
|
||||||
|
chunkOverlap: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vector DB extension base: abstraction over local vector storage and search.
|
||||||
|
*/
|
||||||
|
export abstract class VectorDBExtension extends BaseExtension {
|
||||||
|
type(): ExtensionTypeEnum | undefined {
|
||||||
|
return ExtensionTypeEnum.VectorDB
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getStatus(): Promise<VectorDBStatus>
|
||||||
|
abstract createCollection(threadId: string, dimension: number): Promise<void>
|
||||||
|
abstract insertChunks(
|
||||||
|
threadId: string,
|
||||||
|
fileId: string,
|
||||||
|
chunks: VectorChunkInput[]
|
||||||
|
): Promise<void>
|
||||||
|
abstract ingestFile(
|
||||||
|
threadId: string,
|
||||||
|
file: VectorDBFileInput,
|
||||||
|
opts: VectorDBIngestOptions
|
||||||
|
): Promise<AttachmentFileInfo>
|
||||||
|
abstract searchCollection(
|
||||||
|
threadId: string,
|
||||||
|
query_embedding: number[],
|
||||||
|
limit: number,
|
||||||
|
threshold: number,
|
||||||
|
mode?: SearchMode,
|
||||||
|
fileIds?: string[]
|
||||||
|
): Promise<VectorSearchResult[]>
|
||||||
|
abstract deleteChunks(threadId: string, ids: string[]): Promise<void>
|
||||||
|
abstract deleteFile(threadId: string, fileId: string): Promise<void>
|
||||||
|
abstract deleteCollection(threadId: string): Promise<void>
|
||||||
|
abstract listAttachments(threadId: string, limit?: number): Promise<AttachmentFileInfo[]>
|
||||||
|
abstract getChunks(
|
||||||
|
threadId: string,
|
||||||
|
fileId: string,
|
||||||
|
startOrder: number,
|
||||||
|
endOrder: number
|
||||||
|
): Promise<VectorSearchResult[]>
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@ export type SettingComponentProps = {
|
|||||||
extensionName?: string
|
extensionName?: string
|
||||||
requireModelReload?: boolean
|
requireModelReload?: boolean
|
||||||
configType?: ConfigType
|
configType?: ConfigType
|
||||||
|
titleKey?: string
|
||||||
|
descriptionKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigType = 'runtime' | 'setting'
|
export type ConfigType = 'runtime' | 'setting'
|
||||||
|
|||||||
BIN
docs/public/assets/images/changelog/jan-release-v0.7.0.jpeg
Normal file
BIN
docs/public/assets/images/changelog/jan-release-v0.7.0.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
28
docs/src/pages/changelog/2025-10-02-jan-projects.mdx
Normal file
28
docs/src/pages/changelog/2025-10-02-jan-projects.mdx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: "Jan v0.7.0: Jan Projects"
|
||||||
|
version: 0.7.0
|
||||||
|
description: "Jan v0.7.0 introduces Projects, model renaming, llama.cpp auto-tuning, model stats, and Azure support."
|
||||||
|
date: 2025-10-02
|
||||||
|
ogImage: "/assets/images/changelog/jan-release-v0.7.0.jpeg"
|
||||||
|
---
|
||||||
|
|
||||||
|
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||||
|
import { Callout } from 'nextra/components'
|
||||||
|
|
||||||
|
<ChangelogHeader title="Jan v0.7.0" date="2025-10-01" ogImage="/assets/images/changelog/jan-release-v0.7.0.jpeg" />
|
||||||
|
|
||||||
|
## Jan v0.7.0: Jan Projects
|
||||||
|
|
||||||
|
Jan v0.7.0 is live! This release focuses on helping you organize your workspace and better understand how models run.
|
||||||
|
|
||||||
|
### What’s new
|
||||||
|
- **Projects**: Group related chats under one project for a cleaner workflow.
|
||||||
|
- **Rename models**: Give your models custom names for easier identification.
|
||||||
|
- **Model context stats**: See context usage when a model runs.
|
||||||
|
- **Auto-loaded cloud models**: Cloud model names now appear automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Update your Jan or [download the latest version](https://jan.ai/).
|
||||||
|
|
||||||
|
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.7.0).
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "Jan v0.7.1: Fixes Windows Version Revert & OpenRouter Models"
|
||||||
|
version: 0.7.1
|
||||||
|
description: "Jan v0.7.1 focuses on bug fixes, including a windows version revert and improvements to OpenRouter models."
|
||||||
|
date: 2025-10-03
|
||||||
|
---
|
||||||
|
|
||||||
|
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||||
|
import { Callout } from 'nextra/components'
|
||||||
|
|
||||||
|
<ChangelogHeader title="Jan v0.7.1" date="2025-10-03" />
|
||||||
|
|
||||||
|
### Bug Fixes: Windows Version Revert & OpenRouter Models
|
||||||
|
|
||||||
|
#### Two quick fixes:
|
||||||
|
- Jan no longer reverts to an older version on load
|
||||||
|
- OpenRouter can now add models again
|
||||||
|
- Add headers for anthropic request to fetch models
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Update your Jan or [download the latest version](https://jan.ai/).
|
||||||
|
|
||||||
|
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.7.1).
|
||||||
|
|
||||||
|
|
||||||
25
docs/src/pages/changelog/2025-10-16-jan-security-update.mdx
Normal file
25
docs/src/pages/changelog/2025-10-16-jan-security-update.mdx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "Jan v0.7.2: Security Update"
|
||||||
|
version: 0.7.2
|
||||||
|
description: "Jan v0.7.2 updates the happy-dom dependency to v20.0.0 to address a recently disclosed sandbox vulnerability."
|
||||||
|
date: 2025-10-16
|
||||||
|
---
|
||||||
|
|
||||||
|
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||||
|
import { Callout } from 'nextra/components'
|
||||||
|
|
||||||
|
<ChangelogHeader title="Jan v0.7.2" date="2025-10-16" />
|
||||||
|
|
||||||
|
## Jan v0.7.2: Security Update (happy-dom v20)
|
||||||
|
|
||||||
|
This release focuses on **security and stability improvements**.
|
||||||
|
It updates the `happy-dom` dependency to the latest version to address a recently disclosed vulnerability.
|
||||||
|
|
||||||
|
### Security Fix
|
||||||
|
- Updated `happy-dom` to **^20.0.0**, preventing untrusted JavaScript executed within HAPPY DOM from accessing process-level functions and executing arbitrary code outside the intended sandbox.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Update your Jan or [download the latest version](https://jan.ai/).
|
||||||
|
|
||||||
|
For the complete list of changes, see the [GitHub release notes](https://github.com/janhq/jan/releases/tag/v0.7.2).
|
||||||
@ -41,7 +41,7 @@ Jan is an open-source replacement for ChatGPT:
|
|||||||
|
|
||||||
Jan is a full [product suite](https://en.wikipedia.org/wiki/Software_suite) that offers an alternative to Big AI:
|
Jan is a full [product suite](https://en.wikipedia.org/wiki/Software_suite) that offers an alternative to Big AI:
|
||||||
- [Jan Desktop](/docs/desktop/quickstart): macOS, Windows, and Linux apps with offline mode
|
- [Jan Desktop](/docs/desktop/quickstart): macOS, Windows, and Linux apps with offline mode
|
||||||
- [Jan Web](https://chat.jan.ai): Jan on browser, a direct alternative to chatgpt.com
|
- [Jan Web](https://chat.menlo.ai): Jan on browser, a direct alternative to chatgpt.com
|
||||||
- Jan Mobile: iOS and Android apps (Coming Soon)
|
- Jan Mobile: iOS and Android apps (Coming Soon)
|
||||||
- [Jan Server](/docs/server): deploy locally, in your cloud, or on-prem
|
- [Jan Server](/docs/server): deploy locally, in your cloud, or on-prem
|
||||||
- [Jan Models](/docs/models): Open-source models optimized for deep research, tool use, and reasoning
|
- [Jan Models](/docs/models): Open-source models optimized for deep research, tool use, and reasoning
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
ListConversationItemsResponse
|
ListConversationItemsResponse
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
export class RemoteApi {
|
export class RemoteApi {
|
||||||
private authService: JanAuthService
|
private authService: JanAuthService
|
||||||
@ -28,7 +28,7 @@ export class RemoteApi {
|
|||||||
async createConversation(
|
async createConversation(
|
||||||
data: Conversation
|
data: Conversation
|
||||||
): Promise<ConversationResponse> {
|
): Promise<ConversationResponse> {
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
||||||
url,
|
url,
|
||||||
@ -43,12 +43,12 @@ export class RemoteApi {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
data: Conversation
|
data: Conversation
|
||||||
): Promise<ConversationResponse> {
|
): Promise<ConversationResponse> {
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -70,7 +70,7 @@ export class RemoteApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryString = queryParams.toString()
|
const queryString = queryParams.toString()
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
|
return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
|
||||||
url,
|
url,
|
||||||
@ -114,7 +114,7 @@ export class RemoteApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteConversation(conversationId: string): Promise<void> {
|
async deleteConversation(conversationId: string): Promise<void> {
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
||||||
|
|
||||||
await this.authService.makeAuthenticatedRequest(
|
await this.authService.makeAuthenticatedRequest(
|
||||||
url,
|
url,
|
||||||
@ -141,7 +141,7 @@ export class RemoteApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryString = queryParams.toString()
|
const queryString = queryParams.toString()
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
|
return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
|
||||||
url,
|
url,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export interface ConversationResponse {
|
|||||||
id: string
|
id: string
|
||||||
object: 'conversation'
|
object: 'conversation'
|
||||||
title?: string
|
title?: string
|
||||||
created_at: number
|
created_at: number | string
|
||||||
metadata: ConversationMetadata
|
metadata: ConversationMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +50,7 @@ export interface ConversationItemAnnotation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversationItemContent {
|
export interface ConversationItemContent {
|
||||||
|
type?: string
|
||||||
file?: {
|
file?: {
|
||||||
file_id?: string
|
file_id?: string
|
||||||
mime_type?: string
|
mime_type?: string
|
||||||
@ -62,23 +63,50 @@ export interface ConversationItemContent {
|
|||||||
file_id?: string
|
file_id?: string
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
image_file?: {
|
||||||
|
file_id?: string
|
||||||
|
mime_type?: string
|
||||||
|
}
|
||||||
input_text?: string
|
input_text?: string
|
||||||
output_text?: {
|
output_text?: {
|
||||||
annotations?: ConversationItemAnnotation[]
|
annotations?: ConversationItemAnnotation[]
|
||||||
text?: string
|
text?: string
|
||||||
}
|
}
|
||||||
reasoning_content?: string
|
|
||||||
text?: {
|
text?: {
|
||||||
value?: string
|
value?: string
|
||||||
|
text?: string
|
||||||
}
|
}
|
||||||
type?: string
|
reasoning_content?: string
|
||||||
|
tool_calls?: Array<{
|
||||||
|
id?: string
|
||||||
|
type?: string
|
||||||
|
function?: {
|
||||||
|
name?: string
|
||||||
|
arguments?: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
tool_call_id?: string
|
||||||
|
tool_result?: {
|
||||||
|
content?: Array<{
|
||||||
|
type?: string
|
||||||
|
text?: string
|
||||||
|
output_text?: {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
output_text?: {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text_result?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversationItem {
|
export interface ConversationItem {
|
||||||
content?: ConversationItemContent[]
|
content?: ConversationItemContent[]
|
||||||
created_at: number
|
created_at: number | string
|
||||||
id: string
|
id: string
|
||||||
object: string
|
object: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
role: string
|
role: string
|
||||||
status?: string
|
status?: string
|
||||||
type?: string
|
type?: string
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core'
|
import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core'
|
||||||
import { Conversation, ConversationResponse, ConversationItem } from './types'
|
import { Conversation, ConversationResponse, ConversationItem, ConversationItemContent, ConversationMetadata } from './types'
|
||||||
import { DEFAULT_ASSISTANT } from './const'
|
import { DEFAULT_ASSISTANT } from './const'
|
||||||
|
|
||||||
export class ObjectParser {
|
export class ObjectParser {
|
||||||
@ -7,7 +7,7 @@ export class ObjectParser {
|
|||||||
const modelName = thread.assistants?.[0]?.model?.id || undefined
|
const modelName = thread.assistants?.[0]?.model?.id || undefined
|
||||||
const modelProvider = thread.assistants?.[0]?.model?.engine || undefined
|
const modelProvider = thread.assistants?.[0]?.model?.engine || undefined
|
||||||
const isFavorite = thread.metadata?.is_favorite?.toString() || 'false'
|
const isFavorite = thread.metadata?.is_favorite?.toString() || 'false'
|
||||||
let metadata = {}
|
let metadata: ConversationMetadata = {}
|
||||||
if (modelName && modelProvider) {
|
if (modelName && modelProvider) {
|
||||||
metadata = {
|
metadata = {
|
||||||
model_id: modelName,
|
model_id: modelName,
|
||||||
@ -23,15 +23,14 @@ export class ObjectParser {
|
|||||||
|
|
||||||
static conversationToThread(conversation: ConversationResponse): Thread {
|
static conversationToThread(conversation: ConversationResponse): Thread {
|
||||||
const assistants: ThreadAssistantInfo[] = []
|
const assistants: ThreadAssistantInfo[] = []
|
||||||
if (
|
const metadata: ConversationMetadata = conversation.metadata || {}
|
||||||
conversation.metadata?.model_id &&
|
|
||||||
conversation.metadata?.model_provider
|
if (metadata.model_id && metadata.model_provider) {
|
||||||
) {
|
|
||||||
assistants.push({
|
assistants.push({
|
||||||
...DEFAULT_ASSISTANT,
|
...DEFAULT_ASSISTANT,
|
||||||
model: {
|
model: {
|
||||||
id: conversation.metadata.model_id,
|
id: metadata.model_id,
|
||||||
engine: conversation.metadata.model_provider,
|
engine: metadata.model_provider,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -44,16 +43,18 @@ export class ObjectParser {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFavorite = conversation.metadata?.is_favorite === 'true'
|
const isFavorite = metadata.is_favorite === 'true'
|
||||||
|
const createdAtMs = parseTimestamp(conversation.created_at)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: conversation.id,
|
id: conversation.id,
|
||||||
title: conversation.title || '',
|
title: conversation.title || '',
|
||||||
assistants,
|
assistants,
|
||||||
created: conversation.created_at,
|
created: createdAtMs,
|
||||||
updated: conversation.created_at,
|
updated: createdAtMs,
|
||||||
model: {
|
model: {
|
||||||
id: conversation.metadata.model_id,
|
id: metadata.model_id,
|
||||||
provider: conversation.metadata.model_provider,
|
provider: metadata.model_provider,
|
||||||
},
|
},
|
||||||
isFavorite,
|
isFavorite,
|
||||||
metadata: { is_favorite: isFavorite },
|
metadata: { is_favorite: isFavorite },
|
||||||
@ -65,74 +66,70 @@ export class ObjectParser {
|
|||||||
threadId: string
|
threadId: string
|
||||||
): ThreadMessage {
|
): ThreadMessage {
|
||||||
// Extract text content and metadata from the item
|
// Extract text content and metadata from the item
|
||||||
let textContent = ''
|
const textSegments: string[] = []
|
||||||
let reasoningContent = ''
|
const reasoningSegments: string[] = []
|
||||||
const imageUrls: string[] = []
|
const imageUrls: string[] = []
|
||||||
let toolCalls: any[] = []
|
let toolCalls: any[] = []
|
||||||
let finishReason = ''
|
|
||||||
|
|
||||||
if (item.content && item.content.length > 0) {
|
if (item.content && item.content.length > 0) {
|
||||||
for (const content of item.content) {
|
for (const content of item.content) {
|
||||||
// Handle text content
|
extractContentByType(content, {
|
||||||
if (content.text?.value) {
|
onText: (value) => {
|
||||||
textContent = content.text.value
|
if (value) {
|
||||||
}
|
textSegments.push(value)
|
||||||
// Handle output_text for assistant messages
|
}
|
||||||
if (content.output_text?.text) {
|
},
|
||||||
textContent = content.output_text.text
|
onReasoning: (value) => {
|
||||||
}
|
if (value) {
|
||||||
// Handle reasoning content
|
reasoningSegments.push(value)
|
||||||
if (content.reasoning_content) {
|
}
|
||||||
reasoningContent = content.reasoning_content
|
},
|
||||||
}
|
onImage: (url) => {
|
||||||
// Handle image content
|
if (url) {
|
||||||
if (content.image?.url) {
|
imageUrls.push(url)
|
||||||
imageUrls.push(content.image.url)
|
}
|
||||||
}
|
},
|
||||||
// Extract finish_reason
|
onToolCalls: (calls) => {
|
||||||
if (content.finish_reason) {
|
toolCalls = calls.map((toolCall) => {
|
||||||
finishReason = content.finish_reason
|
const callId = toolCall.id || 'unknown'
|
||||||
}
|
const rawArgs = toolCall.function?.arguments
|
||||||
}
|
const normalizedArgs =
|
||||||
}
|
typeof rawArgs === 'string'
|
||||||
|
? rawArgs
|
||||||
// Handle tool calls parsing for assistant messages
|
: JSON.stringify(rawArgs ?? {})
|
||||||
if (item.role === 'assistant' && finishReason === 'tool_calls') {
|
return {
|
||||||
try {
|
id: callId,
|
||||||
// Tool calls are embedded as JSON string in textContent
|
tool_call_id: callId,
|
||||||
const toolCallMatch = textContent.match(/\[.*\]/)
|
tool: {
|
||||||
if (toolCallMatch) {
|
id: callId,
|
||||||
const toolCallsData = JSON.parse(toolCallMatch[0])
|
function: {
|
||||||
toolCalls = toolCallsData.map((toolCall: any) => ({
|
name: toolCall.function?.name || 'unknown',
|
||||||
tool: {
|
arguments: normalizedArgs,
|
||||||
id: toolCall.id || 'unknown',
|
},
|
||||||
function: {
|
type: toolCall.type || 'function',
|
||||||
name: toolCall.function?.name || 'unknown',
|
},
|
||||||
arguments: toolCall.function?.arguments || '{}'
|
response: {
|
||||||
},
|
error: '',
|
||||||
type: toolCall.type || 'function'
|
content: [],
|
||||||
},
|
},
|
||||||
response: {
|
state: 'pending',
|
||||||
error: '',
|
}
|
||||||
content: []
|
})
|
||||||
},
|
},
|
||||||
state: 'ready'
|
})
|
||||||
}))
|
|
||||||
// Remove tool calls JSON from text content, keep only reasoning
|
|
||||||
textContent = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse tool calls:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format final content with reasoning if present
|
// Format final content with reasoning if present
|
||||||
let finalTextValue = ''
|
let finalTextValue = ''
|
||||||
if (reasoningContent) {
|
if (reasoningSegments.length > 0) {
|
||||||
finalTextValue = `<think>${reasoningContent}</think>`
|
finalTextValue += `<think>${reasoningSegments.join('\n')}</think>`
|
||||||
}
|
}
|
||||||
if (textContent) {
|
if (textSegments.length > 0) {
|
||||||
finalTextValue += textContent
|
if (finalTextValue) {
|
||||||
|
finalTextValue += '\n'
|
||||||
|
}
|
||||||
|
finalTextValue += textSegments.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build content array for ThreadMessage
|
// Build content array for ThreadMessage
|
||||||
@ -157,22 +154,26 @@ export class ObjectParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build metadata
|
// Build metadata
|
||||||
const metadata: any = {}
|
const metadata: any = { ...(item.metadata || {}) }
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
metadata.tool_calls = toolCalls
|
metadata.tool_calls = toolCalls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createdAtMs = parseTimestamp(item.created_at)
|
||||||
|
|
||||||
// Map status from server format to frontend format
|
// Map status from server format to frontend format
|
||||||
const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready'
|
const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready'
|
||||||
|
|
||||||
|
const role = item.role === 'user' || item.role === 'assistant' ? item.role : 'assistant'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
id: item.id,
|
id: item.id,
|
||||||
object: 'thread.message',
|
object: 'thread.message',
|
||||||
thread_id: threadId,
|
thread_id: threadId,
|
||||||
role: item.role as 'user' | 'assistant',
|
role,
|
||||||
content: messageContent,
|
content: messageContent,
|
||||||
created_at: item.created_at * 1000, // Convert to milliseconds
|
created_at: createdAtMs,
|
||||||
completed_at: 0,
|
completed_at: 0,
|
||||||
status: mappedStatus,
|
status: mappedStatus,
|
||||||
metadata,
|
metadata,
|
||||||
@ -201,25 +202,46 @@ export const combineConversationItemsToMessages = (
|
|||||||
): ThreadMessage[] => {
|
): ThreadMessage[] => {
|
||||||
const messages: ThreadMessage[] = []
|
const messages: ThreadMessage[] = []
|
||||||
const toolResponseMap = new Map<string, any>()
|
const toolResponseMap = new Map<string, any>()
|
||||||
|
const sortedItems = [...items].sort(
|
||||||
|
(a, b) => parseTimestamp(a.created_at) - parseTimestamp(b.created_at)
|
||||||
|
)
|
||||||
|
|
||||||
// First pass: collect tool responses
|
// First pass: collect tool responses
|
||||||
for (const item of items) {
|
for (const item of sortedItems) {
|
||||||
if (item.role === 'tool') {
|
if (item.role === 'tool') {
|
||||||
const toolContent = item.content?.[0]?.text?.value || ''
|
for (const content of item.content ?? []) {
|
||||||
toolResponseMap.set(item.id, {
|
const toolCallId = content.tool_call_id || item.id
|
||||||
error: '',
|
const toolResultText =
|
||||||
content: [
|
content.tool_result?.output_text?.text ||
|
||||||
{
|
(Array.isArray(content.tool_result?.content)
|
||||||
type: 'text',
|
? content.tool_result?.content
|
||||||
text: toolContent
|
?.map((entry) => entry.text || entry.output_text?.text)
|
||||||
}
|
.filter((text): text is string => Boolean(text))
|
||||||
]
|
.join('\n')
|
||||||
})
|
: undefined)
|
||||||
|
const toolContent =
|
||||||
|
content.text?.text ||
|
||||||
|
content.text?.value ||
|
||||||
|
content.output_text?.text ||
|
||||||
|
content.input_text ||
|
||||||
|
content.text_result ||
|
||||||
|
toolResultText ||
|
||||||
|
''
|
||||||
|
toolResponseMap.set(toolCallId, {
|
||||||
|
error: '',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: toolContent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: build messages and merge tool responses
|
// Second pass: build messages and merge tool responses
|
||||||
for (const item of items) {
|
for (const item of sortedItems) {
|
||||||
// Skip tool messages as they will be merged into assistant messages
|
// Skip tool messages as they will be merged into assistant messages
|
||||||
if (item.role === 'tool') {
|
if (item.role === 'tool') {
|
||||||
continue
|
continue
|
||||||
@ -228,14 +250,35 @@ export const combineConversationItemsToMessages = (
|
|||||||
const message = ObjectParser.conversationItemToThreadMessage(item, threadId)
|
const message = ObjectParser.conversationItemToThreadMessage(item, threadId)
|
||||||
|
|
||||||
// If this is an assistant message with tool calls, merge tool responses
|
// If this is an assistant message with tool calls, merge tool responses
|
||||||
if (message.role === 'assistant' && message.metadata?.tool_calls && Array.isArray(message.metadata.tool_calls)) {
|
if (
|
||||||
|
message.role === 'assistant' &&
|
||||||
|
message.metadata?.tool_calls &&
|
||||||
|
Array.isArray(message.metadata.tool_calls)
|
||||||
|
) {
|
||||||
const toolCalls = message.metadata.tool_calls as any[]
|
const toolCalls = message.metadata.tool_calls as any[]
|
||||||
let toolResponseIndex = 0
|
|
||||||
|
|
||||||
for (const [responseId, responseData] of toolResponseMap.entries()) {
|
for (const toolCall of toolCalls) {
|
||||||
if (toolResponseIndex < toolCalls.length) {
|
const callId = toolCall.tool_call_id || toolCall.id || toolCall.tool?.id
|
||||||
toolCalls[toolResponseIndex].response = responseData
|
let responseKey: string | undefined
|
||||||
toolResponseIndex++
|
let response: any = null
|
||||||
|
|
||||||
|
if (callId && toolResponseMap.has(callId)) {
|
||||||
|
responseKey = callId
|
||||||
|
response = toolResponseMap.get(callId)
|
||||||
|
} else {
|
||||||
|
const iterator = toolResponseMap.entries().next()
|
||||||
|
if (!iterator.done) {
|
||||||
|
responseKey = iterator.value[0]
|
||||||
|
response = iterator.value[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
toolCall.response = response
|
||||||
|
toolCall.state = 'succeeded'
|
||||||
|
if (responseKey) {
|
||||||
|
toolResponseMap.delete(responseKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,3 +288,79 @@ export const combineConversationItemsToMessages = (
|
|||||||
|
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseTimestamp = (value: number | string | undefined): number => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
// Distinguish between seconds and milliseconds
|
||||||
|
return value > 1e12 ? value : value * 1000
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Date.parse(value)
|
||||||
|
return Number.isNaN(parsed) ? Date.now() : parsed
|
||||||
|
}
|
||||||
|
return Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractContentByType = (
|
||||||
|
content: ConversationItemContent,
|
||||||
|
handlers: {
|
||||||
|
onText: (value: string) => void
|
||||||
|
onReasoning: (value: string) => void
|
||||||
|
onImage: (url: string) => void
|
||||||
|
onToolCalls: (calls: NonNullable<ConversationItemContent['tool_calls']>) => void
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const type = content.type || ''
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'input_text':
|
||||||
|
handlers.onText(content.input_text || '')
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
handlers.onText(content.text?.text || content.text?.value || '')
|
||||||
|
break
|
||||||
|
case 'output_text':
|
||||||
|
handlers.onText(content.output_text?.text || '')
|
||||||
|
break
|
||||||
|
case 'reasoning_content':
|
||||||
|
handlers.onReasoning(content.reasoning_content || '')
|
||||||
|
break
|
||||||
|
case 'image':
|
||||||
|
case 'image_url':
|
||||||
|
if (content.image?.url) {
|
||||||
|
handlers.onImage(content.image.url)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'tool_calls':
|
||||||
|
if (content.tool_calls && Array.isArray(content.tool_calls)) {
|
||||||
|
handlers.onToolCalls(content.tool_calls)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'tool_result':
|
||||||
|
if (content.tool_result?.output_text?.text) {
|
||||||
|
handlers.onText(content.tool_result.output_text.text)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// Fallback for legacy fields without explicit type
|
||||||
|
if (content.text?.value || content.text?.text) {
|
||||||
|
handlers.onText(content.text.value || content.text.text || '')
|
||||||
|
}
|
||||||
|
if (content.text_result) {
|
||||||
|
handlers.onText(content.text_result)
|
||||||
|
}
|
||||||
|
if (content.output_text?.text) {
|
||||||
|
handlers.onText(content.output_text.text)
|
||||||
|
}
|
||||||
|
if (content.reasoning_content) {
|
||||||
|
handlers.onReasoning(content.reasoning_content)
|
||||||
|
}
|
||||||
|
if (content.image?.url) {
|
||||||
|
handlers.onImage(content.image.url)
|
||||||
|
}
|
||||||
|
if (content.tool_calls && Array.isArray(content.tool_calls)) {
|
||||||
|
handlers.onToolCalls(content.tool_calls)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,10 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getSharedAuthService, JanAuthService } from '../shared'
|
import { getSharedAuthService, JanAuthService } from '../shared'
|
||||||
import { JanModel, janProviderStore } from './store'
|
|
||||||
import { ApiError } from '../shared/types/errors'
|
import { ApiError } from '../shared/types/errors'
|
||||||
|
import { JAN_API_ROUTES } from './const'
|
||||||
|
import { JanModel, janProviderStore } from './store'
|
||||||
|
|
||||||
// JAN_API_BASE is defined in vite.config.ts
|
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const TEMPORARY_CHAT_ID = 'temporary-chat'
|
const TEMPORARY_CHAT_ID = 'temporary-chat'
|
||||||
@ -19,12 +20,7 @@ const TEMPORARY_CHAT_ID = 'temporary-chat'
|
|||||||
*/
|
*/
|
||||||
function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) {
|
function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) {
|
||||||
const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID
|
const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID
|
||||||
|
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.CHAT_COMPLETIONS}`
|
||||||
// For temporary chats, use the stateless /chat/completions endpoint
|
|
||||||
// For regular conversations, use the stateful /conv/chat/completions endpoint
|
|
||||||
const endpoint = isTemporaryChat
|
|
||||||
? `${JAN_API_BASE}/chat/completions`
|
|
||||||
: `${JAN_API_BASE}/conv/chat/completions`
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...request,
|
...request,
|
||||||
@ -44,9 +40,30 @@ function getChatCompletionConfig(request: JanChatCompletionRequest, stream: bool
|
|||||||
return { endpoint, payload, isTemporaryChat }
|
return { endpoint, payload, isTemporaryChat }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JanModelsResponse {
|
interface JanModelSummary {
|
||||||
|
id: string
|
||||||
object: string
|
object: string
|
||||||
data: JanModel[]
|
owned_by: string
|
||||||
|
created?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JanModelsResponse {
|
||||||
|
object: string
|
||||||
|
data: JanModelSummary[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JanModelCatalogResponse {
|
||||||
|
id: string
|
||||||
|
supported_parameters?: {
|
||||||
|
names?: string[]
|
||||||
|
default?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
extras?: {
|
||||||
|
supported_parameters?: string[]
|
||||||
|
default_parameters?: Record<string, unknown>
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JanChatMessage {
|
export interface JanChatMessage {
|
||||||
@ -112,6 +129,8 @@ export interface JanChatCompletionChunk {
|
|||||||
export class JanApiClient {
|
export class JanApiClient {
|
||||||
private static instance: JanApiClient
|
private static instance: JanApiClient
|
||||||
private authService: JanAuthService
|
private authService: JanAuthService
|
||||||
|
private modelsCache: JanModel[] | null = null
|
||||||
|
private modelsFetchPromise: Promise<JanModel[]> | null = null
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.authService = getSharedAuthService()
|
this.authService = getSharedAuthService()
|
||||||
@ -124,25 +143,64 @@ export class JanApiClient {
|
|||||||
return JanApiClient.instance
|
return JanApiClient.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
async getModels(): Promise<JanModel[]> {
|
async getModels(options?: { forceRefresh?: boolean }): Promise<JanModel[]> {
|
||||||
try {
|
try {
|
||||||
|
const forceRefresh = options?.forceRefresh ?? false
|
||||||
|
|
||||||
|
if (forceRefresh) {
|
||||||
|
this.modelsCache = null
|
||||||
|
} else if (this.modelsCache) {
|
||||||
|
return this.modelsCache
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.modelsFetchPromise) {
|
||||||
|
return this.modelsFetchPromise
|
||||||
|
}
|
||||||
|
|
||||||
janProviderStore.setLoadingModels(true)
|
janProviderStore.setLoadingModels(true)
|
||||||
janProviderStore.clearError()
|
janProviderStore.clearError()
|
||||||
|
|
||||||
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
|
this.modelsFetchPromise = (async () => {
|
||||||
`${JAN_API_BASE}/conv/models`
|
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
|
||||||
)
|
`${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODELS}`
|
||||||
|
)
|
||||||
|
|
||||||
const models = response.data || []
|
const summaries = response.data || []
|
||||||
janProviderStore.setModels(models)
|
|
||||||
|
|
||||||
return models
|
const models: JanModel[] = await Promise.all(
|
||||||
|
summaries.map(async (summary) => {
|
||||||
|
const supportedParameters = await this.fetchSupportedParameters(summary.id)
|
||||||
|
const capabilities = this.deriveCapabilitiesFromParameters(supportedParameters)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: summary.id,
|
||||||
|
object: summary.object,
|
||||||
|
owned_by: summary.owned_by,
|
||||||
|
created: summary.created,
|
||||||
|
capabilities,
|
||||||
|
supportedParameters,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
this.modelsCache = models
|
||||||
|
janProviderStore.setModels(models)
|
||||||
|
|
||||||
|
return models
|
||||||
|
})()
|
||||||
|
|
||||||
|
return await this.modelsFetchPromise
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.modelsCache = null
|
||||||
|
this.modelsFetchPromise = null
|
||||||
|
|
||||||
const errorMessage = error instanceof ApiError ? error.message :
|
const errorMessage = error instanceof ApiError ? error.message :
|
||||||
error instanceof Error ? error.message : 'Failed to fetch models'
|
error instanceof Error ? error.message : 'Failed to fetch models'
|
||||||
janProviderStore.setError(errorMessage)
|
janProviderStore.setError(errorMessage)
|
||||||
janProviderStore.setLoadingModels(false)
|
janProviderStore.setLoadingModels(false)
|
||||||
throw error
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.modelsFetchPromise = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +312,7 @@ export class JanApiClient {
|
|||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
janProviderStore.setAuthenticated(true)
|
janProviderStore.setAuthenticated(true)
|
||||||
// Fetch initial models
|
// Fetch initial models (cached for subsequent calls)
|
||||||
await this.getModels()
|
await this.getModels()
|
||||||
console.log('Jan API client initialized successfully')
|
console.log('Jan API client initialized successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -266,6 +324,52 @@ export class JanApiClient {
|
|||||||
janProviderStore.setInitializing(false)
|
janProviderStore.setInitializing(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fetchSupportedParameters(modelId: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODEL_CATALOGS}/${this.encodeModelIdForCatalog(modelId)}`
|
||||||
|
const catalog = await this.authService.makeAuthenticatedRequest<JanModelCatalogResponse>(endpoint)
|
||||||
|
return this.extractSupportedParameters(catalog)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch catalog metadata for model "${modelId}":`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private encodeModelIdForCatalog(modelId: string): string {
|
||||||
|
return modelId
|
||||||
|
.split('/')
|
||||||
|
.map((segment) => encodeURIComponent(segment))
|
||||||
|
.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractSupportedParameters(catalog: JanModelCatalogResponse | null | undefined): string[] {
|
||||||
|
if (!catalog) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryNames = catalog.supported_parameters?.names
|
||||||
|
if (Array.isArray(primaryNames) && primaryNames.length > 0) {
|
||||||
|
return [...new Set(primaryNames)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraNames = catalog.extras?.supported_parameters
|
||||||
|
if (Array.isArray(extraNames) && extraNames.length > 0) {
|
||||||
|
return [...new Set(extraNames)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
private deriveCapabilitiesFromParameters(parameters: string[]): string[] {
|
||||||
|
const capabilities = new Set<string>()
|
||||||
|
|
||||||
|
if (parameters.includes('tools')) {
|
||||||
|
capabilities.add('tools')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(capabilities)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const janApiClient = JanApiClient.getInstance()
|
export const janApiClient = JanApiClient.getInstance()
|
||||||
|
|||||||
7
extensions-web/src/jan-provider-web/const.ts
Normal file
7
extensions-web/src/jan-provider-web/const.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const JAN_API_ROUTES = {
|
||||||
|
MODELS: '/models',
|
||||||
|
CHAT_COMPLETIONS: '/chat/completions',
|
||||||
|
MODEL_CATALOGS: '/models/catalogs',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const MODEL_PROVIDER_STORAGE_KEY = 'model-provider'
|
||||||
122
extensions-web/src/jan-provider-web/helpers.ts
Normal file
122
extensions-web/src/jan-provider-web/helpers.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import type { JanModel } from './store'
|
||||||
|
import { MODEL_PROVIDER_STORAGE_KEY } from './const'
|
||||||
|
|
||||||
|
type StoredModel = {
|
||||||
|
id?: string
|
||||||
|
capabilities?: unknown
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoredProvider = {
|
||||||
|
provider?: string
|
||||||
|
models?: StoredModel[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoredState = {
|
||||||
|
state?: {
|
||||||
|
providers?: StoredProvider[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
version?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeCapabilities = (capabilities: unknown): string[] => {
|
||||||
|
if (!Array.isArray(capabilities)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(capabilities.filter((item): item is string => typeof item === 'string'))].sort(
|
||||||
|
(a, b) => a.localeCompare(b)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize Jan models stored in localStorage with the latest server state.
|
||||||
|
* Returns true if the stored data was modified (including being cleared).
|
||||||
|
*/
|
||||||
|
export function syncJanModelsLocalStorage(
|
||||||
|
remoteModels: JanModel[],
|
||||||
|
storageKey: string = MODEL_PROVIDER_STORAGE_KEY
|
||||||
|
): boolean {
|
||||||
|
const rawStorage = localStorage.getItem(storageKey)
|
||||||
|
if (!rawStorage) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let storedState: StoredState
|
||||||
|
try {
|
||||||
|
storedState = JSON.parse(rawStorage) as StoredState
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse Jan model storage; clearing entry.', error)
|
||||||
|
localStorage.removeItem(storageKey)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = storedState?.state?.providers
|
||||||
|
if (!Array.isArray(providers)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteModelMap = new Map(remoteModels.map((model) => [model.id, model]))
|
||||||
|
let storageUpdated = false
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
if (provider.provider !== 'jan' || !Array.isArray(provider.models)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedModels: StoredModel[] = []
|
||||||
|
|
||||||
|
for (const model of provider.models) {
|
||||||
|
const modelId = typeof model.id === 'string' ? model.id : null
|
||||||
|
if (!modelId) {
|
||||||
|
storageUpdated = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteModel = remoteModelMap.get(modelId)
|
||||||
|
if (!remoteModel) {
|
||||||
|
console.log(`Removing unknown Jan model from localStorage: ${modelId}`)
|
||||||
|
storageUpdated = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedCapabilities = normalizeCapabilities(model.capabilities)
|
||||||
|
const remoteCapabilities = normalizeCapabilities(remoteModel.capabilities)
|
||||||
|
|
||||||
|
const capabilitiesMatch =
|
||||||
|
storedCapabilities.length === remoteCapabilities.length &&
|
||||||
|
storedCapabilities.every((cap, index) => cap === remoteCapabilities[index])
|
||||||
|
|
||||||
|
if (!capabilitiesMatch) {
|
||||||
|
console.log(
|
||||||
|
`Updating capabilities for Jan model ${modelId}:`,
|
||||||
|
storedCapabilities,
|
||||||
|
'=>',
|
||||||
|
remoteCapabilities
|
||||||
|
)
|
||||||
|
updatedModels.push({
|
||||||
|
...model,
|
||||||
|
capabilities: remoteModel.capabilities,
|
||||||
|
})
|
||||||
|
storageUpdated = true
|
||||||
|
} else {
|
||||||
|
updatedModels.push(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedModels.length !== provider.models.length) {
|
||||||
|
storageUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.models = updatedModels
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageUpdated) {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(storedState))
|
||||||
|
}
|
||||||
|
|
||||||
|
return storageUpdated
|
||||||
|
}
|
||||||
@ -14,12 +14,10 @@ import {
|
|||||||
ImportOptions,
|
ImportOptions,
|
||||||
} from '@janhq/core' // cspell: disable-line
|
} from '@janhq/core' // cspell: disable-line
|
||||||
import { janApiClient, JanChatMessage } from './api'
|
import { janApiClient, JanChatMessage } from './api'
|
||||||
|
import { syncJanModelsLocalStorage } from './helpers'
|
||||||
import { janProviderStore } from './store'
|
import { janProviderStore } from './store'
|
||||||
import { ApiError } from '../shared/types/errors'
|
import { ApiError } from '../shared/types/errors'
|
||||||
|
|
||||||
// Jan models support tools via MCP
|
|
||||||
const JAN_MODEL_CAPABILITIES = ['tools'] as const
|
|
||||||
|
|
||||||
export default class JanProviderWeb extends AIEngine {
|
export default class JanProviderWeb extends AIEngine {
|
||||||
readonly provider = 'jan'
|
readonly provider = 'jan'
|
||||||
private activeSessions: Map<string, SessionInfo> = new Map()
|
private activeSessions: Map<string, SessionInfo> = new Map()
|
||||||
@ -28,11 +26,11 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
console.log('Loading Jan Provider Extension...')
|
console.log('Loading Jan Provider Extension...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check and clear invalid Jan models (capabilities mismatch)
|
// Initialize authentication
|
||||||
this.validateJanModelsLocalStorage()
|
|
||||||
|
|
||||||
// Initialize authentication and fetch models
|
|
||||||
await janApiClient.initialize()
|
await janApiClient.initialize()
|
||||||
|
// Check and sync stored Jan models against latest catalog data
|
||||||
|
await this.validateJanModelsLocalStorage()
|
||||||
|
|
||||||
console.log('Jan Provider Extension loaded successfully')
|
console.log('Jan Provider Extension loaded successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load Jan Provider Extension:', error)
|
console.error('Failed to load Jan Provider Extension:', error)
|
||||||
@ -43,46 +41,17 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify Jan models capabilities in localStorage
|
// Verify Jan models capabilities in localStorage
|
||||||
private validateJanModelsLocalStorage() {
|
private async validateJanModelsLocalStorage(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log("Validating Jan models in localStorage...")
|
console.log('Validating Jan models in localStorage...')
|
||||||
const storageKey = 'model-provider'
|
|
||||||
const data = localStorage.getItem(storageKey)
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
const parsed = JSON.parse(data)
|
const remoteModels = await janApiClient.getModels()
|
||||||
if (!parsed?.state?.providers) return
|
const storageUpdated = syncJanModelsLocalStorage(remoteModels)
|
||||||
|
|
||||||
// Check if any Jan model has incorrect capabilities
|
if (storageUpdated) {
|
||||||
let hasInvalidModel = false
|
console.log(
|
||||||
|
'Synchronized Jan models in localStorage with server capabilities; reloading...'
|
||||||
for (const provider of parsed.state.providers) {
|
)
|
||||||
if (provider.provider === 'jan' && provider.models) {
|
|
||||||
for (const model of provider.models) {
|
|
||||||
console.log(`Checking Jan model: ${model.id}`, model.capabilities)
|
|
||||||
if (JSON.stringify(model.capabilities) !== JSON.stringify(JAN_MODEL_CAPABILITIES)) {
|
|
||||||
hasInvalidModel = true
|
|
||||||
console.log(`Found invalid Jan model: ${model.id}, clearing localStorage`)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasInvalidModel) break
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any invalid model found, just clear the storage
|
|
||||||
if (hasInvalidModel) {
|
|
||||||
// Force clear the storage
|
|
||||||
localStorage.removeItem(storageKey)
|
|
||||||
// Verify it's actually removed
|
|
||||||
const afterRemoval = localStorage.getItem(storageKey)
|
|
||||||
// If still present, try setting to empty state
|
|
||||||
if (afterRemoval) {
|
|
||||||
// Try alternative clearing method
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify({ state: { providers: [] }, version: parsed.version || 3 }))
|
|
||||||
}
|
|
||||||
console.log('Cleared model-provider from localStorage due to invalid Jan capabilities')
|
|
||||||
// Force a page reload to ensure clean state
|
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -119,7 +88,7 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
path: undefined, // Remote model, no local path
|
path: undefined, // Remote model, no local path
|
||||||
owned_by: model.owned_by,
|
owned_by: model.owned_by,
|
||||||
object: model.object,
|
object: model.object,
|
||||||
capabilities: [...JAN_MODEL_CAPABILITIES],
|
capabilities: [...model.capabilities],
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
)
|
)
|
||||||
@ -140,7 +109,7 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
path: undefined, // Remote model, no local path
|
path: undefined, // Remote model, no local path
|
||||||
owned_by: model.owned_by,
|
owned_by: model.owned_by,
|
||||||
object: model.object,
|
object: model.object,
|
||||||
capabilities: [...JAN_MODEL_CAPABILITIES],
|
capabilities: [...model.capabilities],
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to list Jan models:', error)
|
console.error('Failed to list Jan models:', error)
|
||||||
@ -159,6 +128,7 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
port: 443, // HTTPS port
|
port: 443, // HTTPS port
|
||||||
model_id: modelId,
|
model_id: modelId,
|
||||||
model_path: `remote:${modelId}`, // Indicate this is a remote model
|
model_path: `remote:${modelId}`, // Indicate this is a remote model
|
||||||
|
is_embedding: false, // assume false here, TODO: might need further implementation
|
||||||
api_key: '', // API key handled by auth service
|
api_key: '', // API key handled by auth service
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,8 +163,12 @@ export default class JanProviderWeb extends AIEngine {
|
|||||||
console.error(`Failed to unload Jan session ${sessionId}:`, error)
|
console.error(`Failed to unload Jan session ${sessionId}:`, error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof ApiError ? error.message :
|
error:
|
||||||
error instanceof Error ? error.message : 'Unknown error',
|
error instanceof ApiError
|
||||||
|
? error.message
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,9 @@ export interface JanModel {
|
|||||||
id: string
|
id: string
|
||||||
object: string
|
object: string
|
||||||
owned_by: string
|
owned_by: string
|
||||||
|
created?: number
|
||||||
|
capabilities: string[]
|
||||||
|
supportedParameters?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JanProviderState {
|
export interface JanProviderState {
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { JanMCPOAuthProvider } from './oauth-provider'
|
|||||||
import { WebSearchButton } from './components'
|
import { WebSearchButton } from './components'
|
||||||
import type { ComponentType } from 'react'
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
// JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1')
|
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts (defaults to 'https://api-dev.menlo.ai/jan/v1')
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
export default class MCPExtensionWeb extends MCPExtension {
|
export default class MCPExtensionWeb extends MCPExtension {
|
||||||
private mcpEndpoint = '/mcp'
|
private mcpEndpoint = '/mcp'
|
||||||
@ -77,7 +77,7 @@ export default class MCPExtensionWeb extends MCPExtension {
|
|||||||
|
|
||||||
// Create transport with OAuth provider (handles token refresh automatically)
|
// Create transport with OAuth provider (handles token refresh automatically)
|
||||||
const transport = new StreamableHTTPClientTransport(
|
const transport = new StreamableHTTPClientTransport(
|
||||||
new URL(`${JAN_API_BASE}${this.mcpEndpoint}`),
|
new URL(`${MENLO_PLATFORM_BASE_URL}${this.mcpEndpoint}`),
|
||||||
{
|
{
|
||||||
authProvider: this.oauthProvider
|
authProvider: this.oauthProvider
|
||||||
// No sessionId needed - server will generate one automatically
|
// No sessionId needed - server will generate one automatically
|
||||||
|
|||||||
@ -6,13 +6,13 @@
|
|||||||
import { AuthTokens } from './types'
|
import { AuthTokens } from './types'
|
||||||
import { AUTH_ENDPOINTS } from './const'
|
import { AUTH_ENDPOINTS } from './const'
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout user on server
|
* Logout user on server
|
||||||
*/
|
*/
|
||||||
export async function logoutUser(): Promise<void> {
|
export async function logoutUser(): Promise<void> {
|
||||||
const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.LOGOUT}`, {
|
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.LOGOUT}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@ -29,7 +29,7 @@ export async function logoutUser(): Promise<void> {
|
|||||||
* Guest login
|
* Guest login
|
||||||
*/
|
*/
|
||||||
export async function guestLogin(): Promise<AuthTokens> {
|
export async function guestLogin(): Promise<AuthTokens> {
|
||||||
const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
|
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@ -51,7 +51,7 @@ export async function guestLogin(): Promise<AuthTokens> {
|
|||||||
*/
|
*/
|
||||||
export async function refreshToken(): Promise<AuthTokens> {
|
export async function refreshToken(): Promise<AuthTokens> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${JAN_API_BASE}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
|
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
import { AuthTokens, LoginUrlResponse } from './types'
|
import { AuthTokens, LoginUrlResponse } from './types'
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> {
|
export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> {
|
||||||
const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
|
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@ -30,7 +30,7 @@ export async function handleOAuthCallback(
|
|||||||
code: string,
|
code: string,
|
||||||
state?: string
|
state?: string
|
||||||
): Promise<AuthTokens> {
|
): Promise<AuthTokens> {
|
||||||
const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
|
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
* Handles authentication flows for any OAuth provider
|
* Handles authentication flows for any OAuth provider
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
import { User, AuthState, AuthBroadcastMessage } from './types'
|
import { User, AuthState, AuthBroadcastMessage, AuthTokens } from './types'
|
||||||
import {
|
import {
|
||||||
AUTH_STORAGE_KEYS,
|
AUTH_STORAGE_KEYS,
|
||||||
AUTH_ENDPOINTS,
|
AUTH_ENDPOINTS,
|
||||||
@ -115,7 +115,7 @@ export class JanAuthService {
|
|||||||
|
|
||||||
// Store tokens and set authenticated state
|
// Store tokens and set authenticated state
|
||||||
this.accessToken = tokens.access_token
|
this.accessToken = tokens.access_token
|
||||||
this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
|
this.tokenExpiryTime = this.computeTokenExpiry(tokens)
|
||||||
this.setAuthProvider(providerId)
|
this.setAuthProvider(providerId)
|
||||||
|
|
||||||
this.authBroadcast.broadcastLogin()
|
this.authBroadcast.broadcastLogin()
|
||||||
@ -158,7 +158,7 @@ export class JanAuthService {
|
|||||||
const tokens = await refreshToken()
|
const tokens = await refreshToken()
|
||||||
|
|
||||||
this.accessToken = tokens.access_token
|
this.accessToken = tokens.access_token
|
||||||
this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
|
this.tokenExpiryTime = this.computeTokenExpiry(tokens)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh access token:', error)
|
console.error('Failed to refresh access token:', error)
|
||||||
if (error instanceof ApiError && error.isStatus(401)) {
|
if (error instanceof ApiError && error.isStatus(401)) {
|
||||||
@ -343,6 +343,23 @@ export class JanAuthService {
|
|||||||
localStorage.removeItem(AUTH_STORAGE_KEYS.AUTH_PROVIDER)
|
localStorage.removeItem(AUTH_STORAGE_KEYS.AUTH_PROVIDER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeTokenExpiry(tokens: AuthTokens): number {
|
||||||
|
if (tokens.expires_at) {
|
||||||
|
const expiresAt = new Date(tokens.expires_at).getTime()
|
||||||
|
if (!Number.isNaN(expiresAt)) {
|
||||||
|
return expiresAt
|
||||||
|
}
|
||||||
|
console.warn('Invalid expires_at format in auth tokens:', tokens.expires_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tokens.expires_in === 'number') {
|
||||||
|
return Date.now() + tokens.expires_in * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Auth tokens missing expiry information; defaulting to immediate expiry')
|
||||||
|
return Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure guest access is available
|
* Ensure guest access is available
|
||||||
*/
|
*/
|
||||||
@ -352,7 +369,7 @@ export class JanAuthService {
|
|||||||
if (!this.accessToken || Date.now() > this.tokenExpiryTime) {
|
if (!this.accessToken || Date.now() > this.tokenExpiryTime) {
|
||||||
const tokens = await guestLogin()
|
const tokens = await guestLogin()
|
||||||
this.accessToken = tokens.access_token
|
this.accessToken = tokens.access_token
|
||||||
this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000
|
this.tokenExpiryTime = this.computeTokenExpiry(tokens)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to ensure guest access:', error)
|
console.error('Failed to ensure guest access:', error)
|
||||||
@ -387,7 +404,6 @@ export class JanAuthService {
|
|||||||
case AUTH_EVENTS.LOGOUT:
|
case AUTH_EVENTS.LOGOUT:
|
||||||
// Another tab logged out, clear our state
|
// Another tab logged out, clear our state
|
||||||
this.clearAuthState()
|
this.clearAuthState()
|
||||||
this.ensureGuestAccess().catch(console.error)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -413,7 +429,7 @@ export class JanAuthService {
|
|||||||
private async fetchUserProfile(): Promise<User | null> {
|
private async fetchUserProfile(): Promise<User | null> {
|
||||||
try {
|
try {
|
||||||
return await this.makeAuthenticatedRequest<User>(
|
return await this.makeAuthenticatedRequest<User>(
|
||||||
`${JAN_API_BASE}${AUTH_ENDPOINTS.ME}`
|
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.ME}`
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch user profile:', error)
|
console.error('Failed to fetch user profile:', error)
|
||||||
|
|||||||
@ -16,7 +16,8 @@ export type AuthType = ProviderType | 'guest'
|
|||||||
|
|
||||||
export interface AuthTokens {
|
export interface AuthTokens {
|
||||||
access_token: string
|
access_token: string
|
||||||
expires_in: number
|
expires_in?: number
|
||||||
|
expires_at?: string
|
||||||
object: string
|
object: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
extensions-web/src/types/global.d.ts
vendored
2
extensions-web/src/types/global.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,6 @@ export default defineConfig({
|
|||||||
emptyOutDir: false // Don't clean the output directory
|
emptyOutDir: false // Don't clean the output directory
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'),
|
MENLO_PLATFORM_BASE_URL: JSON.stringify(process.env.MENLO_PLATFORM_BASE_URL || 'https://api-dev.menlo.ai/v1'),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -156,8 +156,13 @@ export async function listSupportedBackends(): Promise<
|
|||||||
supportedBackends.push('macos-arm64')
|
supportedBackends.push('macos-arm64')
|
||||||
}
|
}
|
||||||
// get latest backends from Github
|
// get latest backends from Github
|
||||||
const remoteBackendVersions =
|
let remoteBackendVersions = []
|
||||||
|
try {
|
||||||
|
remoteBackendVersions =
|
||||||
await fetchRemoteSupportedBackends(supportedBackends)
|
await fetchRemoteSupportedBackends(supportedBackends)
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(`Not able to get remote backends, Jan might be offline or network problem: ${String(e)}`)
|
||||||
|
}
|
||||||
|
|
||||||
// Get locally installed versions
|
// Get locally installed versions
|
||||||
const localBackendVersions = await getLocalInstalledBackends()
|
const localBackendVersions = await getLocalInstalledBackends()
|
||||||
|
|||||||
@ -39,7 +39,6 @@ import { getProxyConfig } from './util'
|
|||||||
import { basename } from '@tauri-apps/api/path'
|
import { basename } from '@tauri-apps/api/path'
|
||||||
import {
|
import {
|
||||||
readGgufMetadata,
|
readGgufMetadata,
|
||||||
estimateKVCacheSize,
|
|
||||||
getModelSize,
|
getModelSize,
|
||||||
isModelSupported,
|
isModelSupported,
|
||||||
planModelLoadInternal,
|
planModelLoadInternal,
|
||||||
@ -58,6 +57,8 @@ type LlamacppConfig = {
|
|||||||
chat_template: string
|
chat_template: string
|
||||||
n_gpu_layers: number
|
n_gpu_layers: number
|
||||||
offload_mmproj: boolean
|
offload_mmproj: boolean
|
||||||
|
cpu_moe: boolean
|
||||||
|
n_cpu_moe: number
|
||||||
override_tensor_buffer_t: string
|
override_tensor_buffer_t: string
|
||||||
ctx_size: number
|
ctx_size: number
|
||||||
threads: number
|
threads: number
|
||||||
@ -1527,6 +1528,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.autoUnload &&
|
this.autoUnload &&
|
||||||
|
!isEmbedding &&
|
||||||
(loadedModels.length > 0 || otherLoadingPromises.length > 0)
|
(loadedModels.length > 0 || otherLoadingPromises.length > 0)
|
||||||
) {
|
) {
|
||||||
// Wait for OTHER loading models to finish, then unload everything
|
// Wait for OTHER loading models to finish, then unload everything
|
||||||
@ -1534,10 +1536,33 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
await Promise.all(otherLoadingPromises)
|
await Promise.all(otherLoadingPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now unload all loaded models
|
// Now unload all loaded Text models excluding embedding models
|
||||||
const allLoadedModels = await this.getLoadedModels()
|
const allLoadedModels = await this.getLoadedModels()
|
||||||
if (allLoadedModels.length > 0) {
|
if (allLoadedModels.length > 0) {
|
||||||
await Promise.all(allLoadedModels.map((model) => this.unload(model)))
|
const sessionInfos: (SessionInfo | null)[] = await Promise.all(
|
||||||
|
allLoadedModels.map(async (modelId) => {
|
||||||
|
try {
|
||||||
|
return await this.findSessionByModel(modelId)
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Unable to find session for model "${modelId}": ${e}`)
|
||||||
|
return null // treat as “not‑eligible for unload”
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(JSON.stringify(sessionInfos))
|
||||||
|
|
||||||
|
const nonEmbeddingModels: string[] = sessionInfos
|
||||||
|
.filter(
|
||||||
|
(s): s is SessionInfo => s !== null && s.is_embedding === false
|
||||||
|
)
|
||||||
|
.map((s) => s.model_id)
|
||||||
|
|
||||||
|
if (nonEmbeddingModels.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
nonEmbeddingModels.map((modelId) => this.unload(modelId))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
@ -1581,6 +1606,10 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
])
|
])
|
||||||
args.push('--jinja')
|
args.push('--jinja')
|
||||||
args.push('-m', modelPath)
|
args.push('-m', modelPath)
|
||||||
|
if (cfg.cpu_moe) args.push('--cpu-moe')
|
||||||
|
if (cfg.n_cpu_moe && cfg.n_cpu_moe > 0) {
|
||||||
|
args.push('--n-cpu-moe', String(cfg.n_cpu_moe))
|
||||||
|
}
|
||||||
// For overriding tensor buffer type, useful where
|
// For overriding tensor buffer type, useful where
|
||||||
// massive MOE models can be made faster by keeping attention on the GPU
|
// massive MOE models can be made faster by keeping attention on the GPU
|
||||||
// and offloading the expert FFNs to the CPU.
|
// and offloading the expert FFNs to the CPU.
|
||||||
@ -1631,7 +1660,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
if (cfg.no_kv_offload) args.push('--no-kv-offload')
|
if (cfg.no_kv_offload) args.push('--no-kv-offload')
|
||||||
if (isEmbedding) {
|
if (isEmbedding) {
|
||||||
args.push('--embedding')
|
args.push('--embedding')
|
||||||
args.push('--pooling mean')
|
args.push('--pooling', 'mean')
|
||||||
} else {
|
} else {
|
||||||
if (cfg.ctx_size > 0) args.push('--ctx-size', String(cfg.ctx_size))
|
if (cfg.ctx_size > 0) args.push('--ctx-size', String(cfg.ctx_size))
|
||||||
if (cfg.n_predict > 0) args.push('--n-predict', String(cfg.n_predict))
|
if (cfg.n_predict > 0) args.push('--n-predict', String(cfg.n_predict))
|
||||||
@ -1670,6 +1699,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
libraryPath,
|
libraryPath,
|
||||||
args,
|
args,
|
||||||
envs,
|
envs,
|
||||||
|
isEmbedding,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return sInfo
|
return sInfo
|
||||||
@ -2005,6 +2035,69 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
libraryPath,
|
libraryPath,
|
||||||
envs,
|
envs,
|
||||||
})
|
})
|
||||||
|
// On Linux with AMD GPUs, llama.cpp via Vulkan may report UMA (shared) memory as device-local.
|
||||||
|
// For clearer UX, override with dedicated VRAM from the hardware plugin when available.
|
||||||
|
try {
|
||||||
|
const sysInfo = await getSystemInfo()
|
||||||
|
if (sysInfo?.os_type === 'linux' && Array.isArray(sysInfo.gpus)) {
|
||||||
|
const usage = await getSystemUsage()
|
||||||
|
if (usage && Array.isArray(usage.gpus)) {
|
||||||
|
const uuidToUsage: Record<string, { total_memory: number; used_memory: number }> = {}
|
||||||
|
for (const u of usage.gpus as any[]) {
|
||||||
|
if (u && typeof u.uuid === 'string') {
|
||||||
|
uuidToUsage[u.uuid] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexToAmdUuid = new Map<number, string>()
|
||||||
|
for (const gpu of sysInfo.gpus as any[]) {
|
||||||
|
const vendorStr =
|
||||||
|
typeof gpu?.vendor === 'string'
|
||||||
|
? gpu.vendor
|
||||||
|
: typeof gpu?.vendor === 'object' && gpu.vendor !== null
|
||||||
|
? String(gpu.vendor)
|
||||||
|
: ''
|
||||||
|
if (
|
||||||
|
vendorStr.toUpperCase().includes('AMD') &&
|
||||||
|
gpu?.vulkan_info &&
|
||||||
|
typeof gpu.vulkan_info.index === 'number' &&
|
||||||
|
typeof gpu.uuid === 'string'
|
||||||
|
) {
|
||||||
|
indexToAmdUuid.set(gpu.vulkan_info.index, gpu.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexToAmdUuid.size > 0) {
|
||||||
|
const adjusted = dList.map((dev) => {
|
||||||
|
if (dev.id?.startsWith('Vulkan')) {
|
||||||
|
const match = /^Vulkan(\d+)/.exec(dev.id)
|
||||||
|
if (match) {
|
||||||
|
const vIdx = Number(match[1])
|
||||||
|
const uuid = indexToAmdUuid.get(vIdx)
|
||||||
|
if (uuid) {
|
||||||
|
const u = uuidToUsage[uuid]
|
||||||
|
if (
|
||||||
|
u &&
|
||||||
|
typeof u.total_memory === 'number' &&
|
||||||
|
typeof u.used_memory === 'number'
|
||||||
|
) {
|
||||||
|
const total = Math.max(0, Math.floor(u.total_memory))
|
||||||
|
const free = Math.max(0, Math.floor(u.total_memory - u.used_memory))
|
||||||
|
return { ...dev, mem: total, free }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dev
|
||||||
|
})
|
||||||
|
return adjusted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Device memory override (AMD/Linux) failed:', e)
|
||||||
|
}
|
||||||
|
|
||||||
return dList
|
return dList
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to query devices:\n', error)
|
logger.error('Failed to query devices:\n', error)
|
||||||
@ -2013,6 +2106,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async embed(text: string[]): Promise<EmbeddingResponse> {
|
async embed(text: string[]): Promise<EmbeddingResponse> {
|
||||||
|
// Ensure the sentence-transformer model is present
|
||||||
let sInfo = await this.findSessionByModel('sentence-transformer-mini')
|
let sInfo = await this.findSessionByModel('sentence-transformer-mini')
|
||||||
if (!sInfo) {
|
if (!sInfo) {
|
||||||
const downloadedModelList = await this.list()
|
const downloadedModelList = await this.list()
|
||||||
@ -2026,30 +2120,45 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
'https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-ggml-model-f16.gguf?download=true',
|
'https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-ggml-model-f16.gguf?download=true',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sInfo = await this.load('sentence-transformer-mini')
|
// Load specifically in embedding mode
|
||||||
|
sInfo = await this.load('sentence-transformer-mini', undefined, true)
|
||||||
}
|
}
|
||||||
const baseUrl = `http://localhost:${sInfo.port}/v1/embeddings`
|
|
||||||
const headers = {
|
const attemptRequest = async (session: SessionInfo) => {
|
||||||
'Content-Type': 'application/json',
|
const baseUrl = `http://localhost:${session.port}/v1/embeddings`
|
||||||
'Authorization': `Bearer ${sInfo.api_key}`,
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${session.api_key}`,
|
||||||
|
}
|
||||||
|
const body = JSON.stringify({
|
||||||
|
input: text,
|
||||||
|
model: session.model_id,
|
||||||
|
encoding_format: 'float',
|
||||||
|
})
|
||||||
|
const response = await fetch(baseUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try with the existing session (may have been started without --embedding previously)
|
||||||
|
let response = await attemptRequest(sInfo)
|
||||||
|
|
||||||
|
// If embeddings endpoint is not available (501), reload with embedding mode and retry once
|
||||||
|
if (response.status === 501) {
|
||||||
|
try {
|
||||||
|
await this.unload('sentence-transformer-mini')
|
||||||
|
} catch {}
|
||||||
|
sInfo = await this.load('sentence-transformer-mini', undefined, true)
|
||||||
|
response = await attemptRequest(sInfo)
|
||||||
}
|
}
|
||||||
const body = JSON.stringify({
|
|
||||||
input: text,
|
|
||||||
model: sInfo.model_id,
|
|
||||||
encoding_format: 'float',
|
|
||||||
})
|
|
||||||
const response = await fetch(baseUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => null)
|
const errorData = await response.json().catch(() => null)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`API request failed with status ${response.status}: ${JSON.stringify(
|
`API request failed with status ${response.status}: ${JSON.stringify(errorData)}`
|
||||||
errorData
|
|
||||||
)}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const responseData = await response.json()
|
const responseData = await response.json()
|
||||||
@ -2151,7 +2260,12 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
if (mmprojPath && !this.isAbsolutePath(mmprojPath))
|
if (mmprojPath && !this.isAbsolutePath(mmprojPath))
|
||||||
mmprojPath = await joinPath([await getJanDataFolderPath(), path])
|
mmprojPath = await joinPath([await getJanDataFolderPath(), path])
|
||||||
try {
|
try {
|
||||||
const result = await planModelLoadInternal(path, this.memoryMode, mmprojPath, requestedCtx)
|
const result = await planModelLoadInternal(
|
||||||
|
path,
|
||||||
|
this.memoryMode,
|
||||||
|
mmprojPath,
|
||||||
|
requestedCtx
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(String(e))
|
throw new Error(String(e))
|
||||||
@ -2279,12 +2393,18 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate text tokens
|
// Calculate text tokens
|
||||||
const messages = JSON.stringify({ messages: opts.messages })
|
// Use chat_template_kwargs from opts if provided, otherwise default to disable enable_thinking
|
||||||
|
const tokenizeRequest = {
|
||||||
|
messages: opts.messages,
|
||||||
|
chat_template_kwargs: opts.chat_template_kwargs || {
|
||||||
|
enable_thinking: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
let parseResponse = await fetch(`${baseUrl}/apply-template`, {
|
let parseResponse = await fetch(`${baseUrl}/apply-template`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: messages,
|
body: JSON.stringify(tokenizeRequest),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!parseResponse.ok) {
|
if (!parseResponse.ok) {
|
||||||
|
|||||||
33
extensions/rag-extension/package.json
Normal file
33
extensions/rag-extension/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@janhq/rag-extension",
|
||||||
|
"productName": "RAG Tools",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Registers RAG tools and orchestrates retrieval across parser, embeddings, and vector DB",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/module.js",
|
||||||
|
"author": "Jan <service@jan.ai>",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rolldown -c rolldown.config.mjs",
|
||||||
|
"build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cpx": "1.5.0",
|
||||||
|
"rimraf": "6.0.1",
|
||||||
|
"rolldown": "1.0.0-beta.1",
|
||||||
|
"typescript": "5.9.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@janhq/core": "../../core/package.tgz",
|
||||||
|
"@janhq/tauri-plugin-rag-api": "link:../../src-tauri/plugins/tauri-plugin-rag",
|
||||||
|
"@janhq/tauri-plugin-vector-db-api": "link:../../src-tauri/plugins/tauri-plugin-vector-db"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"installConfig": {
|
||||||
|
"hoistingLimits": "workspaces"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.5.3"
|
||||||
|
}
|
||||||
14
extensions/rag-extension/rolldown.config.mjs
Normal file
14
extensions/rag-extension/rolldown.config.mjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'rolldown'
|
||||||
|
import settingJson from './settings.json' with { type: 'json' }
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: {
|
||||||
|
format: 'esm',
|
||||||
|
file: 'dist/index.js',
|
||||||
|
},
|
||||||
|
platform: 'browser',
|
||||||
|
define: {
|
||||||
|
SETTINGS: JSON.stringify(settingJson),
|
||||||
|
},
|
||||||
|
})
|
||||||
58
extensions/rag-extension/settings.json
Normal file
58
extensions/rag-extension/settings.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "enabled",
|
||||||
|
"titleKey": "settings:attachments.enable",
|
||||||
|
"descriptionKey": "settings:attachments.enableDesc",
|
||||||
|
"controllerType": "checkbox",
|
||||||
|
"controllerProps": { "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "max_file_size_mb",
|
||||||
|
"titleKey": "settings:attachments.maxFile",
|
||||||
|
"descriptionKey": "settings:attachments.maxFileDesc",
|
||||||
|
"controllerType": "input",
|
||||||
|
"controllerProps": { "value": 20, "type": "number", "min": 1, "max": 200, "step": 1, "textAlign": "right" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "retrieval_limit",
|
||||||
|
"titleKey": "settings:attachments.topK",
|
||||||
|
"descriptionKey": "settings:attachments.topKDesc",
|
||||||
|
"controllerType": "input",
|
||||||
|
"controllerProps": { "value": 3, "type": "number", "min": 1, "max": 20, "step": 1, "textAlign": "right" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "retrieval_threshold",
|
||||||
|
"titleKey": "settings:attachments.threshold",
|
||||||
|
"descriptionKey": "settings:attachments.thresholdDesc",
|
||||||
|
"controllerType": "input",
|
||||||
|
"controllerProps": { "value": 0.3, "type": "number", "min": 0, "max": 1, "step": 0.01, "textAlign": "right" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "chunk_size_tokens",
|
||||||
|
"titleKey": "settings:attachments.chunkSize",
|
||||||
|
"descriptionKey": "settings:attachments.chunkSizeDesc",
|
||||||
|
"controllerType": "input",
|
||||||
|
"controllerProps": { "value": 512, "type": "number", "min": 64, "max": 8192, "step": 64, "textAlign": "right" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "overlap_tokens",
|
||||||
|
"titleKey": "settings:attachments.chunkOverlap",
|
||||||
|
"descriptionKey": "settings:attachments.chunkOverlapDesc",
|
||||||
|
"controllerType": "input",
|
||||||
|
"controllerProps": { "value": 64, "type": "number", "min": 0, "max": 1024, "step": 16, "textAlign": "right" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "search_mode",
|
||||||
|
"titleKey": "settings:attachments.searchMode",
|
||||||
|
"descriptionKey": "settings:attachments.searchModeDesc",
|
||||||
|
"controllerType": "dropdown",
|
||||||
|
"controllerProps": {
|
||||||
|
"value": "auto",
|
||||||
|
"options": [
|
||||||
|
{ "name": "Auto (recommended)", "value": "auto" },
|
||||||
|
{ "name": "ANN (sqlite-vec)", "value": "ann" },
|
||||||
|
{ "name": "Linear", "value": "linear" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
5
extensions/rag-extension/src/env.d.ts
vendored
Normal file
5
extensions/rag-extension/src/env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { SettingComponentProps } from '@janhq/core'
|
||||||
|
declare global {
|
||||||
|
const SETTINGS: SettingComponentProps[]
|
||||||
|
}
|
||||||
|
export {}
|
||||||
14
extensions/rag-extension/src/global.d.ts
vendored
Normal file
14
extensions/rag-extension/src/global.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { BaseExtension, ExtensionTypeEnum } from '@janhq/core'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
core?: {
|
||||||
|
extensionManager: {
|
||||||
|
get<T = BaseExtension>(type: ExtensionTypeEnum): T | undefined
|
||||||
|
getByName(name: string): BaseExtension | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
305
extensions/rag-extension/src/index.ts
Normal file
305
extensions/rag-extension/src/index.ts
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import { RAGExtension, MCPTool, MCPToolCallResult, ExtensionTypeEnum, VectorDBExtension, type AttachmentInput, type SettingComponentProps, AIEngine, type AttachmentFileInfo } from '@janhq/core'
|
||||||
|
import './env.d'
|
||||||
|
import { getRAGTools, RETRIEVE, LIST_ATTACHMENTS, GET_CHUNKS } from './tools'
|
||||||
|
|
||||||
|
export default class RagExtension extends RAGExtension {
|
||||||
|
private config = {
|
||||||
|
enabled: true,
|
||||||
|
retrievalLimit: 3,
|
||||||
|
retrievalThreshold: 0.3,
|
||||||
|
chunkSizeTokens: 512,
|
||||||
|
overlapTokens: 64,
|
||||||
|
searchMode: 'auto' as 'auto' | 'ann' | 'linear',
|
||||||
|
maxFileSizeMB: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLoad(): Promise<void> {
|
||||||
|
const settings = structuredClone(SETTINGS) as SettingComponentProps[]
|
||||||
|
await this.registerSettings(settings)
|
||||||
|
this.config.enabled = await this.getSetting('enabled', this.config.enabled)
|
||||||
|
this.config.maxFileSizeMB = await this.getSetting('max_file_size_mb', this.config.maxFileSizeMB)
|
||||||
|
this.config.retrievalLimit = await this.getSetting('retrieval_limit', this.config.retrievalLimit)
|
||||||
|
this.config.retrievalThreshold = await this.getSetting('retrieval_threshold', this.config.retrievalThreshold)
|
||||||
|
this.config.chunkSizeTokens = await this.getSetting('chunk_size_tokens', this.config.chunkSizeTokens)
|
||||||
|
this.config.overlapTokens = await this.getSetting('overlap_tokens', this.config.overlapTokens)
|
||||||
|
this.config.searchMode = await this.getSetting('search_mode', this.config.searchMode)
|
||||||
|
|
||||||
|
// Check ANN availability on load
|
||||||
|
try {
|
||||||
|
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
|
||||||
|
if (vec?.getStatus) {
|
||||||
|
const status = await vec.getStatus()
|
||||||
|
console.log('[RAG] Vector DB ANN support:', status.ann_available ? '✓ AVAILABLE' : '✗ NOT AVAILABLE')
|
||||||
|
if (!status.ann_available) {
|
||||||
|
console.warn('[RAG] Warning: sqlite-vec not loaded. Collections will use slower linear search.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[RAG] Failed to check ANN status:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(): void {}
|
||||||
|
|
||||||
|
async getTools(): Promise<MCPTool[]> {
|
||||||
|
return getRAGTools(this.config.retrievalLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getToolNames(): Promise<string[]> {
|
||||||
|
// Keep this in sync with getTools() but without building full schemas
|
||||||
|
return [LIST_ATTACHMENTS, RETRIEVE, GET_CHUNKS]
|
||||||
|
}
|
||||||
|
|
||||||
|
async callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult> {
|
||||||
|
switch (toolName) {
|
||||||
|
case LIST_ATTACHMENTS:
|
||||||
|
return this.listAttachments(args)
|
||||||
|
case RETRIEVE:
|
||||||
|
return this.retrieve(args)
|
||||||
|
case GET_CHUNKS:
|
||||||
|
return this.getChunks(args)
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
error: `Unknown tool: ${toolName}`,
|
||||||
|
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listAttachments(args: Record<string, unknown>): Promise<MCPToolCallResult> {
|
||||||
|
const threadId = String(args['thread_id'] || '')
|
||||||
|
if (!threadId) {
|
||||||
|
return { error: 'Missing thread_id', content: [{ type: 'text', text: 'Missing thread_id' }] }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
|
||||||
|
if (!vec?.listAttachments) {
|
||||||
|
return { error: 'Vector DB extension missing listAttachments', content: [{ type: 'text', text: 'Vector DB extension missing listAttachments' }] }
|
||||||
|
}
|
||||||
|
const files = await vec.listAttachments(threadId)
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({ thread_id: threadId, attachments: files || [] }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
return { error: msg, content: [{ type: 'text', text: `List attachments failed: ${msg}` }] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async retrieve(args: Record<string, unknown>): Promise<MCPToolCallResult> {
|
||||||
|
const threadId = String(args['thread_id'] || '')
|
||||||
|
const query = String(args['query'] || '')
|
||||||
|
const fileIds = args['file_ids'] as string[] | undefined
|
||||||
|
|
||||||
|
const s = this.config
|
||||||
|
const topK = (args['top_k'] as number) || s.retrievalLimit || 3
|
||||||
|
const threshold = s.retrievalThreshold ?? 0.3
|
||||||
|
const mode: 'auto' | 'ann' | 'linear' = s.searchMode || 'auto'
|
||||||
|
|
||||||
|
if (s.enabled === false) {
|
||||||
|
return {
|
||||||
|
error: 'Attachments feature disabled',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Attachments are disabled in Settings. Enable them to use retrieval.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!threadId || !query) {
|
||||||
|
return {
|
||||||
|
error: 'Missing thread_id or query',
|
||||||
|
content: [{ type: 'text', text: 'Missing required parameters' }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve extensions
|
||||||
|
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
|
||||||
|
if (!vec?.searchCollection) {
|
||||||
|
return {
|
||||||
|
error: 'RAG dependencies not available',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: 'Vector DB extension not available' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryEmb = (await this.embedTexts([query]))?.[0]
|
||||||
|
if (!queryEmb) {
|
||||||
|
return {
|
||||||
|
error: 'Failed to compute embeddings',
|
||||||
|
content: [{ type: 'text', text: 'Failed to compute embeddings' }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await vec.searchCollection(
|
||||||
|
threadId,
|
||||||
|
queryEmb,
|
||||||
|
topK,
|
||||||
|
threshold,
|
||||||
|
mode,
|
||||||
|
fileIds
|
||||||
|
)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
thread_id: threadId,
|
||||||
|
query,
|
||||||
|
citations: results?.map((r: any) => ({
|
||||||
|
id: r.id,
|
||||||
|
text: r.text,
|
||||||
|
score: r.score,
|
||||||
|
file_id: r.file_id,
|
||||||
|
chunk_file_order: r.chunk_file_order
|
||||||
|
})) ?? [],
|
||||||
|
mode,
|
||||||
|
}
|
||||||
|
return { error: '', content: [{ type: 'text', text: JSON.stringify(payload) }] }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[RAG] Retrieve error:', e)
|
||||||
|
let msg = 'Unknown error'
|
||||||
|
if (e instanceof Error) {
|
||||||
|
msg = e.message
|
||||||
|
} else if (typeof e === 'string') {
|
||||||
|
msg = e
|
||||||
|
} else if (e && typeof e === 'object') {
|
||||||
|
msg = JSON.stringify(e)
|
||||||
|
}
|
||||||
|
return { error: msg, content: [{ type: 'text', text: `Retrieve failed: ${msg}` }] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getChunks(args: Record<string, unknown>): Promise<MCPToolCallResult> {
|
||||||
|
const threadId = String(args['thread_id'] || '')
|
||||||
|
const fileId = String(args['file_id'] || '')
|
||||||
|
const startOrder = args['start_order'] as number | undefined
|
||||||
|
const endOrder = args['end_order'] as number | undefined
|
||||||
|
|
||||||
|
if (!threadId || !fileId || startOrder === undefined || endOrder === undefined) {
|
||||||
|
return {
|
||||||
|
error: 'Missing thread_id, file_id, start_order, or end_order',
|
||||||
|
content: [{ type: 'text', text: 'Missing required parameters' }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
|
||||||
|
if (!vec?.getChunks) {
|
||||||
|
return {
|
||||||
|
error: 'Vector DB extension not available',
|
||||||
|
content: [{ type: 'text', text: 'Vector DB extension not available' }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = await vec.getChunks(threadId, fileId, startOrder, endOrder)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
thread_id: threadId,
|
||||||
|
file_id: fileId,
|
||||||
|
chunks: chunks || [],
|
||||||
|
}
|
||||||
|
return { error: '', content: [{ type: 'text', text: JSON.stringify(payload) }] }
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
return { error: msg, content: [{ type: 'text', text: `Get chunks failed: ${msg}` }] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop-only ingestion by file paths
|
||||||
|
async ingestAttachments(
|
||||||
|
threadId: string,
|
||||||
|
files: AttachmentInput[]
|
||||||
|
): Promise<{ filesProcessed: number; chunksInserted: number; files: AttachmentFileInfo[] }> {
|
||||||
|
if (!threadId || !Array.isArray(files) || files.length === 0) {
|
||||||
|
return { filesProcessed: 0, chunksInserted: 0, files: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respect feature flag: do nothing when disabled
|
||||||
|
if (this.config.enabled === false) {
|
||||||
|
return { filesProcessed: 0, chunksInserted: 0, files: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const vec = window.core?.extensionManager.get(ExtensionTypeEnum.VectorDB) as unknown as VectorDBExtension
|
||||||
|
if (!vec?.createCollection || !vec?.insertChunks) {
|
||||||
|
throw new Error('Vector DB extension not available')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
const s = this.config
|
||||||
|
const maxSize = (s?.enabled === false ? 0 : s?.maxFileSizeMB) || undefined
|
||||||
|
const chunkSize = s?.chunkSizeTokens as number | undefined
|
||||||
|
const chunkOverlap = s?.overlapTokens as number | undefined
|
||||||
|
|
||||||
|
let totalChunks = 0
|
||||||
|
const processedFiles: AttachmentFileInfo[] = []
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
if (!f?.path) continue
|
||||||
|
if (maxSize && f.size && f.size > maxSize * 1024 * 1024) {
|
||||||
|
throw new Error(`File '${f.name}' exceeds size limit (${f.size} bytes > ${maxSize} MB).`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = f.name || f.path.split(/[\\/]/).pop()
|
||||||
|
// Preferred/required path: let Vector DB extension handle full file ingestion
|
||||||
|
const canIngestFile = typeof (vec as any)?.ingestFile === 'function'
|
||||||
|
if (!canIngestFile) {
|
||||||
|
console.error('[RAG] Vector DB extension missing ingestFile; cannot ingest document')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const info = await (vec as VectorDBExtension).ingestFile(
|
||||||
|
threadId,
|
||||||
|
{ path: f.path, name: fileName, type: f.type, size: f.size },
|
||||||
|
{ chunkSize: chunkSize ?? 512, chunkOverlap: chunkOverlap ?? 64 }
|
||||||
|
)
|
||||||
|
totalChunks += Number(info?.chunk_count || 0)
|
||||||
|
processedFiles.push(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return files we ingested with real IDs directly from ingestFile
|
||||||
|
return { filesProcessed: processedFiles.length, chunksInserted: totalChunks, files: processedFiles }
|
||||||
|
}
|
||||||
|
|
||||||
|
onSettingUpdate<T>(key: string, value: T): void {
|
||||||
|
switch (key) {
|
||||||
|
case 'enabled':
|
||||||
|
this.config.enabled = Boolean(value)
|
||||||
|
break
|
||||||
|
case 'max_file_size_mb':
|
||||||
|
this.config.maxFileSizeMB = Number(value)
|
||||||
|
break
|
||||||
|
case 'retrieval_limit':
|
||||||
|
this.config.retrievalLimit = Number(value)
|
||||||
|
break
|
||||||
|
case 'retrieval_threshold':
|
||||||
|
this.config.retrievalThreshold = Number(value)
|
||||||
|
break
|
||||||
|
case 'chunk_size_tokens':
|
||||||
|
this.config.chunkSizeTokens = Number(value)
|
||||||
|
break
|
||||||
|
case 'overlap_tokens':
|
||||||
|
this.config.overlapTokens = Number(value)
|
||||||
|
break
|
||||||
|
case 'search_mode':
|
||||||
|
this.config.searchMode = String(value) as 'auto' | 'ann' | 'linear'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locally implement embedding logic (previously in embeddings-extension)
|
||||||
|
private async embedTexts(texts: string[]): Promise<number[][]> {
|
||||||
|
const llm = window.core?.extensionManager.getByName('@janhq/llamacpp-extension') as AIEngine & { embed?: (texts: string[]) => Promise<{ data: Array<{ embedding: number[]; index: number }> }> }
|
||||||
|
if (!llm?.embed) throw new Error('llamacpp extension not available')
|
||||||
|
const res = await llm.embed(texts)
|
||||||
|
const data: Array<{ embedding: number[]; index: number }> = res?.data || []
|
||||||
|
const out: number[][] = new Array(texts.length)
|
||||||
|
for (const item of data) out[item.index] = item.embedding
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
58
extensions/rag-extension/src/tools.ts
Normal file
58
extensions/rag-extension/src/tools.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { MCPTool, RAG_INTERNAL_SERVER } from '@janhq/core'
|
||||||
|
|
||||||
|
// Tool names
|
||||||
|
export const RETRIEVE = 'retrieve'
|
||||||
|
export const LIST_ATTACHMENTS = 'list_attachments'
|
||||||
|
export const GET_CHUNKS = 'get_chunks'
|
||||||
|
|
||||||
|
export function getRAGTools(retrievalLimit: number): MCPTool[] {
|
||||||
|
const maxTopK = Math.max(1, Number(retrievalLimit ?? 3))
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: LIST_ATTACHMENTS,
|
||||||
|
description:
|
||||||
|
'List files attached to the current thread. Thread is inferred automatically; you may optionally provide {"scope":"thread"}. Returns basic file info (name/path).',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
scope: { type: 'string', enum: ['thread'], description: 'Retrieval scope; currently only thread is supported' },
|
||||||
|
},
|
||||||
|
required: ['scope'],
|
||||||
|
},
|
||||||
|
server: RAG_INTERNAL_SERVER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: RETRIEVE,
|
||||||
|
description:
|
||||||
|
'Retrieve relevant snippets from locally attached, indexed documents. Use query only; do not pass raw document content. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}. Use file_ids to search within specific files only.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'User query to search for' },
|
||||||
|
top_k: { type: 'number', description: 'Optional: Max citations to return. Adjust as needed.', minimum: 1, maximum: maxTopK, default: retrievalLimit ?? 3 },
|
||||||
|
scope: { type: 'string', enum: ['thread'], description: 'Retrieval scope; currently only thread is supported' },
|
||||||
|
file_ids: { type: 'array', items: { type: 'string' }, description: 'Optional: Filter search to specific file IDs from list_attachments' },
|
||||||
|
},
|
||||||
|
required: ['query', 'scope'],
|
||||||
|
},
|
||||||
|
server: RAG_INTERNAL_SERVER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: GET_CHUNKS,
|
||||||
|
description:
|
||||||
|
'Retrieve chunks from a file by their order range. For a single chunk, use start_order = end_order. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}. Use sparingly; intended for advanced usage. Prefer using retrieve instead for relevance-based fetching.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
file_id: { type: 'string', description: 'File ID from list_attachments' },
|
||||||
|
start_order: { type: 'number', description: 'Start of chunk range (inclusive, 0-indexed)' },
|
||||||
|
end_order: { type: 'number', description: 'End of chunk range (inclusive, 0-indexed). For single chunk, use start_order = end_order.' },
|
||||||
|
scope: { type: 'string', enum: ['thread'], description: 'Retrieval scope; currently only thread is supported' },
|
||||||
|
},
|
||||||
|
required: ['file_id', 'start_order', 'end_order', 'scope'],
|
||||||
|
},
|
||||||
|
server: RAG_INTERNAL_SERVER,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
33
extensions/vector-db-extension/package.json
Normal file
33
extensions/vector-db-extension/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@janhq/vector-db-extension",
|
||||||
|
"productName": "Vector DB",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Vector DB integration using sqlite-vec if available with linear fallback",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/module.js",
|
||||||
|
"author": "Jan <service@jan.ai>",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rolldown -c rolldown.config.mjs",
|
||||||
|
"build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cpx": "1.5.0",
|
||||||
|
"rimraf": "6.0.1",
|
||||||
|
"rolldown": "1.0.0-beta.1",
|
||||||
|
"typescript": "5.9.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@janhq/core": "../../core/package.tgz",
|
||||||
|
"@janhq/tauri-plugin-rag-api": "link:../../src-tauri/plugins/tauri-plugin-rag",
|
||||||
|
"@janhq/tauri-plugin-vector-db-api": "link:../../src-tauri/plugins/tauri-plugin-vector-db"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"installConfig": {
|
||||||
|
"hoistingLimits": "workspaces"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.5.3"
|
||||||
|
}
|
||||||
11
extensions/vector-db-extension/rolldown.config.mjs
Normal file
11
extensions/vector-db-extension/rolldown.config.mjs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'rolldown'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: {
|
||||||
|
format: 'esm',
|
||||||
|
file: 'dist/index.js',
|
||||||
|
},
|
||||||
|
platform: 'browser',
|
||||||
|
define: {},
|
||||||
|
})
|
||||||
107
extensions/vector-db-extension/src/index.ts
Normal file
107
extensions/vector-db-extension/src/index.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { VectorDBExtension, type SearchMode, type VectorDBStatus, type VectorChunkInput, type VectorSearchResult, type AttachmentFileInfo, type VectorDBFileInput, type VectorDBIngestOptions, AIEngine } from '@janhq/core'
|
||||||
|
import * as vecdb from '@janhq/tauri-plugin-vector-db-api'
|
||||||
|
import * as ragApi from '@janhq/tauri-plugin-rag-api'
|
||||||
|
|
||||||
|
export default class VectorDBExt extends VectorDBExtension {
|
||||||
|
async onLoad(): Promise<void> {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(): void {}
|
||||||
|
|
||||||
|
async getStatus(): Promise<VectorDBStatus> {
|
||||||
|
return await vecdb.getStatus() as VectorDBStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectionForThread(threadId: string): string {
|
||||||
|
return `attachments_${threadId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCollection(threadId: string, dimension: number): Promise<void> {
|
||||||
|
return await vecdb.createCollection(this.collectionForThread(threadId), dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertChunks(threadId: string, fileId: string, chunks: VectorChunkInput[]): Promise<void> {
|
||||||
|
return await vecdb.insertChunks(this.collectionForThread(threadId), fileId, chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchCollection(
|
||||||
|
threadId: string,
|
||||||
|
query_embedding: number[],
|
||||||
|
limit: number,
|
||||||
|
threshold: number,
|
||||||
|
mode?: SearchMode,
|
||||||
|
fileIds?: string[]
|
||||||
|
): Promise<VectorSearchResult[]> {
|
||||||
|
return await vecdb.searchCollection(this.collectionForThread(threadId), query_embedding, limit, threshold, mode, fileIds) as VectorSearchResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteChunks(threadId: string, ids: string[]): Promise<void> {
|
||||||
|
return await vecdb.deleteChunks(this.collectionForThread(threadId), ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCollection(threadId: string): Promise<void> {
|
||||||
|
return await vecdb.deleteCollection(this.collectionForThread(threadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional helper for chunking
|
||||||
|
private async chunkText(text: string, chunkSize: number, chunkOverlap: number): Promise<string[]> {
|
||||||
|
return await vecdb.chunkText(text, chunkSize, chunkOverlap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async embedTexts(texts: string[]): Promise<number[][]> {
|
||||||
|
const llm = window.core?.extensionManager.getByName('@janhq/llamacpp-extension') as AIEngine & { embed?: (texts: string[]) => Promise<{ data: Array<{ embedding: number[]; index: number }> }> }
|
||||||
|
if (!llm?.embed) throw new Error('llamacpp extension not available')
|
||||||
|
const res = await llm.embed(texts)
|
||||||
|
const data: Array<{ embedding: number[]; index: number }> = res?.data || []
|
||||||
|
const out: number[][] = new Array(texts.length)
|
||||||
|
for (const item of data) out[item.index] = item.embedding
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async ingestFile(threadId: string, file: VectorDBFileInput, opts: VectorDBIngestOptions): Promise<AttachmentFileInfo> {
|
||||||
|
// Check for duplicate file (same name + path)
|
||||||
|
const existingFiles = await vecdb.listAttachments(this.collectionForThread(threadId)).catch(() => [])
|
||||||
|
const duplicate = existingFiles.find((f: any) => f.name === file.name && f.path === file.path)
|
||||||
|
if (duplicate) {
|
||||||
|
throw new Error(`File '${file.name}' has already been attached to this thread`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await ragApi.parseDocument(file.path, file.type || 'application/octet-stream')
|
||||||
|
const chunks = await this.chunkText(text, opts.chunkSize, opts.chunkOverlap)
|
||||||
|
if (!chunks.length) {
|
||||||
|
const fi = await vecdb.createFile(this.collectionForThread(threadId), file)
|
||||||
|
return fi
|
||||||
|
}
|
||||||
|
const embeddings = await this.embedTexts(chunks)
|
||||||
|
const dimension = embeddings[0]?.length || 0
|
||||||
|
if (dimension <= 0) throw new Error('Embedding dimension not available')
|
||||||
|
await this.createCollection(threadId, dimension)
|
||||||
|
const fi = await vecdb.createFile(this.collectionForThread(threadId), file)
|
||||||
|
await vecdb.insertChunks(
|
||||||
|
this.collectionForThread(threadId),
|
||||||
|
fi.id,
|
||||||
|
chunks.map((t, i) => ({ text: t, embedding: embeddings[i] }))
|
||||||
|
)
|
||||||
|
const infos = await vecdb.listAttachments(this.collectionForThread(threadId))
|
||||||
|
const updated = infos.find((e) => e.id === fi.id)
|
||||||
|
return updated || { ...fi, chunk_count: chunks.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAttachments(threadId: string, limit?: number): Promise<AttachmentFileInfo[]> {
|
||||||
|
return await vecdb.listAttachments(this.collectionForThread(threadId), limit) as AttachmentFileInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChunks(
|
||||||
|
threadId: string,
|
||||||
|
fileId: string,
|
||||||
|
startOrder: number,
|
||||||
|
endOrder: number
|
||||||
|
): Promise<VectorSearchResult[]> {
|
||||||
|
return await vecdb.getChunks(this.collectionForThread(threadId), fileId, startOrder, endOrder) as VectorSearchResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(threadId: string, fileId: string): Promise<void> {
|
||||||
|
return await vecdb.deleteFile(this.collectionForThread(threadId), fileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
package.json
13
package.json
@ -26,17 +26,16 @@
|
|||||||
"serve:web-app": "yarn workspace @janhq/web-app serve:web",
|
"serve:web-app": "yarn workspace @janhq/web-app serve:web",
|
||||||
"build:serve:web-app": "yarn build:web-app && yarn serve:web-app",
|
"build:serve:web-app": "yarn build:web-app && yarn serve:web-app",
|
||||||
"dev:tauri": "yarn build:icon && yarn copy:assets:tauri && cross-env IS_CLEAN=true tauri dev",
|
"dev:tauri": "yarn build:icon && yarn copy:assets:tauri && cross-env IS_CLEAN=true tauri dev",
|
||||||
"dev:ios": "yarn build:extensions-web && yarn copy:assets:mobile && RUSTC_WRAPPER= yarn tauri ios dev --features mobile",
|
"dev:ios": "yarn copy:assets:mobile && RUSTC_WRAPPER= cross-env IS_IOS=true yarn tauri ios dev --features mobile",
|
||||||
"dev:android": "yarn build:extensions-web && yarn copy:assets:mobile && cross-env IS_CLEAN=true TAURI_ANDROID_BUILD=true yarn tauri android dev --features mobile",
|
"dev:android": "yarn copy:assets:mobile && cross-env IS_ANDROID=true yarn tauri android dev --features mobile",
|
||||||
"build:android": "yarn build:icon && yarn copy:assets:mobile && cross-env IS_CLEAN=true TAURI_ANDROID_BUILD=true yarn tauri android build -- --no-default-features --features mobile",
|
"build:android": "yarn build:icon && yarn copy:assets:mobile && cross-env IS_CLEAN=true yarn tauri android build -- --no-default-features --features mobile",
|
||||||
"build:ios": "yarn copy:assets:mobile && yarn tauri ios build -- --no-default-features --features mobile",
|
"build:ios": "yarn build:icon && yarn copy:assets:mobile && cross-env IS_IOS=true yarn tauri ios build -- --no-default-features --features mobile",
|
||||||
"build:ios:device": "yarn build:icon && yarn copy:assets:mobile && yarn tauri ios build -- --no-default-features --features mobile --export-method debugging",
|
"build:ios:device": "yarn build:icon && yarn copy:assets:mobile && cross-env IS_IOS=true yarn tauri ios build -- --no-default-features --features mobile --export-method debugging",
|
||||||
"copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"",
|
"copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"",
|
||||||
"copy:assets:mobile": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"",
|
"copy:assets:mobile": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"",
|
||||||
"download:lib": "node ./scripts/download-lib.mjs",
|
"download:lib": "node ./scripts/download-lib.mjs",
|
||||||
"download:bin": "node ./scripts/download-bin.mjs",
|
"download:bin": "node ./scripts/download-bin.mjs",
|
||||||
"download:windows-installer": "node ./scripts/download-win-installer-deps.mjs",
|
"build:tauri:win32": "yarn download:bin && yarn tauri build",
|
||||||
"build:tauri:win32": "yarn download:bin && yarn download:windows-installer && yarn tauri build",
|
|
||||||
"build:tauri:linux": "yarn download:bin && NO_STRIP=1 ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh",
|
"build:tauri:linux": "yarn download:bin && NO_STRIP=1 ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh",
|
||||||
"build:tauri:darwin": "yarn download:bin && yarn tauri build --target universal-apple-darwin",
|
"build:tauri:darwin": "yarn download:bin && yarn tauri build --target universal-apple-darwin",
|
||||||
"build:tauri": "yarn build:icon && yarn copy:assets:tauri && run-script-os",
|
"build:tauri": "yarn build:icon && yarn copy:assets:tauri && run-script-os",
|
||||||
|
|||||||
@ -56,6 +56,75 @@ async function decompress(filePath, targetDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getJson(url, headers = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opts = new URL(url)
|
||||||
|
opts.headers = {
|
||||||
|
'User-Agent': 'jan-app',
|
||||||
|
'Accept': 'application/vnd.github+json',
|
||||||
|
...headers,
|
||||||
|
}
|
||||||
|
https
|
||||||
|
.get(opts, (res) => {
|
||||||
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
return getJson(res.headers.location, headers).then(resolve, reject)
|
||||||
|
}
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`GET ${url} failed with status ${res.statusCode}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let data = ''
|
||||||
|
res.on('data', (chunk) => (data += chunk))
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data))
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchSqliteVecAsset(assets, platform, arch) {
|
||||||
|
const osHints =
|
||||||
|
platform === 'darwin'
|
||||||
|
? ['darwin', 'macos', 'apple-darwin']
|
||||||
|
: platform === 'win32'
|
||||||
|
? ['windows', 'win', 'msvc']
|
||||||
|
: ['linux']
|
||||||
|
|
||||||
|
const archHints = arch === 'arm64' ? ['arm64', 'aarch64'] : ['x86_64', 'x64', 'amd64']
|
||||||
|
const extHints = ['zip', 'tar.gz']
|
||||||
|
|
||||||
|
const lc = (s) => s.toLowerCase()
|
||||||
|
const candidates = assets
|
||||||
|
.filter((a) => a && a.browser_download_url && a.name)
|
||||||
|
.map((a) => ({ name: lc(a.name), url: a.browser_download_url }))
|
||||||
|
|
||||||
|
// Prefer exact OS + arch matches
|
||||||
|
let matches = candidates.filter((c) => osHints.some((o) => c.name.includes(o)) && archHints.some((h) => c.name.includes(h)) && extHints.some((e) => c.name.endsWith(e)))
|
||||||
|
if (matches.length) return matches[0].url
|
||||||
|
// Fallback: OS only
|
||||||
|
matches = candidates.filter((c) => osHints.some((o) => c.name.includes(o)) && extHints.some((e) => c.name.endsWith(e)))
|
||||||
|
if (matches.length) return matches[0].url
|
||||||
|
// Last resort: any asset with shared library extension inside is unknown here, so pick any zip/tar.gz
|
||||||
|
matches = candidates.filter((c) => extHints.some((e) => c.name.endsWith(e)))
|
||||||
|
return matches.length ? matches[0].url : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLatestSqliteVecUrl(platform, arch) {
|
||||||
|
try {
|
||||||
|
const rel = await getJson('https://api.github.com/repos/asg017/sqlite-vec/releases/latest')
|
||||||
|
const url = matchSqliteVecAsset(rel.assets || [], platform, arch)
|
||||||
|
return url
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to query sqlite-vec latest release:', e.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getPlatformArch() {
|
function getPlatformArch() {
|
||||||
const platform = os.platform() // 'darwin', 'linux', 'win32'
|
const platform = os.platform() // 'darwin', 'linux', 'win32'
|
||||||
const arch = os.arch() // 'x64', 'arm64', etc.
|
const arch = os.arch() // 'x64', 'arm64', etc.
|
||||||
@ -266,6 +335,64 @@ async function main() {
|
|||||||
}
|
}
|
||||||
console.log('UV downloaded.')
|
console.log('UV downloaded.')
|
||||||
|
|
||||||
|
// ----- sqlite-vec (optional, ANN acceleration) -----
|
||||||
|
try {
|
||||||
|
const binDir = 'src-tauri/resources/bin'
|
||||||
|
const platform = os.platform()
|
||||||
|
const ext = platform === 'darwin' ? 'dylib' : platform === 'win32' ? 'dll' : 'so'
|
||||||
|
const targetLibPath = path.join(binDir, `sqlite-vec.${ext}`)
|
||||||
|
|
||||||
|
if (fs.existsSync(targetLibPath)) {
|
||||||
|
console.log(`sqlite-vec already present at ${targetLibPath}`)
|
||||||
|
} else {
|
||||||
|
let sqlvecUrl = await fetchLatestSqliteVecUrl(platform, os.arch())
|
||||||
|
// Allow override via env if needed
|
||||||
|
if ((process.env.SQLVEC_URL || process.env.JAN_SQLITE_VEC_URL) && !sqlvecUrl) {
|
||||||
|
sqlvecUrl = process.env.SQLVEC_URL || process.env.JAN_SQLITE_VEC_URL
|
||||||
|
}
|
||||||
|
if (!sqlvecUrl) {
|
||||||
|
console.log('Could not determine sqlite-vec download URL; skipping (linear fallback will be used).')
|
||||||
|
} else {
|
||||||
|
console.log(`Downloading sqlite-vec from ${sqlvecUrl}...`)
|
||||||
|
const sqlvecArchive = path.join(tempBinDir, `sqlite-vec-download`)
|
||||||
|
const guessedExt = sqlvecUrl.endsWith('.zip') ? '.zip' : sqlvecUrl.endsWith('.tar.gz') ? '.tar.gz' : ''
|
||||||
|
const archivePath = sqlvecArchive + guessedExt
|
||||||
|
await download(sqlvecUrl, archivePath)
|
||||||
|
if (!guessedExt) {
|
||||||
|
console.log('Unknown archive type for sqlite-vec; expecting .zip or .tar.gz')
|
||||||
|
} else {
|
||||||
|
await decompress(archivePath, tempBinDir)
|
||||||
|
// Try to find a shared library in the extracted files
|
||||||
|
const candidates = []
|
||||||
|
function walk(dir) {
|
||||||
|
for (const entry of fs.readdirSync(dir)) {
|
||||||
|
const full = path.join(dir, entry)
|
||||||
|
const stat = fs.statSync(full)
|
||||||
|
if (stat.isDirectory()) walk(full)
|
||||||
|
else if (full.endsWith(`.${ext}`)) candidates.push(full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(tempBinDir)
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
console.log('No sqlite-vec shared library found in archive; skipping copy.')
|
||||||
|
} else {
|
||||||
|
// Pick the first match and copy/rename to sqlite-vec.<ext>
|
||||||
|
const libSrc = candidates[0]
|
||||||
|
// Ensure we copy the FILE, not a directory (fs-extra copySync can copy dirs)
|
||||||
|
if (fs.statSync(libSrc).isFile()) {
|
||||||
|
fs.copyFileSync(libSrc, targetLibPath)
|
||||||
|
console.log(`sqlite-vec installed at ${targetLibPath}`)
|
||||||
|
} else {
|
||||||
|
console.log(`Found non-file at ${libSrc}; skipping.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('sqlite-vec download step failed (non-fatal):', err)
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Downloads completed.')
|
console.log('Downloads completed.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
console.log('Downloading Windows installer dependencies...')
|
|
||||||
// scripts/download-win-installer-deps.mjs
|
|
||||||
import https from 'https'
|
|
||||||
import fs, { mkdirSync } from 'fs'
|
|
||||||
import os from 'os'
|
|
||||||
import path from 'path'
|
|
||||||
import { copySync } from 'cpx'
|
|
||||||
|
|
||||||
function download(url, dest) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
console.log(`Downloading ${url} to ${dest}`)
|
|
||||||
const file = fs.createWriteStream(dest)
|
|
||||||
https
|
|
||||||
.get(url, (response) => {
|
|
||||||
console.log(`Response status code: ${response.statusCode}`)
|
|
||||||
if (
|
|
||||||
response.statusCode >= 300 &&
|
|
||||||
response.statusCode < 400 &&
|
|
||||||
response.headers.location
|
|
||||||
) {
|
|
||||||
// Handle redirect
|
|
||||||
const redirectURL = response.headers.location
|
|
||||||
console.log(`Redirecting to ${redirectURL}`)
|
|
||||||
download(redirectURL, dest).then(resolve, reject) // Recursive call
|
|
||||||
return
|
|
||||||
} else if (response.statusCode !== 200) {
|
|
||||||
reject(`Failed to get '${url}' (${response.statusCode})`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.pipe(file)
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close(resolve)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
fs.unlink(dest, () => reject(err.message))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('Starting Windows installer dependencies download')
|
|
||||||
const platform = os.platform() // 'darwin', 'linux', 'win32'
|
|
||||||
const arch = os.arch() // 'x64', 'arm64', etc.
|
|
||||||
|
|
||||||
if (arch != 'x64') return
|
|
||||||
|
|
||||||
|
|
||||||
const libDir = 'src-tauri/resources/lib'
|
|
||||||
const tempDir = 'scripts/dist'
|
|
||||||
|
|
||||||
try {
|
|
||||||
mkdirSync('scripts/dist')
|
|
||||||
} catch (err) {
|
|
||||||
// Expect EEXIST error if the directory already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download VC++ Redistributable 17
|
|
||||||
if (platform == 'win32') {
|
|
||||||
const vcFilename = 'vc_redist.x64.exe'
|
|
||||||
const vcUrl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe'
|
|
||||||
|
|
||||||
console.log(`Downloading VC++ Redistributable...`)
|
|
||||||
const vcSavePath = path.join(tempDir, vcFilename)
|
|
||||||
if (!fs.existsSync(vcSavePath)) {
|
|
||||||
await download(vcUrl, vcSavePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy to tauri resources
|
|
||||||
try {
|
|
||||||
copySync(vcSavePath, libDir)
|
|
||||||
} catch (err) {
|
|
||||||
// Expect EEXIST error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Windows installer dependencies downloads completed.')
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error('Error:', err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
2364
src-tauri/Cargo.lock
generated
2364
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,7 @@ mobile = [
|
|||||||
"tauri/protocol-asset",
|
"tauri/protocol-asset",
|
||||||
"tauri/test",
|
"tauri/test",
|
||||||
"tauri/wry",
|
"tauri/wry",
|
||||||
|
"dep:sqlx",
|
||||||
]
|
]
|
||||||
test-tauri = [
|
test-tauri = [
|
||||||
"tauri/wry",
|
"tauri/wry",
|
||||||
@ -59,11 +60,12 @@ hyper = { version = "0.14", features = ["server"] }
|
|||||||
jan-utils = { path = "./utils" }
|
jan-utils = { path = "./utils" }
|
||||||
libloading = "0.8.7"
|
libloading = "0.8.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
reqwest = { version = "0.11", features = ["json", "blocking", "stream", "native-tls-vendored"] }
|
|
||||||
rmcp = { version = "0.6.0", features = [
|
rmcp = { version = "0.6.0", features = [
|
||||||
"client",
|
"client",
|
||||||
"transport-sse-client",
|
"transport-sse-client",
|
||||||
|
"transport-sse-client-reqwest",
|
||||||
"transport-streamable-http-client",
|
"transport-streamable-http-client",
|
||||||
|
"transport-streamable-http-client-reqwest",
|
||||||
"transport-child-process",
|
"transport-child-process",
|
||||||
"tower",
|
"tower",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -77,12 +79,15 @@ tauri-plugin-dialog = "2.2.1"
|
|||||||
tauri-plugin-deep-link = { version = "2", optional = true }
|
tauri-plugin-deep-link = { version = "2", optional = true }
|
||||||
tauri-plugin-hardware = { path = "./plugins/tauri-plugin-hardware", optional = true }
|
tauri-plugin-hardware = { path = "./plugins/tauri-plugin-hardware", optional = true }
|
||||||
tauri-plugin-llamacpp = { path = "./plugins/tauri-plugin-llamacpp" }
|
tauri-plugin-llamacpp = { path = "./plugins/tauri-plugin-llamacpp" }
|
||||||
|
tauri-plugin-vector-db = { path = "./plugins/tauri-plugin-vector-db" }
|
||||||
|
tauri-plugin-rag = { path = "./plugins/tauri-plugin-rag" }
|
||||||
tauri-plugin-http = { version = "2", features = ["unsafe-headers"] }
|
tauri-plugin-http = { version = "2", features = ["unsafe-headers"] }
|
||||||
tauri-plugin-log = "2.0.0-rc"
|
tauri-plugin-log = "2.0.0-rc"
|
||||||
tauri-plugin-opener = "2.2.7"
|
tauri-plugin-opener = "2.2.7"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.2.1"
|
||||||
tauri-plugin-shell = "2.2.0"
|
tauri-plugin-shell = "2.2.0"
|
||||||
tauri-plugin-store = "2"
|
tauri-plugin-store = "2"
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"], optional = true }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-util = "0.7.14"
|
tokio-util = "0.7.14"
|
||||||
@ -105,11 +110,13 @@ libc = "0.2.172"
|
|||||||
windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] }
|
windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] }
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
reqwest = { version = "0.11", features = ["json", "blocking", "stream", "native-tls-vendored"] }
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
once_cell = "1.18"
|
once_cell = "1.18"
|
||||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||||
|
reqwest = { version = "0.11", features = ["json", "blocking", "stream", "rustls-tls"], default-features = false }
|
||||||
tauri-plugin-dialog = { version = "2.2.1", default-features = false }
|
tauri-plugin-dialog = { version = "2.2.1", default-features = false }
|
||||||
tauri-plugin-http = { version = "2", default-features = false }
|
tauri-plugin-http = { version = "2", default-features = false }
|
||||||
tauri-plugin-log = { version = "2.0.0-rc", default-features = false }
|
tauri-plugin-log = { version = "2.0.0-rc", default-features = false }
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"opener:allow-open-url",
|
"opener:allow-open-url",
|
||||||
"store:default",
|
"store:default",
|
||||||
|
"vector-db:default",
|
||||||
|
"rag:default",
|
||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
"allow": [
|
"allow": [
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
"core:webview:allow-set-webview-zoom",
|
"core:webview:allow-set-webview-zoom",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"shell:allow-spawn",
|
"shell:allow-spawn",
|
||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"core:app:allow-set-app-theme",
|
"core:app:allow-set-app-theme",
|
||||||
@ -23,6 +25,8 @@
|
|||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"opener:allow-open-url",
|
"opener:allow-open-url",
|
||||||
"store:default",
|
"store:default",
|
||||||
|
"vector-db:default",
|
||||||
|
"rag:default",
|
||||||
"llamacpp:default",
|
"llamacpp:default",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"hardware:default",
|
"hardware:default",
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "logs-app-window",
|
"identifier": "log-app-window",
|
||||||
"description": "enables permissions for the logs app window",
|
"description": "enables permissions for the logs app window",
|
||||||
"windows": ["logs-app-window"],
|
"windows": ["logs-app-window"],
|
||||||
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"log:default",
|
"log:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
|
"core:webview:allow-get-all-webviews",
|
||||||
"core:window:allow-set-focus"
|
"core:window:allow-set-focus"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,16 @@
|
|||||||
"identifier": "logs-window",
|
"identifier": "logs-window",
|
||||||
"description": "enables permissions for the logs window",
|
"description": "enables permissions for the logs window",
|
||||||
"windows": ["logs-window-local-api-server"],
|
"windows": ["logs-window-local-api-server"],
|
||||||
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"log:default",
|
"log:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
|
"core:webview:allow-get-all-webviews",
|
||||||
"core:window:allow-set-focus"
|
"core:window:allow-set-focus"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,28 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"log:default",
|
"log:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
|
"core:webview:allow-get-all-webviews",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"hardware:allow-get-system-info",
|
"hardware:allow-get-system-info",
|
||||||
"hardware:allow-get-system-usage",
|
"hardware:allow-get-system-usage",
|
||||||
"llamacpp:allow-get-devices",
|
"llamacpp:allow-get-devices",
|
||||||
"llamacpp:allow-read-gguf-metadata",
|
"llamacpp:allow-read-gguf-metadata",
|
||||||
"deep-link:allow-get-current"
|
"deep-link:allow-get-current",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"url": "https://*:*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://*:*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,14 @@ sysinfo = "0.34.2"
|
|||||||
tauri = { version = "2.5.0", default-features = false, features = [] }
|
tauri = { version = "2.5.0", default-features = false, features = [] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.11", features = ["json", "blocking", "stream"] }
|
|
||||||
tauri-plugin-hardware = { path = "../tauri-plugin-hardware" }
|
tauri-plugin-hardware = { path = "../tauri-plugin-hardware" }
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
reqwest = { version = "0.11", features = ["json", "blocking", "stream", "native-tls"] }
|
||||||
|
|
||||||
|
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||||
|
reqwest = { version = "0.11", features = ["json", "blocking", "stream", "rustls-tls"], default-features = false }
|
||||||
|
|
||||||
# Unix-specific dependencies
|
# Unix-specific dependencies
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix = { version = "=0.30.1", features = ["signal", "process"] }
|
nix = { version = "=0.30.1", features = ["signal", "process"] }
|
||||||
|
|||||||
@ -30,12 +30,14 @@ export async function cleanupLlamaProcesses(): Promise<void> {
|
|||||||
export async function loadLlamaModel(
|
export async function loadLlamaModel(
|
||||||
backendPath: string,
|
backendPath: string,
|
||||||
libraryPath?: string,
|
libraryPath?: string,
|
||||||
args: string[] = []
|
args: string[] = [],
|
||||||
|
isEmbedding: boolean = false
|
||||||
): Promise<SessionInfo> {
|
): Promise<SessionInfo> {
|
||||||
return await invoke('plugin:llamacpp|load_llama_model', {
|
return await invoke('plugin:llamacpp|load_llama_model', {
|
||||||
backendPath,
|
backendPath,
|
||||||
libraryPath,
|
libraryPath,
|
||||||
args,
|
args,
|
||||||
|
isEmbedding,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ pub async fn load_llama_model<R: Runtime>(
|
|||||||
library_path: Option<&str>,
|
library_path: Option<&str>,
|
||||||
mut args: Vec<String>,
|
mut args: Vec<String>,
|
||||||
envs: HashMap<String, String>,
|
envs: HashMap<String, String>,
|
||||||
|
is_embedding: bool,
|
||||||
) -> ServerResult<SessionInfo> {
|
) -> ServerResult<SessionInfo> {
|
||||||
let state: State<LlamacppState> = app_handle.state();
|
let state: State<LlamacppState> = app_handle.state();
|
||||||
let mut process_map = state.llama_server_process.lock().await;
|
let mut process_map = state.llama_server_process.lock().await;
|
||||||
@ -223,6 +224,7 @@ pub async fn load_llama_model<R: Runtime>(
|
|||||||
port: port,
|
port: port,
|
||||||
model_id: model_id,
|
model_id: model_id,
|
||||||
model_path: model_path_pb.display().to_string(),
|
model_path: model_path_pb.display().to_string(),
|
||||||
|
is_embedding: is_embedding,
|
||||||
api_key: api_key,
|
api_key: api_key,
|
||||||
mmproj_path: mmproj_path_string,
|
mmproj_path: mmproj_path_string,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -87,19 +87,25 @@ pub async fn is_model_supported(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const RESERVE_BYTES: u64 = 2288490189;
|
const RESERVE_BYTES: u64 = 2288490189;
|
||||||
let total_system_memory = system_info.total_memory * 1024 * 1024;
|
let total_system_memory: u64 = match system_info.gpus.is_empty() {
|
||||||
|
// on MacOS with unified memory, treat RAM = 0 for now
|
||||||
|
true => 0,
|
||||||
|
false => system_info.total_memory * 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate total VRAM from all GPUs
|
// Calculate total VRAM from all GPUs
|
||||||
let total_vram: u64 = if system_info.gpus.is_empty() {
|
let total_vram: u64 = match system_info.gpus.is_empty() {
|
||||||
// On macOS with unified memory, GPU info may be empty
|
// On macOS with unified memory, GPU info may be empty
|
||||||
// Use total RAM as VRAM since memory is shared
|
// Use total RAM as VRAM since memory is shared
|
||||||
log::info!("No GPUs detected (likely unified memory system), using total RAM as VRAM");
|
true => {
|
||||||
total_system_memory
|
log::info!("No GPUs detected (likely unified memory system), using total RAM as VRAM");
|
||||||
} else {
|
system_info.total_memory * 1024 * 1024
|
||||||
system_info
|
}
|
||||||
|
false => system_info
|
||||||
.gpus
|
.gpus
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.total_memory * 1024 * 1024)
|
.map(|g| g.total_memory * 1024 * 1024)
|
||||||
.sum::<u64>()
|
.sum::<u64>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("Total VRAM reported/calculated (in bytes): {}", &total_vram);
|
log::info!("Total VRAM reported/calculated (in bytes): {}", &total_vram);
|
||||||
@ -113,7 +119,7 @@ pub async fn is_model_supported(
|
|||||||
let usable_total_memory = if total_system_memory > RESERVE_BYTES {
|
let usable_total_memory = if total_system_memory > RESERVE_BYTES {
|
||||||
(total_system_memory - RESERVE_BYTES) + usable_vram
|
(total_system_memory - RESERVE_BYTES) + usable_vram
|
||||||
} else {
|
} else {
|
||||||
0
|
usable_vram
|
||||||
};
|
};
|
||||||
log::info!("System RAM: {} bytes", &total_system_memory);
|
log::info!("System RAM: {} bytes", &total_system_memory);
|
||||||
log::info!("Total VRAM: {} bytes", &total_vram);
|
log::info!("Total VRAM: {} bytes", &total_vram);
|
||||||
|
|||||||
@ -80,25 +80,25 @@ pub async fn plan_model_load(
|
|||||||
|
|
||||||
log::info!("Got GPUs:\n{:?}", &sys_info.gpus);
|
log::info!("Got GPUs:\n{:?}", &sys_info.gpus);
|
||||||
|
|
||||||
let total_ram: u64 = sys_info.total_memory * 1024 * 1024;
|
let total_ram: u64 = match sys_info.gpus.is_empty() {
|
||||||
log::info!(
|
// Consider RAM as 0 for unified memory
|
||||||
"Total system memory reported from tauri_plugin_hardware(in bytes): {}",
|
true => 0,
|
||||||
&total_ram
|
false => sys_info.total_memory * 1024 * 1024,
|
||||||
);
|
};
|
||||||
|
|
||||||
let total_vram: u64 = if sys_info.gpus.is_empty() {
|
// Calculate total VRAM from all GPUs
|
||||||
// On macOS with unified memory, GPU info may be empty
|
let total_vram: u64 = match sys_info.gpus.is_empty() {
|
||||||
// Use total RAM as VRAM since memory is shared
|
true => {
|
||||||
log::info!("No GPUs detected (likely unified memory system), using total RAM as VRAM");
|
log::info!("No GPUs detected (likely unified memory system), using total RAM as VRAM");
|
||||||
total_ram
|
sys_info.total_memory * 1024 * 1024
|
||||||
} else {
|
}
|
||||||
sys_info
|
false => sys_info
|
||||||
.gpus
|
.gpus
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| g.total_memory * 1024 * 1024)
|
.map(|g| g.total_memory * 1024 * 1024)
|
||||||
.sum::<u64>()
|
.sum::<u64>(),
|
||||||
};
|
};
|
||||||
|
log::info!("Total RAM reported/calculated (in bytes): {}", &total_ram);
|
||||||
log::info!("Total VRAM reported/calculated (in bytes): {}", &total_vram);
|
log::info!("Total VRAM reported/calculated (in bytes): {}", &total_vram);
|
||||||
let usable_vram: u64 = if total_vram > RESERVE_BYTES {
|
let usable_vram: u64 = if total_vram > RESERVE_BYTES {
|
||||||
(((total_vram - RESERVE_BYTES) as f64) * multiplier) as u64
|
(((total_vram - RESERVE_BYTES) as f64) * multiplier) as u64
|
||||||
|
|||||||
@ -10,6 +10,7 @@ pub struct SessionInfo {
|
|||||||
pub port: i32, // llama-server output port
|
pub port: i32, // llama-server output port
|
||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
pub model_path: String, // path of the loaded model
|
pub model_path: String, // path of the loaded model
|
||||||
|
pub is_embedding: bool,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mmproj_path: Option<String>,
|
pub mmproj_path: Option<String>,
|
||||||
|
|||||||
17
src-tauri/plugins/tauri-plugin-rag/.gitignore
vendored
Normal file
17
src-tauri/plugins/tauri-plugin-rag/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/.vs
|
||||||
|
.DS_Store
|
||||||
|
.Thumbs.db
|
||||||
|
*.sublime*
|
||||||
|
.idea/
|
||||||
|
debug.log
|
||||||
|
package-lock.json
|
||||||
|
.vscode/settings.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
/.tauri
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
dist-js
|
||||||
|
dist
|
||||||
31
src-tauri/plugins/tauri-plugin-rag/Cargo.toml
Normal file
31
src-tauri/plugins/tauri-plugin-rag/Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "tauri-plugin-rag"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Jan <service@jan.ai>"]
|
||||||
|
description = "Tauri plugin for RAG utilities (document parsing, types)"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/menloresearch/jan"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||||
|
links = "tauri-plugin-rag"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2.8.5", default-features = false }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "2.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
log = "0.4"
|
||||||
|
pdf-extract = "0.7"
|
||||||
|
zip = "0.6"
|
||||||
|
quick-xml = { version = "0.31", features = ["serialize"] }
|
||||||
|
csv = "1.3"
|
||||||
|
calamine = "0.23"
|
||||||
|
html2text = "0.11"
|
||||||
|
chardetng = "0.1"
|
||||||
|
encoding_rs = "0.8"
|
||||||
|
infer = "0.15"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-plugin = { version = "2.3.1", features = ["build"] }
|
||||||
7
src-tauri/plugins/tauri-plugin-rag/build.rs
Normal file
7
src-tauri/plugins/tauri-plugin-rag/build.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_plugin::Builder::new(&[
|
||||||
|
"parse_document",
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
6
src-tauri/plugins/tauri-plugin-rag/guest-js/index.ts
Normal file
6
src-tauri/plugins/tauri-plugin-rag/guest-js/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export async function parseDocument(filePath: string, fileType: string): Promise<string> {
|
||||||
|
// Send both snake_case and camelCase for compatibility across runtimes/builds
|
||||||
|
return await invoke('plugin:rag|parse_document', { filePath, fileType })
|
||||||
|
}
|
||||||
33
src-tauri/plugins/tauri-plugin-rag/package.json
Normal file
33
src-tauri/plugins/tauri-plugin-rag/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@janhq/tauri-plugin-rag-api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Guest JS API for Jan RAG plugin",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"main": "./dist-js/index.cjs",
|
||||||
|
"module": "./dist-js/index.js",
|
||||||
|
"exports": {
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"import": "./dist-js/index.js",
|
||||||
|
"require": "./dist-js/index.cjs"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist-js",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"prepublishOnly": "yarn build",
|
||||||
|
"pretest": "yarn build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": ">=2.0.0-beta.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-typescript": "^12.0.0",
|
||||||
|
"rollup": "^4.9.6",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-parse-document"
|
||||||
|
description = "Enables the parse_document command without any pre-configured scope."
|
||||||
|
commands.allow = ["parse_document"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-parse-document"
|
||||||
|
description = "Denies the parse_document command without any pre-configured scope."
|
||||||
|
commands.deny = ["parse_document"]
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
## Default Permission
|
||||||
|
|
||||||
|
Default permissions for the rag plugin
|
||||||
|
|
||||||
|
#### This default permission set includes the following:
|
||||||
|
|
||||||
|
- `allow-parse-document`
|
||||||
|
|
||||||
|
## Permission Table
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Identifier</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`rag:allow-parse-document`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the parse_document command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`rag:deny-parse-document`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the parse_document command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
[default]
|
||||||
|
description = "Default permissions for the rag plugin"
|
||||||
|
permissions = [
|
||||||
|
"allow-parse-document",
|
||||||
|
]
|
||||||
|
|
||||||
@ -0,0 +1,318 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "PermissionFile",
|
||||||
|
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"default": {
|
||||||
|
"description": "The default permission set for the plugin",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/DefaultPermission"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"description": "A list of permissions sets defined",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/PermissionSet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"description": "A list of inlined permissions",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Permission"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"DefaultPermission": {
|
||||||
|
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permissions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"description": "The version of the permission.",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"description": "All permissions this set contains.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PermissionSet": {
|
||||||
|
"description": "A set of direct permissions grouped together under a new name.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"identifier",
|
||||||
|
"permissions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"identifier": {
|
||||||
|
"description": "A unique identifier for the permission.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Human-readable description of what the permission does.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"description": "All permissions this set contains.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/PermissionKind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"identifier"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"description": "The version of the permission.",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"description": "A unique identifier for the permission.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"description": "Allowed or denied commands when using this permission.",
|
||||||
|
"default": {
|
||||||
|
"allow": [],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Commands"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"description": "Allowed or denied scoped when using this permission.",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Scopes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platforms": {
|
||||||
|
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Target"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Commands": {
|
||||||
|
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allow": {
|
||||||
|
"description": "Allowed command.",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deny": {
|
||||||
|
"description": "Denied command, which takes priority.",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Scopes": {
|
||||||
|
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allow": {
|
||||||
|
"description": "Data that defines what is allowed by the scope.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deny": {
|
||||||
|
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"description": "All supported ACL values.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"description": "Represents a null JSON value.",
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a [`bool`].",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a valid ACL [`Number`].",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a [`String`].",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a list of other [`Value`]s.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Number": {
|
||||||
|
"description": "A valid ACL number.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"description": "Represents an [`i64`].",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a [`f64`].",
|
||||||
|
"type": "number",
|
||||||
|
"format": "double"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Target": {
|
||||||
|
"description": "Platform target.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "MacOS.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"macOS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Windows.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"windows"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Linux.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Android.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "iOS.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"iOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PermissionKind": {
|
||||||
|
"type": "string",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Enables the parse_document command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-parse-document",
|
||||||
|
"markdownDescription": "Enables the parse_document command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the parse_document command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-parse-document",
|
||||||
|
"markdownDescription": "Denies the parse_document command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Default permissions for the rag plugin\n#### This default permission set includes:\n\n- `allow-parse-document`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "default",
|
||||||
|
"markdownDescription": "Default permissions for the rag plugin\n#### This default permission set includes:\n\n- `allow-parse-document`"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src-tauri/plugins/tauri-plugin-rag/rollup.config.js
Normal file
32
src-tauri/plugins/tauri-plugin-rag/rollup.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { cwd } from 'node:process'
|
||||||
|
import typescript from '@rollup/plugin-typescript'
|
||||||
|
|
||||||
|
const pkg = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8'))
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'guest-js/index.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: pkg.exports.import,
|
||||||
|
format: 'esm'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: pkg.exports.require,
|
||||||
|
format: 'cjs'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
declaration: true,
|
||||||
|
declarationDir: dirname(pkg.exports.import)
|
||||||
|
})
|
||||||
|
],
|
||||||
|
external: [
|
||||||
|
/^@tauri-apps\/api/,
|
||||||
|
...Object.keys(pkg.dependencies || {}),
|
||||||
|
...Object.keys(pkg.peerDependencies || {})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
12
src-tauri/plugins/tauri-plugin-rag/src/commands.rs
Normal file
12
src-tauri/plugins/tauri-plugin-rag/src/commands.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use crate::{RagError, parser};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn parse_document<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
file_path: String,
|
||||||
|
file_type: String,
|
||||||
|
) -> Result<String, RagError> {
|
||||||
|
log::info!("Parsing document: {} (type: {})", file_path, file_type);
|
||||||
|
let res = parser::parse_document(&file_path, &file_type);
|
||||||
|
res
|
||||||
|
}
|
||||||
20
src-tauri/plugins/tauri-plugin-rag/src/error.rs
Normal file
20
src-tauri/plugins/tauri-plugin-rag/src/error.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
|
||||||
|
pub enum RagError {
|
||||||
|
#[error("Failed to parse document: {0}")]
|
||||||
|
ParseError(String),
|
||||||
|
|
||||||
|
#[error("Unsupported file type: {0}")]
|
||||||
|
UnsupportedFileType(String),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for RagError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
RagError::IoError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
20
src-tauri/plugins/tauri-plugin-rag/src/lib.rs
Normal file
20
src-tauri/plugins/tauri-plugin-rag/src/lib.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use tauri::{
|
||||||
|
plugin::{Builder, TauriPlugin},
|
||||||
|
Runtime,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod parser;
|
||||||
|
mod error;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
pub use error::RagError;
|
||||||
|
|
||||||
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
|
Builder::new("rag")
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::parse_document,
|
||||||
|
])
|
||||||
|
.setup(|_app, _api| Ok(()))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
274
src-tauri/plugins/tauri-plugin-rag/src/parser.rs
Normal file
274
src-tauri/plugins/tauri-plugin-rag/src/parser.rs
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
use crate::RagError;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Read, Cursor};
|
||||||
|
use zip::read::ZipArchive;
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::Reader;
|
||||||
|
use csv as csv_crate;
|
||||||
|
use calamine::{Reader as _, open_workbook_auto, DataType};
|
||||||
|
use html2text;
|
||||||
|
use chardetng::EncodingDetector;
|
||||||
|
use infer;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub fn parse_pdf(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let bytes = fs::read(file_path)?;
|
||||||
|
let text = pdf_extract::extract_text_from_mem(&bytes)
|
||||||
|
.map_err(|e| RagError::ParseError(format!("PDF parse error: {}", e)))?;
|
||||||
|
|
||||||
|
// Validate that the PDF has extractable text (not image-based/scanned)
|
||||||
|
// Count meaningful characters (excluding whitespace)
|
||||||
|
let meaningful_chars = text.chars()
|
||||||
|
.filter(|c| !c.is_whitespace())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Require at least 50 non-whitespace characters to consider it a text PDF
|
||||||
|
// This threshold filters out PDFs that are purely images or scanned documents
|
||||||
|
if meaningful_chars < 50 {
|
||||||
|
return Err(RagError::ParseError(
|
||||||
|
"PDF appears to be image-based or scanned. OCR is not supported yet. Please use a text-based PDF.".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_text(file_path: &str) -> Result<String, RagError> {
|
||||||
|
read_text_auto(file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_document(file_path: &str, file_type: &str) -> Result<String, RagError> {
|
||||||
|
match file_type.to_lowercase().as_str() {
|
||||||
|
"pdf" | "application/pdf" => parse_pdf(file_path),
|
||||||
|
"txt" | "text/plain" | "md" | "text/markdown" => parse_text(file_path),
|
||||||
|
"csv" | "text/csv" => parse_csv(file_path),
|
||||||
|
// Excel family via calamine
|
||||||
|
"xlsx"
|
||||||
|
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
| "xls"
|
||||||
|
| "application/vnd.ms-excel"
|
||||||
|
| "ods"
|
||||||
|
| "application/vnd.oasis.opendocument.spreadsheet" => parse_spreadsheet(file_path),
|
||||||
|
// PowerPoint
|
||||||
|
"pptx"
|
||||||
|
| "application/vnd.openxmlformats-officedocument.presentationml.presentation" => parse_pptx(file_path),
|
||||||
|
// HTML
|
||||||
|
"html" | "htm" | "text/html" => parse_html(file_path),
|
||||||
|
"docx"
|
||||||
|
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => {
|
||||||
|
parse_docx(file_path)
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
// Try MIME sniffing when extension or MIME is unknown
|
||||||
|
if let Ok(Some(k)) = infer::get_from_path(file_path) {
|
||||||
|
let mime = k.mime_type();
|
||||||
|
return parse_document(file_path, mime);
|
||||||
|
}
|
||||||
|
Err(RagError::UnsupportedFileType(other.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_docx(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let file = std::fs::File::open(file_path)?;
|
||||||
|
let mut zip = ZipArchive::new(file).map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
|
||||||
|
// Standard DOCX stores document text at word/document.xml
|
||||||
|
let mut doc_xml = match zip.by_name("word/document.xml") {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => return Err(RagError::ParseError("document.xml not found".into())),
|
||||||
|
};
|
||||||
|
let mut xml_content = String::new();
|
||||||
|
doc_xml
|
||||||
|
.read_to_string(&mut xml_content)
|
||||||
|
.map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
|
||||||
|
// Parse XML and extract text from w:t nodes; add newlines on w:p boundaries
|
||||||
|
let mut reader = Reader::from_str(&xml_content);
|
||||||
|
reader.trim_text(true);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut in_text = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) => {
|
||||||
|
let name: String = reader
|
||||||
|
.decoder()
|
||||||
|
.decode(e.name().as_ref())
|
||||||
|
.unwrap_or(Cow::Borrowed(""))
|
||||||
|
.into_owned();
|
||||||
|
if name.ends_with(":t") || name == "w:t" || name == "t" {
|
||||||
|
in_text = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(e)) => {
|
||||||
|
let name: String = reader
|
||||||
|
.decoder()
|
||||||
|
.decode(e.name().as_ref())
|
||||||
|
.unwrap_or(Cow::Borrowed(""))
|
||||||
|
.into_owned();
|
||||||
|
if name.ends_with(":t") || name == "w:t" || name == "t" {
|
||||||
|
in_text = false;
|
||||||
|
result.push(' ');
|
||||||
|
}
|
||||||
|
if name.ends_with(":p") || name == "w:p" || name == "p" {
|
||||||
|
// Paragraph end – add newline
|
||||||
|
result.push_str("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Text(t)) => {
|
||||||
|
if in_text {
|
||||||
|
let text = t.unescape().unwrap_or_default();
|
||||||
|
result.push_str(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(RagError::ParseError(e.to_string())),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize whitespace
|
||||||
|
let normalized = result
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_csv(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let mut rdr = csv_crate::ReaderBuilder::new()
|
||||||
|
.has_headers(false)
|
||||||
|
.flexible(true)
|
||||||
|
.from_path(file_path)
|
||||||
|
.map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
let mut out = String::new();
|
||||||
|
for rec in rdr.records() {
|
||||||
|
let rec = rec.map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
out.push_str(&rec.iter().collect::<Vec<_>>().join(", "));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_spreadsheet(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let mut workbook = open_workbook_auto(file_path)
|
||||||
|
.map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
let mut out = String::new();
|
||||||
|
for sheet_name in workbook.sheet_names().to_owned() {
|
||||||
|
if let Ok(range) = workbook.worksheet_range(&sheet_name) {
|
||||||
|
out.push_str(&format!("# Sheet: {}\n", sheet_name));
|
||||||
|
for row in range.rows() {
|
||||||
|
let cells = row
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
DataType::Empty => "".to_string(),
|
||||||
|
DataType::String(s) => s.to_string(),
|
||||||
|
DataType::Float(f) => format!("{}", f),
|
||||||
|
DataType::Int(i) => i.to_string(),
|
||||||
|
DataType::Bool(b) => b.to_string(),
|
||||||
|
DataType::DateTime(f) => format!("{}", f),
|
||||||
|
other => other.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\t");
|
||||||
|
out.push_str(&cells);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pptx(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let file = std::fs::File::open(file_path)?;
|
||||||
|
let mut zip = ZipArchive::new(file).map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
|
||||||
|
// Collect slide files: ppt/slides/slide*.xml
|
||||||
|
let mut slides = Vec::new();
|
||||||
|
for i in 0..zip.len() {
|
||||||
|
let name = zip.by_index(i).map(|f| f.name().to_string()).unwrap_or_default();
|
||||||
|
if name.starts_with("ppt/slides/") && name.ends_with(".xml") {
|
||||||
|
slides.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slides.sort();
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
for slide_name in slides {
|
||||||
|
let mut file = zip.by_name(&slide_name).map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
let mut xml = String::new();
|
||||||
|
file.read_to_string(&mut xml).map_err(|e| RagError::ParseError(e.to_string()))?;
|
||||||
|
output.push_str(&extract_pptx_text(&xml));
|
||||||
|
output.push_str("\n\n");
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_pptx_text(xml: &str) -> String {
|
||||||
|
let mut reader = Reader::from_str(xml);
|
||||||
|
reader.trim_text(true);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut in_text = false;
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) => {
|
||||||
|
let name: String = reader
|
||||||
|
.decoder()
|
||||||
|
.decode(e.name().as_ref())
|
||||||
|
.unwrap_or(Cow::Borrowed(""))
|
||||||
|
.into_owned();
|
||||||
|
if name.ends_with(":t") || name == "a:t" || name == "t" {
|
||||||
|
in_text = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(e)) => {
|
||||||
|
let name: String = reader
|
||||||
|
.decoder()
|
||||||
|
.decode(e.name().as_ref())
|
||||||
|
.unwrap_or(Cow::Borrowed(""))
|
||||||
|
.into_owned();
|
||||||
|
if name.ends_with(":t") || name == "a:t" || name == "t" {
|
||||||
|
in_text = false;
|
||||||
|
result.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Text(t)) => {
|
||||||
|
if in_text {
|
||||||
|
let text = t.unescape().unwrap_or_default();
|
||||||
|
result.push_str(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(_) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_html(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let html = read_text_auto(file_path)?;
|
||||||
|
// 80-column wrap default
|
||||||
|
Ok(html2text::from_read(Cursor::new(html), 80))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text_auto(file_path: &str) -> Result<String, RagError> {
|
||||||
|
let bytes = fs::read(file_path)?;
|
||||||
|
// Detect encoding
|
||||||
|
let mut detector = EncodingDetector::new();
|
||||||
|
detector.feed(&bytes, true);
|
||||||
|
let enc = detector.guess(None, true);
|
||||||
|
let (decoded, _, had_errors) = enc.decode(&bytes);
|
||||||
|
if had_errors {
|
||||||
|
// fallback to UTF-8 lossy
|
||||||
|
Ok(String::from_utf8_lossy(&bytes).to_string())
|
||||||
|
} else {
|
||||||
|
Ok(decoded.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src-tauri/plugins/tauri-plugin-rag/tsconfig.json
Normal file
15
src-tauri/plugins/tauri-plugin-rag/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2021",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["guest-js/*.ts"],
|
||||||
|
"exclude": ["dist-js", "node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
17
src-tauri/plugins/tauri-plugin-vector-db/.gitignore
vendored
Normal file
17
src-tauri/plugins/tauri-plugin-vector-db/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/.vs
|
||||||
|
.DS_Store
|
||||||
|
.Thumbs.db
|
||||||
|
*.sublime*
|
||||||
|
.idea/
|
||||||
|
debug.log
|
||||||
|
package-lock.json
|
||||||
|
.vscode/settings.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
/.tauri
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
dist-js
|
||||||
|
dist
|
||||||
25
src-tauri/plugins/tauri-plugin-vector-db/Cargo.toml
Normal file
25
src-tauri/plugins/tauri-plugin-vector-db/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "tauri-plugin-vector-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Jan <service@jan.ai>"]
|
||||||
|
description = "Tauri plugin for vector storage and similarity search"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/menloresearch/jan"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||||
|
links = "tauri-plugin-vector-db"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2.8.5", default-features = false }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "2.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
log = "0.4"
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled", "load_extension"] }
|
||||||
|
uuid = { version = "1.7", features = ["v4", "serde"] }
|
||||||
|
dirs = "6.0.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-plugin = { version = "2.3.1", features = ["build"] }
|
||||||
16
src-tauri/plugins/tauri-plugin-vector-db/build.rs
Normal file
16
src-tauri/plugins/tauri-plugin-vector-db/build.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_plugin::Builder::new(&[
|
||||||
|
"create_collection",
|
||||||
|
"create_file",
|
||||||
|
"insert_chunks",
|
||||||
|
"search_collection",
|
||||||
|
"delete_chunks",
|
||||||
|
"delete_file",
|
||||||
|
"delete_collection",
|
||||||
|
"chunk_text",
|
||||||
|
"get_status",
|
||||||
|
"list_attachments",
|
||||||
|
"get_chunks",
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
}
|
||||||
114
src-tauri/plugins/tauri-plugin-vector-db/guest-js/index.ts
Normal file
114
src-tauri/plugins/tauri-plugin-vector-db/guest-js/index.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export type SearchMode = 'auto' | 'ann' | 'linear'
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
score?: number
|
||||||
|
file_id: string
|
||||||
|
chunk_file_order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
ann_available: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachmentFileInfo {
|
||||||
|
id: string
|
||||||
|
name?: string
|
||||||
|
path?: string
|
||||||
|
type?: string
|
||||||
|
size?: number
|
||||||
|
chunk_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
// Events are not exported in guest-js to keep API minimal
|
||||||
|
|
||||||
|
export async function getStatus(): Promise<Status> {
|
||||||
|
return await invoke('plugin:vector-db|get_status')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCollection(name: string, dimension: number): Promise<void> {
|
||||||
|
// Use camelCase param name `dimension` to match Tauri v2 argument keys
|
||||||
|
return await invoke('plugin:vector-db|create_collection', { name, dimension })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFile(
|
||||||
|
collection: string,
|
||||||
|
file: { path: string; name?: string; type?: string; size?: number }
|
||||||
|
): Promise<AttachmentFileInfo> {
|
||||||
|
return await invoke('plugin:vector-db|create_file', { collection, file })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertChunks(
|
||||||
|
collection: string,
|
||||||
|
fileId: string,
|
||||||
|
chunks: Array<{ text: string; embedding: number[] }>
|
||||||
|
): Promise<void> {
|
||||||
|
return await invoke('plugin:vector-db|insert_chunks', { collection, fileId, chunks })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFile(
|
||||||
|
collection: string,
|
||||||
|
fileId: string
|
||||||
|
): Promise<void> {
|
||||||
|
return await invoke('plugin:vector-db|delete_file', { collection, fileId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchCollection(
|
||||||
|
collection: string,
|
||||||
|
queryEmbedding: number[],
|
||||||
|
limit: number,
|
||||||
|
threshold: number,
|
||||||
|
mode?: SearchMode,
|
||||||
|
fileIds?: string[]
|
||||||
|
): Promise<SearchResult[]> {
|
||||||
|
return await invoke('plugin:vector-db|search_collection', {
|
||||||
|
collection,
|
||||||
|
queryEmbedding,
|
||||||
|
limit,
|
||||||
|
threshold,
|
||||||
|
mode,
|
||||||
|
fileIds,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteChunks(collection: string, ids: string[]): Promise<void> {
|
||||||
|
return await invoke('plugin:vector-db|delete_chunks', { collection, ids })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCollection(collection: string): Promise<void> {
|
||||||
|
return await invoke('plugin:vector-db|delete_collection', { collection })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function chunkText(
|
||||||
|
text: string,
|
||||||
|
chunkSize: number,
|
||||||
|
chunkOverlap: number
|
||||||
|
): Promise<string[]> {
|
||||||
|
// Use snake_case to match Rust command parameter names
|
||||||
|
return await invoke('plugin:vector-db|chunk_text', { text, chunkSize, chunkOverlap })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAttachments(
|
||||||
|
collection: string,
|
||||||
|
limit?: number
|
||||||
|
): Promise<AttachmentFileInfo[]> {
|
||||||
|
return await invoke('plugin:vector-db|list_attachments', { collection, limit })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChunks(
|
||||||
|
collection: string,
|
||||||
|
fileId: string,
|
||||||
|
startOrder: number,
|
||||||
|
endOrder: number
|
||||||
|
): Promise<SearchResult[]> {
|
||||||
|
return await invoke('plugin:vector-db|get_chunks', {
|
||||||
|
collection,
|
||||||
|
fileId,
|
||||||
|
startOrder,
|
||||||
|
endOrder,
|
||||||
|
})
|
||||||
|
}
|
||||||
33
src-tauri/plugins/tauri-plugin-vector-db/package.json
Normal file
33
src-tauri/plugins/tauri-plugin-vector-db/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@janhq/tauri-plugin-vector-db-api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Guest JS API for Jan vector DB plugin",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"main": "./dist-js/index.cjs",
|
||||||
|
"module": "./dist-js/index.js",
|
||||||
|
"exports": {
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"import": "./dist-js/index.js",
|
||||||
|
"require": "./dist-js/index.cjs"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist-js",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"prepublishOnly": "yarn build",
|
||||||
|
"pretest": "yarn build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": ">=2.0.0-beta.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-typescript": "^12.0.0",
|
||||||
|
"rollup": "^4.9.6",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-chunk-text"
|
||||||
|
description = "Enables the chunk_text command without any pre-configured scope."
|
||||||
|
commands.allow = ["chunk_text"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-chunk-text"
|
||||||
|
description = "Denies the chunk_text command without any pre-configured scope."
|
||||||
|
commands.deny = ["chunk_text"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-create-collection"
|
||||||
|
description = "Enables the create_collection command without any pre-configured scope."
|
||||||
|
commands.allow = ["create_collection"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-create-collection"
|
||||||
|
description = "Denies the create_collection command without any pre-configured scope."
|
||||||
|
commands.deny = ["create_collection"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-create-file"
|
||||||
|
description = "Enables the create_file command without any pre-configured scope."
|
||||||
|
commands.allow = ["create_file"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-create-file"
|
||||||
|
description = "Denies the create_file command without any pre-configured scope."
|
||||||
|
commands.deny = ["create_file"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-delete-chunks"
|
||||||
|
description = "Enables the delete_chunks command without any pre-configured scope."
|
||||||
|
commands.allow = ["delete_chunks"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-delete-chunks"
|
||||||
|
description = "Denies the delete_chunks command without any pre-configured scope."
|
||||||
|
commands.deny = ["delete_chunks"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-delete-collection"
|
||||||
|
description = "Enables the delete_collection command without any pre-configured scope."
|
||||||
|
commands.allow = ["delete_collection"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-delete-collection"
|
||||||
|
description = "Denies the delete_collection command without any pre-configured scope."
|
||||||
|
commands.deny = ["delete_collection"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-delete-file"
|
||||||
|
description = "Enables the delete_file command without any pre-configured scope."
|
||||||
|
commands.allow = ["delete_file"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-delete-file"
|
||||||
|
description = "Denies the delete_file command without any pre-configured scope."
|
||||||
|
commands.deny = ["delete_file"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-get-chunks"
|
||||||
|
description = "Enables the get_chunks command without any pre-configured scope."
|
||||||
|
commands.allow = ["get_chunks"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-get-chunks"
|
||||||
|
description = "Denies the get_chunks command without any pre-configured scope."
|
||||||
|
commands.deny = ["get_chunks"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-get-status"
|
||||||
|
description = "Enables the get_status command without any pre-configured scope."
|
||||||
|
commands.allow = ["get_status"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-get-status"
|
||||||
|
description = "Denies the get_status command without any pre-configured scope."
|
||||||
|
commands.deny = ["get_status"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-insert-chunks"
|
||||||
|
description = "Enables the insert_chunks command without any pre-configured scope."
|
||||||
|
commands.allow = ["insert_chunks"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-insert-chunks"
|
||||||
|
description = "Denies the insert_chunks command without any pre-configured scope."
|
||||||
|
commands.deny = ["insert_chunks"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-list-attachments"
|
||||||
|
description = "Enables the list_attachments command without any pre-configured scope."
|
||||||
|
commands.allow = ["list_attachments"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-list-attachments"
|
||||||
|
description = "Denies the list_attachments command without any pre-configured scope."
|
||||||
|
commands.deny = ["list_attachments"]
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-search-collection"
|
||||||
|
description = "Enables the search_collection command without any pre-configured scope."
|
||||||
|
commands.allow = ["search_collection"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-search-collection"
|
||||||
|
description = "Denies the search_collection command without any pre-configured scope."
|
||||||
|
commands.deny = ["search_collection"]
|
||||||
@ -0,0 +1,313 @@
|
|||||||
|
## Default Permission
|
||||||
|
|
||||||
|
Default permissions for the vector-db plugin
|
||||||
|
|
||||||
|
#### This default permission set includes the following:
|
||||||
|
|
||||||
|
- `allow-get-status`
|
||||||
|
- `allow-create-collection`
|
||||||
|
- `allow-insert-chunks`
|
||||||
|
- `allow-create-file`
|
||||||
|
- `allow-search-collection`
|
||||||
|
- `allow-delete-chunks`
|
||||||
|
- `allow-delete-file`
|
||||||
|
- `allow-delete-collection`
|
||||||
|
- `allow-chunk-text`
|
||||||
|
- `allow-list-attachments`
|
||||||
|
- `allow-get-chunks`
|
||||||
|
|
||||||
|
## Permission Table
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Identifier</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-chunk-text`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the chunk_text command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-chunk-text`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the chunk_text command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-create-collection`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the create_collection command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-create-collection`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the create_collection command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-create-file`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the create_file command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-create-file`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the create_file command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-delete-chunks`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the delete_chunks command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-delete-chunks`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the delete_chunks command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-delete-collection`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the delete_collection command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-delete-collection`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the delete_collection command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-delete-file`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the delete_file command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-delete-file`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the delete_file command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-get-chunks`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the get_chunks command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-get-chunks`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the get_chunks command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-get-status`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the get_status command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-get-status`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the get_status command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-insert-chunks`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the insert_chunks command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-insert-chunks`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the insert_chunks command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-list-attachments`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the list_attachments command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-list-attachments`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the list_attachments command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:allow-search-collection`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the search_collection command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`vector-db:deny-search-collection`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the search_collection command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
[default]
|
||||||
|
description = "Default permissions for the vector-db plugin"
|
||||||
|
permissions = [
|
||||||
|
"allow-get-status",
|
||||||
|
"allow-create-collection",
|
||||||
|
"allow-insert-chunks",
|
||||||
|
"allow-create-file",
|
||||||
|
"allow-search-collection",
|
||||||
|
"allow-delete-chunks",
|
||||||
|
"allow-delete-file",
|
||||||
|
"allow-delete-collection",
|
||||||
|
"allow-chunk-text",
|
||||||
|
"allow-list-attachments",
|
||||||
|
"allow-get-chunks",
|
||||||
|
]
|
||||||
@ -0,0 +1,438 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "PermissionFile",
|
||||||
|
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"default": {
|
||||||
|
"description": "The default permission set for the plugin",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/DefaultPermission"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"description": "A list of permissions sets defined",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/PermissionSet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"description": "A list of inlined permissions",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Permission"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"DefaultPermission": {
|
||||||
|
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permissions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"description": "The version of the permission.",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"description": "All permissions this set contains.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PermissionSet": {
|
||||||
|
"description": "A set of direct permissions grouped together under a new name.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"identifier",
|
||||||
|
"permissions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"identifier": {
|
||||||
|
"description": "A unique identifier for the permission.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Human-readable description of what the permission does.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"description": "All permissions this set contains.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/PermissionKind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"identifier"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"description": "The version of the permission.",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"description": "A unique identifier for the permission.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"description": "Allowed or denied commands when using this permission.",
|
||||||
|
"default": {
|
||||||
|
"allow": [],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Commands"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"description": "Allowed or denied scoped when using this permission.",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Scopes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platforms": {
|
||||||
|
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Target"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Commands": {
|
||||||
|
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allow": {
|
||||||
|
"description": "Allowed command.",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deny": {
|
||||||
|
"description": "Denied command, which takes priority.",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Scopes": {
|
||||||
|
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allow": {
|
||||||
|
"description": "Data that defines what is allowed by the scope.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deny": {
|
||||||
|
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"description": "All supported ACL values.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"description": "Represents a null JSON value.",
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a [`bool`].",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a valid ACL [`Number`].",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a [`String`].",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a list of other [`Value`]s.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/Value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Number": {
|
||||||
|
"description": "A valid ACL number.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"description": "Represents an [`i64`].",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Represents a [`f64`].",
|
||||||
|
"type": "number",
|
||||||
|
"format": "double"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Target": {
|
||||||
|
"description": "Platform target.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "MacOS.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"macOS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Windows.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"windows"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Linux.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Android.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "iOS.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"iOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PermissionKind": {
|
||||||
|
"type": "string",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Enables the chunk_text command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-chunk-text",
|
||||||
|
"markdownDescription": "Enables the chunk_text command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the chunk_text command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-chunk-text",
|
||||||
|
"markdownDescription": "Denies the chunk_text command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_collection command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-create-collection",
|
||||||
|
"markdownDescription": "Enables the create_collection command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_collection command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-create-collection",
|
||||||
|
"markdownDescription": "Denies the create_collection command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_file command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-create-file",
|
||||||
|
"markdownDescription": "Enables the create_file command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_file command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-create-file",
|
||||||
|
"markdownDescription": "Denies the create_file command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_chunks command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-delete-chunks",
|
||||||
|
"markdownDescription": "Enables the delete_chunks command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_chunks command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-delete-chunks",
|
||||||
|
"markdownDescription": "Denies the delete_chunks command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_collection command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-delete-collection",
|
||||||
|
"markdownDescription": "Enables the delete_collection command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_collection command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-delete-collection",
|
||||||
|
"markdownDescription": "Denies the delete_collection command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_file command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-delete-file",
|
||||||
|
"markdownDescription": "Enables the delete_file command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_file command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-delete-file",
|
||||||
|
"markdownDescription": "Denies the delete_file command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_chunks command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-get-chunks",
|
||||||
|
"markdownDescription": "Enables the get_chunks command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_chunks command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-get-chunks",
|
||||||
|
"markdownDescription": "Denies the get_chunks command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_status command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-get-status",
|
||||||
|
"markdownDescription": "Enables the get_status command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_status command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-get-status",
|
||||||
|
"markdownDescription": "Denies the get_status command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the insert_chunks command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-insert-chunks",
|
||||||
|
"markdownDescription": "Enables the insert_chunks command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the insert_chunks command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-insert-chunks",
|
||||||
|
"markdownDescription": "Denies the insert_chunks command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the list_attachments command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-list-attachments",
|
||||||
|
"markdownDescription": "Enables the list_attachments command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the list_attachments command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-list-attachments",
|
||||||
|
"markdownDescription": "Denies the list_attachments command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the search_collection command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-search-collection",
|
||||||
|
"markdownDescription": "Enables the search_collection command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the search_collection command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-search-collection",
|
||||||
|
"markdownDescription": "Denies the search_collection command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Default permissions for the vector-db plugin\n#### This default permission set includes:\n\n- `allow-get-status`\n- `allow-create-collection`\n- `allow-insert-chunks`\n- `allow-create-file`\n- `allow-search-collection`\n- `allow-delete-chunks`\n- `allow-delete-file`\n- `allow-delete-collection`\n- `allow-chunk-text`\n- `allow-list-attachments`\n- `allow-get-chunks`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "default",
|
||||||
|
"markdownDescription": "Default permissions for the vector-db plugin\n#### This default permission set includes:\n\n- `allow-get-status`\n- `allow-create-collection`\n- `allow-insert-chunks`\n- `allow-create-file`\n- `allow-search-collection`\n- `allow-delete-chunks`\n- `allow-delete-file`\n- `allow-delete-collection`\n- `allow-chunk-text`\n- `allow-list-attachments`\n- `allow-get-chunks`"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src-tauri/plugins/tauri-plugin-vector-db/rollup.config.js
Normal file
32
src-tauri/plugins/tauri-plugin-vector-db/rollup.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { cwd } from 'node:process'
|
||||||
|
import typescript from '@rollup/plugin-typescript'
|
||||||
|
|
||||||
|
const pkg = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8'))
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'guest-js/index.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: pkg.exports.import,
|
||||||
|
format: 'esm'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: pkg.exports.require,
|
||||||
|
format: 'cjs'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
declaration: true,
|
||||||
|
declarationDir: dirname(pkg.exports.import)
|
||||||
|
})
|
||||||
|
],
|
||||||
|
external: [
|
||||||
|
/^@tauri-apps\/api/,
|
||||||
|
...Object.keys(pkg.dependencies || {}),
|
||||||
|
...Object.keys(pkg.peerDependencies || {})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
206
src-tauri/plugins/tauri-plugin-vector-db/src/commands.rs
Normal file
206
src-tauri/plugins/tauri-plugin-vector-db/src/commands.rs
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
use crate::{VectorDBError, VectorDBState};
|
||||||
|
use crate::db::{
|
||||||
|
self, AttachmentFileInfo, SearchResult, MinimalChunkInput,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub ann_available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct FileInput {
|
||||||
|
pub path: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub file_type: Option<String>,
|
||||||
|
pub size: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tauri Command Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_status(state: State<'_, VectorDBState>) -> Result<Status, VectorDBError> {
|
||||||
|
println!("[VectorDB] Checking ANN availability...");
|
||||||
|
let temp = db::collection_path(&state.base_dir, "__status__");
|
||||||
|
let conn = db::open_or_init_conn(&temp)?;
|
||||||
|
|
||||||
|
// Verbose version for startup diagnostics
|
||||||
|
let ann = {
|
||||||
|
if conn.execute("CREATE VIRTUAL TABLE IF NOT EXISTS temp.temp_vec USING vec0(embedding float[1])", []).is_ok() {
|
||||||
|
let _ = conn.execute("DROP TABLE IF EXISTS temp.temp_vec", []);
|
||||||
|
println!("[VectorDB] ✓ sqlite-vec already loaded");
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
unsafe { let _ = conn.load_extension_enable(); }
|
||||||
|
let paths = db::possible_sqlite_vec_paths();
|
||||||
|
println!("[VectorDB] Trying {} bundled paths...", paths.len());
|
||||||
|
let mut found = false;
|
||||||
|
for p in paths {
|
||||||
|
println!("[VectorDB] Trying: {}", p);
|
||||||
|
unsafe {
|
||||||
|
if let Ok(_) = conn.load_extension(&p, Some("sqlite3_vec_init")) {
|
||||||
|
if conn.execute("CREATE VIRTUAL TABLE IF NOT EXISTS temp.temp_vec USING vec0(embedding float[1])", []).is_ok() {
|
||||||
|
let _ = conn.execute("DROP TABLE IF EXISTS temp.temp_vec", []);
|
||||||
|
println!("[VectorDB] ✓ sqlite-vec loaded from: {}", p);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
println!("[VectorDB] ✗ Failed to load sqlite-vec from all paths");
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("[VectorDB] ANN status: {}", if ann { "AVAILABLE ✓" } else { "NOT AVAILABLE ✗" });
|
||||||
|
Ok(Status { ann_available: ann })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_collection<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
name: String,
|
||||||
|
dimension: usize,
|
||||||
|
) -> Result<(), VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &name);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
|
||||||
|
let has_ann = db::create_schema(&conn, dimension)?;
|
||||||
|
if has_ann {
|
||||||
|
println!("[VectorDB] ✓ Collection '{}' created with ANN support", name);
|
||||||
|
} else {
|
||||||
|
println!("[VectorDB] ⚠ Collection '{}' created WITHOUT ANN support (will use linear search)", name);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_file<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
file: FileInput,
|
||||||
|
) -> Result<AttachmentFileInfo, VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
db::create_file(
|
||||||
|
&conn,
|
||||||
|
&file.path,
|
||||||
|
file.name.as_deref(),
|
||||||
|
file.file_type.as_deref(),
|
||||||
|
file.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn insert_chunks<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
file_id: String,
|
||||||
|
chunks: Vec<MinimalChunkInput>,
|
||||||
|
) -> Result<(), VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
let vec_loaded = db::try_load_sqlite_vec(&conn);
|
||||||
|
db::insert_chunks(&conn, &file_id, chunks, vec_loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_file<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
file_id: String,
|
||||||
|
) -> Result<(), VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
db::delete_file(&conn, &file_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn search_collection<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
query_embedding: Vec<f32>,
|
||||||
|
limit: usize,
|
||||||
|
threshold: f32,
|
||||||
|
mode: Option<String>,
|
||||||
|
file_ids: Option<Vec<String>>,
|
||||||
|
) -> Result<Vec<SearchResult>, VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
let vec_loaded = db::try_load_sqlite_vec(&conn);
|
||||||
|
db::search_collection(&conn, &query_embedding, limit, threshold, mode, vec_loaded, file_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_attachments<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
limit: Option<usize>,
|
||||||
|
) -> Result<Vec<AttachmentFileInfo>, VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
db::list_attachments(&conn, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_chunks<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
ids: Vec<String>,
|
||||||
|
) -> Result<(), VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
db::delete_chunks(&conn, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_collection<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
) -> Result<(), VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(path).ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn chunk_text<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
text: String,
|
||||||
|
chunk_size: usize,
|
||||||
|
chunk_overlap: usize,
|
||||||
|
) -> Result<Vec<String>, VectorDBError> {
|
||||||
|
Ok(db::chunk_text(text, chunk_size, chunk_overlap))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_chunks<R: tauri::Runtime>(
|
||||||
|
_app: tauri::AppHandle<R>,
|
||||||
|
state: State<'_, VectorDBState>,
|
||||||
|
collection: String,
|
||||||
|
file_id: String,
|
||||||
|
start_order: i64,
|
||||||
|
end_order: i64,
|
||||||
|
) -> Result<Vec<SearchResult>, VectorDBError> {
|
||||||
|
let path = db::collection_path(&state.base_dir, &collection);
|
||||||
|
let conn = db::open_or_init_conn(&path)?;
|
||||||
|
db::get_chunks(&conn, file_id, start_order, end_order)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user